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'])