Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions concore_cli/commands/_source_dir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pathlib import Path


def resolve_source_dir(workflow_path, source_dir):
"""Resolve --source the same way for build, validate and inspect.

A path that exists from the current directory wins, so
`--source demo/src` works from outside the project. Otherwise fall
back to the folder next to the workflow, so the default `src` still
works when the workflow is in a subdirectory.
"""
source = Path(source_dir)
if source.is_absolute() or source.exists():
return source
return Path(workflow_path).parent / source
3 changes: 2 additions & 1 deletion concore_cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn

from ._source_dir import resolve_source_dir
from .metadata import write_study_metadata


Expand Down Expand Up @@ -162,7 +163,7 @@ def build_workflow(
zmq_mode=False,
):
workflow_path = Path(workflow_file).resolve()
source_path = Path(source).resolve()
source_path = resolve_source_dir(workflow_path, source).resolve()
output_path = Path(output).resolve()

if not source_path.exists():
Expand Down
12 changes: 6 additions & 6 deletions concore_cli/commands/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
from collections import defaultdict
import re

from ._source_dir import resolve_source_dir


def inspect_workflow(workflow_file, source_dir, output_json, console):
workflow_path = Path(workflow_file)
source_dir = resolve_source_dir(workflow_path, source_dir)

if output_json:
return _inspect_json(workflow_path, source_dir)
Expand Down Expand Up @@ -61,8 +64,7 @@ def _inspect_rich(workflow_path, source_dir, console):
else:
lang_counts["Other"] += 1

src_dir = workflow_path.parent / source_dir
if not (src_dir / filename).exists():
if not (source_dir / filename).exists():
missing_files.append(filename)

nodes_branch = tree.add(f"Nodes: [bold]{len(nodes)}[/bold]")
Expand Down Expand Up @@ -133,8 +135,7 @@ def _inspect_rich(workflow_path, source_dir, console):
}
lang = lang_map.get(ext, "Other")

src_dir = workflow_path.parent / source_dir
status = "✓" if (src_dir / filename).exists() else "✗"
status = "✓" if (source_dir / filename).exists() else "✗"

table.add_row(node_id, filename, lang, status)

Expand Down Expand Up @@ -220,8 +221,7 @@ def _inspect_json(workflow_path, source_dir):
lang = lang_map.get(ext, "other")
lang_counts[lang] += 1

src_dir = workflow_path.parent / source_dir
exists = (src_dir / filename).exists()
exists = (source_dir / filename).exists()
if not exists:
missing_files.append(filename)

Expand Down
4 changes: 3 additions & 1 deletion concore_cli/commands/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import re
import xml.etree.ElementTree as ET

from ._source_dir import resolve_source_dir


def _classify_message(message, bucket_name):
if bucket_name == "info":
Expand Down Expand Up @@ -132,7 +134,7 @@ def _build_payload(workflow_path, source_root, errors, warnings, info, source_no

def validate_workflow(workflow_file, source_dir, console, output_format="text"):
workflow_path = Path(workflow_file)
source_root = workflow_path.parent / source_dir
source_root = resolve_source_dir(workflow_path, source_dir)

if output_format == "text":
console.print(f"[cyan]Validating:[/cyan] {workflow_path.name}")
Expand Down
61 changes: 61 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,30 @@ def test_validate_json_output_for_missing_source_file(self):
self.assertEqual(payload["errors"][0]["error_type"], "missing_source_file")
self.assertEqual(payload["errors"][0]["node_id"], "n1")

def test_validate_source_relative_to_cwd(self):
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
result = self.runner.invoke(cli, ["init", "test-project"])
self.assertEqual(result.exit_code, 0)

Path("test-project/src/script.py").unlink()

result = self.runner.invoke(
cli,
[
"validate",
"test-project/workflow.graphml",
"--source",
"test-project/src",
"--format",
"json",
],
)
self.assertNotEqual(result.exit_code, 0)

payload = json.loads(result.output)
self.assertEqual(payload["errors"][0]["error_type"], "missing_source_file")
self.assertEqual(Path(payload["source_dir"]), Path("test-project/src"))

def test_validate_json_output_for_node_without_label(self):
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
Path("src").mkdir()
Expand Down Expand Up @@ -178,6 +202,25 @@ def test_build_command_missing_source(self):
)
self.assertNotEqual(result.exit_code, 0)

def test_build_command_default_source_next_to_workflow(self):
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
result = self.runner.invoke(cli, ["init", "test-project"])
self.assertEqual(result.exit_code, 0)

result = self.runner.invoke(
cli,
[
"build",
"test-project/workflow.graphml",
"--output",
"out",
"--type",
"posix",
],
)
self.assertEqual(result.exit_code, 0, result.output)
self.assertTrue(Path("out/src/script.py").exists())

def test_build_command_rejects_edge_label_with_multiple_sources(self):
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
Path("src").mkdir()
Expand Down Expand Up @@ -888,6 +931,24 @@ def test_inspect_missing_source_file(self):
self.assertEqual(result.exit_code, 0)
self.assertIn("Missing files", result.output)

def test_inspect_source_relative_to_cwd(self):
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
result = self.runner.invoke(cli, ["init", "test-project"])
self.assertEqual(result.exit_code, 0)

result = self.runner.invoke(
cli,
[
"inspect",
"test-project/workflow.graphml",
"--source",
"test-project/src",
"--json",
],
)
self.assertEqual(result.exit_code, 0)
self.assertEqual(json.loads(result.output)["missing_files"], [])

def test_watch_rejects_negative_interval(self):
with self.runner.isolated_filesystem(temp_dir=self.temp_dir):
os.mkdir("study")
Expand Down
Loading