#!/usr/bin/env python3 """ Verify that a scenario directory is complete and ready for Curtin ICS-SimLab. Checks: 1. configuration.json exists 2. logic/ directory exists 3. All logic files referenced in config exist in logic/ 4. (Optional) Run validate_logic checks Usage: python3 -m tools.verify_scenario --scenario outputs/scenario_run """ from __future__ import annotations import argparse import json from pathlib import Path from typing import List, Set, Tuple def get_logic_files_from_config(config: dict) -> Tuple[Set[str], Set[str]]: """ Extract logic filenames referenced in configuration. Returns: (plc_logic_files, hil_logic_files) """ plc_files: Set[str] = set() hil_files: Set[str] = set() for plc in config.get("plcs", []): logic = plc.get("logic", "") if logic: plc_files.add(logic) for hil in config.get("hils", []): logic = hil.get("logic", "") if logic: hil_files.add(logic) return plc_files, hil_files def verify_scenario(scenario_dir: Path) -> Tuple[bool, List[str]]: """ Verify scenario directory is complete. Returns: (success: bool, errors: List[str]) """ errors: List[str] = [] # Check configuration.json exists config_path = scenario_dir / "configuration.json" if not config_path.exists(): errors.append(f"Missing: {config_path}") return False, errors # Load config try: config = json.loads(config_path.read_text(encoding="utf-8")) except Exception as e: errors.append(f"Invalid JSON in {config_path}: {e}") return False, errors # Check logic/ directory exists logic_dir = scenario_dir / "logic" if not logic_dir.exists(): errors.append(f"Missing directory: {logic_dir}") return False, errors # Check all referenced logic files exist plc_files, hil_files = get_logic_files_from_config(config) all_files = plc_files | hil_files for fname in sorted(all_files): fpath = logic_dir / fname if not fpath.exists(): errors.append(f"Missing logic file: {fpath} (referenced in config)") # Check for orphan logic files (warning only) existing_files = {f.name for f in logic_dir.glob("*.py")} orphans = existing_files - all_files if orphans: # Not an error, just informational pass success = len(errors) == 0 return success, errors def main() -> None: parser = argparse.ArgumentParser( description="Verify scenario directory is complete for ICS-SimLab" ) parser.add_argument( "--scenario", required=True, help="Path to scenario directory (e.g., outputs/scenario_run)", ) parser.add_argument( "--verbose", "-v", action="store_true", help="Show detailed information", ) args = parser.parse_args() scenario_dir = Path(args.scenario) if not scenario_dir.exists(): raise SystemExit(f"ERROR: Scenario directory not found: {scenario_dir}") print(f"Verifying scenario: {scenario_dir}") print() success, errors = verify_scenario(scenario_dir) if args.verbose or success: # Show contents config_path = scenario_dir / "configuration.json" logic_dir = scenario_dir / "logic" if config_path.exists(): config = json.loads(config_path.read_text(encoding="utf-8")) plc_files, hil_files = get_logic_files_from_config(config) print("Configuration:") print(f" PLCs: {len(config.get('plcs', []))}") print(f" HILs: {len(config.get('hils', []))}") print(f" Sensors: {len(config.get('sensors', []))}") print(f" Actuators: {len(config.get('actuators', []))}") print() print("Logic files referenced:") for f in sorted(plc_files): status = "OK" if (logic_dir / f).exists() else "MISSING" print(f" [PLC] {f}: {status}") for f in sorted(hil_files): status = "OK" if (logic_dir / f).exists() else "MISSING" print(f" [HIL] {f}: {status}") print() # Show orphans if logic_dir.exists(): existing = {f.name for f in logic_dir.glob("*.py")} orphans = existing - (plc_files | hil_files) if orphans: print("Orphan files (not referenced in config):") for f in sorted(orphans): print(f" {f}") print() if errors: print(f"VERIFICATION FAILED: {len(errors)} error(s)") for err in errors: print(f" - {err}") raise SystemExit(1) else: print("VERIFICATION PASSED: Scenario is complete") print() print("To run with ICS-SimLab:") print(f" cd ~/projects/ICS-SimLab-main/curtin-ics-simlab") print(f" sudo ./start.sh {scenario_dir.absolute()}") if __name__ == "__main__": main()