diff --git a/concore_cli/README.md b/concore_cli/README.md
index e29c7657..b0da5057 100644
--- a/concore_cli/README.md
+++ b/concore_cli/README.md
@@ -79,6 +79,9 @@ Checks:
- File references and naming conventions
- ZMQ vs file-based communication
+**Options:**
+- `-s, --source
` - Source directory (default: src)
+
**Example:**
```bash
concore validate workflow.graphml
diff --git a/concore_cli/cli.py b/concore_cli/cli.py
index 6e9ed076..615cb7b9 100644
--- a/concore_cli/cli.py
+++ b/concore_cli/cli.py
@@ -45,10 +45,13 @@ def run(workflow_file, source, output, type, auto_build):
@cli.command()
@click.argument('workflow_file', type=click.Path(exists=True))
-def validate(workflow_file):
+@click.option('--source', '-s', default='src', help='Source directory')
+def validate(workflow_file, source):
"""Validate a workflow file"""
try:
- validate_workflow(workflow_file, console)
+ ok = validate_workflow(workflow_file, source, console)
+ if not ok:
+ sys.exit(1)
except Exception as e:
console.print(f"[red]Error:[/red] {str(e)}")
sys.exit(1)
diff --git a/concore_cli/commands/validate.py b/concore_cli/commands/validate.py
index fa1ea184..74cd7900 100644
--- a/concore_cli/commands/validate.py
+++ b/concore_cli/commands/validate.py
@@ -5,8 +5,9 @@
import re
import xml.etree.ElementTree as ET
-def validate_workflow(workflow_file, console):
+def validate_workflow(workflow_file, source_dir, console):
workflow_path = Path(workflow_file)
+ source_root = (workflow_path.parent / source_dir)
console.print(f"[cyan]Validating:[/cyan] {workflow_path.name}")
console.print()
@@ -15,31 +16,35 @@ def validate_workflow(workflow_file, console):
warnings = []
info = []
+ def finalize():
+ show_results(console, errors, warnings, info)
+ return len(errors) == 0
+
try:
with open(workflow_path, 'r') as f:
content = f.read()
if not content.strip():
errors.append("File is empty")
- return show_results(console, errors, warnings, info)
+ return finalize()
# strict XML syntax check
try:
ET.fromstring(content)
except ET.ParseError as e:
errors.append(f"Invalid XML: {str(e)}")
- return show_results(console, errors, warnings, info)
+ return finalize()
try:
soup = BeautifulSoup(content, 'xml')
except Exception as e:
errors.append(f"Invalid XML: {str(e)}")
- return show_results(console, errors, warnings, info)
+ return finalize()
root = soup.find('graphml')
if not root:
errors.append("Not a valid GraphML file - missing root element")
- return show_results(console, errors, warnings, info)
+ return finalize()
# check the graph attributes
graph = soup.find('graph')
@@ -64,6 +69,9 @@ def validate_workflow(workflow_file, console):
warnings.append("No edges found in workflow")
else:
info.append(f"Found {len(edges)} edge(s)")
+
+ if not source_root.exists():
+ warnings.append(f"Source directory not found: {source_root}")
node_labels = []
for node in nodes:
@@ -96,6 +104,10 @@ def validate_workflow(workflow_file, console):
errors.append(f"Node '{label}' has no filename")
elif not any(filename.endswith(ext) for ext in ['.py', '.cpp', '.m', '.v', '.java']):
warnings.append(f"Node '{label}' has unusual file extension")
+ elif source_root.exists():
+ file_path = source_root / filename
+ if not file_path.exists():
+ errors.append(f"Missing source file: {filename}")
else:
warnings.append(f"Node {node_id} has no label")
except Exception as e:
@@ -138,12 +150,14 @@ def validate_workflow(workflow_file, console):
if file_edges > 0:
info.append(f"File-based edges: {file_edges}")
- show_results(console, errors, warnings, info)
-
+ return finalize()
+
except FileNotFoundError:
console.print(f"[red]Error:[/red] File not found: {workflow_path}")
+ return False
except Exception as e:
console.print(f"[red]Validation failed:[/red] {str(e)}")
+ return False
def show_results(console, errors, warnings, info):
if errors:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 6aa78109..8d5a3994 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -58,6 +58,19 @@ def test_validate_valid_file(self):
result = self.runner.invoke(cli, ['validate', 'test-project/workflow.graphml'])
self.assertEqual(result.exit_code, 0)
self.assertIn('Validation passed', result.output)
+
+ def test_validate_missing_node_file(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)
+
+ missing_file = Path('test-project/src/script.py')
+ if missing_file.exists():
+ missing_file.unlink()
+
+ result = self.runner.invoke(cli, ['validate', 'test-project/workflow.graphml'])
+ self.assertNotEqual(result.exit_code, 0)
+ self.assertIn('Missing source file', result.output)
def test_status_command(self):
result = self.runner.invoke(cli, ['status'])