Feature/enhanced workflow validation#229
Feature/enhanced workflow validation#229Sahil-u07 wants to merge 5 commits intoControlCore-Project:devfrom
Conversation
- Register terminate_zmq() with atexit to ensure cleanup on normal exit - Add signal handlers for SIGINT (Ctrl+C) and SIGTERM - Improve terminate_zmq() with better logging and error handling - Clear zmq_ports dict after cleanup to prevent double-cleanup
- Add optional --source flag to validate command for file existence checking - Implement cycle detection to identify control loops in workflows - Add ZMQ port conflict detection and validation - Warn about reserved ports (< 1024) and invalid port ranges - Expand test coverage with 6 new comprehensive test cases - Update CLI documentation with new validation features This improves the user experience by catching common configuration errors before workflow execution, reducing runtime failures.
There was a problem hiding this comment.
Pull request overview
Enhances the concore validate CLI command to catch additional workflow configuration issues prior to execution, including optional source-file existence checks, cycle detection warnings, and ZMQ port validation.
Changes:
- Add
--sourceoption toconcore validateand validate referenced node source files when provided. - Add workflow cycle detection (warning) and ZMQ port conflict/reserved-range validation.
- Expand graph validation test coverage to cover the new validation behaviors.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
concore_cli/commands/validate.py |
Adds optional source-file validation plus cycle and ZMQ port validation helpers. |
concore_cli/cli.py |
Adds concore validate --source option and wires it into validate_workflow. |
concore_cli/README.md |
Documents the new validate option and expanded validation checks. |
tests/test_graph.py |
Adds new CLI tests for source-file validation, cycle warnings, and ZMQ port checks. |
concore.py |
Adds signal handling and more robust ZMQ cleanup behavior (not described in PR summary). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
concore_cli/commands/validate.py
Outdated
| if port_num < 1024: | ||
| warnings.append(f"Port {port_num} (0x{port_hex}) is in reserved range (< 1024)") | ||
| elif port_num > 65535: | ||
| errors.append(f"Invalid port number: {port_num} (0x{port_hex}) exceeds maximum (65535)") |
There was a problem hiding this comment.
_check_zmq_ports() treats any port <1024 as a warning, which includes port 0. Port 0 is not a valid TCP port, and the PR description states ports must be within 1–65535. Add an explicit error for port_num < 1 (before the reserved-range warning) so invalid ports are rejected.
concore_cli/cli.py
Outdated
| @cli.command() | ||
| @click.argument('workflow_file', type=click.Path(exists=True)) | ||
| def validate(workflow_file): | ||
| @click.option('--source', '-s', type=click.Path(exists=True), help='Source directory to check file references') |
There was a problem hiding this comment.
The validate --source option uses click.Path(exists=True) but doesn’t restrict the value to a directory, so a file path would be accepted and then used as a “directory” in validation. Configure the option with file_okay=False/dir_okay=True (and optionally path_type=Path) so the CLI enforces a directory input.
| @click.option('--source', '-s', type=click.Path(exists=True), help='Source directory to check file references') | |
| @click.option( | |
| '--source', | |
| '-s', | |
| type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), | |
| help='Source directory to check file references', | |
| ) |
| def _check_source_files(soup, source_path, errors, warnings): | ||
| nodes = soup.find_all('node') | ||
|
|
There was a problem hiding this comment.
_check_source_files() takes a warnings parameter but never uses it. Either remove the unused parameter (and update the call site) or add a warning for skipped/invalid node label formats so callers can understand why some nodes weren’t checked.
| print("\nCleaning up ZMQ resources...") | ||
| for port_name, port in zmq_ports.items(): | ||
| try: | ||
| port.socket.close() | ||
| port.context.term() | ||
| print(f"Closed ZMQ port: {port_name}") | ||
| except Exception as e: | ||
| logging.error(f"Error while terminating ZMQ port {port.address}: {e}") | ||
| zmq_ports.clear() | ||
| _cleanup_in_progress = False | ||
|
|
||
| def signal_handler(sig, frame): | ||
| """Handle interrupt signals gracefully.""" | ||
| print(f"\nReceived signal {sig}, shutting down gracefully...") |
There was a problem hiding this comment.
terminate_zmq()/signal_handler() print directly to stdout (cleanup messages). This can pollute workflow output and makes log handling inconsistent with the rest of the file which uses logging. Prefer logging.info/warning (or gate prints behind a verbosity/TTY check) for these messages.
| def signal_handler(sig, frame): | ||
| """Handle interrupt signals gracefully.""" | ||
| print(f"\nReceived signal {sig}, shutting down gracefully...") | ||
| # Prevent terminate_zmq from being called twice: once here and once via atexit | ||
| try: | ||
| atexit.unregister(terminate_zmq) | ||
| except Exception: | ||
| # If unregister fails for any reason, proceed with explicit cleanup anyway | ||
| pass | ||
| terminate_zmq() | ||
| sys.exit(0) | ||
|
|
||
| # Register cleanup handlers | ||
| atexit.register(terminate_zmq) | ||
| signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C | ||
| if not hasattr(sys, 'getwindowsversion'): | ||
| signal.signal(signal.SIGTERM, signal_handler) # Handle termination (Unix only) | ||
|
|
There was a problem hiding this comment.
The PR description doesn’t mention changes to concore.py, but this PR adds signal handling and alters ZMQ shutdown behavior here. Either update the PR description to include this file/change or split it into a separate PR so the validate-related change set stays focused.
| def test_validate_zmq_port_conflict(self): | ||
| content = ''' | ||
| <graphml xmlns:y="http://www.yworks.com/xml/graphml"> | ||
| <graph id="G" edgedefault="directed"> | ||
| <node id="n0"> | ||
| <data key="d0"><y:NodeLabel>n0:script1.py</y:NodeLabel></data> | ||
| </node> | ||
| <node id="n1"> | ||
| <data key="d0"><y:NodeLabel>n1:script2.py</y:NodeLabel></data> | ||
| </node> | ||
| <edge source="n0" target="n1"> | ||
| <data key="d1"><y:EdgeLabel>0x1234_portA</y:EdgeLabel></data> | ||
| </edge> | ||
| <edge source="n1" target="n0"> | ||
| <data key="d1"><y:EdgeLabel>0x1234_portB</y:EdgeLabel></data> | ||
| </edge> | ||
| </graph> | ||
| </graphml> | ||
| ''' | ||
| filepath = self.create_graph_file('conflict.graphml', content) | ||
|
|
||
| result = self.runner.invoke(cli, ['validate', filepath]) | ||
|
|
||
| self.assertIn('Validation failed', result.output) | ||
| self.assertIn('Port conflict', result.output) | ||
|
|
||
| def test_validate_reserved_port(self): | ||
| content = ''' | ||
| <graphml xmlns:y="http://www.yworks.com/xml/graphml"> | ||
| <graph id="G" edgedefault="directed"> | ||
| <node id="n0"> | ||
| <data key="d0"><y:NodeLabel>n0:script1.py</y:NodeLabel></data> | ||
| </node> | ||
| <node id="n1"> | ||
| <data key="d0"><y:NodeLabel>n1:script2.py</y:NodeLabel></data> | ||
| </node> | ||
| <edge source="n0" target="n1"> | ||
| <data key="d1"><y:EdgeLabel>0x50_data</y:EdgeLabel></data> | ||
| </edge> | ||
| </graph> | ||
| </graphml> | ||
| ''' | ||
| filepath = self.create_graph_file('reserved.graphml', content) | ||
|
|
||
| result = self.runner.invoke(cli, ['validate', filepath]) | ||
|
|
||
| self.assertIn('Port 80', result.output) | ||
| self.assertIn('reserved range', result.output) | ||
|
|
There was a problem hiding this comment.
Port validation tests cover reserved-port warnings and same-port conflicts, but there’s no test for the stated port-range requirement (1–65535). After adding the <1 check (and optionally asserting the existing >65535 behavior), add tests for port 0 (should fail) and a port > 65535 (should fail) to lock in the intended behavior.
- Add explicit validation for port 0 (invalid) - Enforce directory-only input for --source option - Use warnings parameter in _check_source_files for clarity - Add test coverage for port 0 and port > 65535 edge cases - Reorder port validation to check range before conflicts
Summary
Enhanced the
concore validatecommand with comprehensive checking capabilities to catch configuration errors before workflow execution.New Features
--sourceflag to verify referenced files existChanges
concore_cli/commands/validate.py- Added 3 helper functionsconcore_cli/cli.py- Added --source optionconcore_cli/README.md- Updated documentationtests/test_graph.py- Added 6 new test casesTesting
All 13 validation tests pass
Benefits