diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index 16da5b8dbd..7f976f610d 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -115,16 +115,15 @@ fi ) & PID_PARAM_DOCS=$! -# Example case validation +# Example case validation - validate ALL cases in ONE process (validate accepts multiple files) instead of a +# per-case './mfc.sh validate' loop. Each invocation re-paid the mfc.sh/venv bootstrap (~2.4s), which dominated +# (~90%) the ~6 min serial loop over 155 cases; batching drops it to well under a minute. ( - failed=0 - for case in examples/*/case.py; do - [ -f "$case" ] || continue - if ! ./mfc.sh validate "$case" > /dev/null 2>&1; then - failed=$((failed + 1)) - fi - done - echo "$failed" > "$TMPDIR_PC/examples_exit" + if ./mfc.sh validate examples/*/case.py > /dev/null 2>&1; then + echo "0" > "$TMPDIR_PC/examples_exit" + else + echo "1" > "$TMPDIR_PC/examples_exit" + fi ) & PID_EXAMPLES=$! @@ -194,7 +193,7 @@ EXAMPLES_FAILED=$(cat "$TMPDIR_PC/examples_exit" 2>/dev/null || echo "1") if [ "$EXAMPLES_FAILED" = "0" ]; then ok "All example cases are valid." else - error "$EXAMPLES_FAILED example case(s) failed validation. Run$MAGENTA ./mfc.sh validate examples/\*/case.py$COLOR_RESET for details." + error "Example case validation failed. Run$MAGENTA ./mfc.sh validate examples/\*/case.py$COLOR_RESET for the failing case(s)." FAILED=1 fi diff --git a/toolchain/mfc/args.py b/toolchain/mfc/args.py index 9a3d82f83f..5c68f35ef8 100644 --- a/toolchain/mfc/args.py +++ b/toolchain/mfc/args.py @@ -175,6 +175,9 @@ def custom_error(message): continue if args.get(e) is not None: - args[e] = os.path.abspath(args[e]) + if isinstance(args[e], list): # validate accepts multiple case files (nargs="+") + args[e] = [os.path.abspath(p) for p in args[e]] + else: + args[e] = os.path.abspath(args[e]) return args diff --git a/toolchain/mfc/cli/commands.py b/toolchain/mfc/cli/commands.py index 2fce946693..ca60b53ee3 100644 --- a/toolchain/mfc/cli/commands.py +++ b/toolchain/mfc/cli/commands.py @@ -546,7 +546,8 @@ positionals=[ Positional( name="input", - help="Path to case file to validate.", + help="Path(s) to case file(s) to validate. Multiple files validate in one process (used by precheck).", + nargs="+", completion=Completion(type=CompletionType.FILES_PY), ), ], diff --git a/toolchain/mfc/validate.py b/toolchain/mfc/validate.py index 8e16688aaf..0aa649c9de 100644 --- a/toolchain/mfc/validate.py +++ b/toolchain/mfc/validate.py @@ -12,9 +12,58 @@ from .state import ARG +def _validate_batch(inputs): + """Load + constraint-validate many case files in ONE process. Used by precheck to validate every example + case without re-paying the mfc.sh/venv bootstrap per case (the serial per-case './mfc.sh validate' loop + was ~90% bootstrap overhead). Only a load/parse failure fails the command - constraint issues are warnings - + matching the historical per-file exit behavior (a single './mfc.sh validate' exits non-zero only on load error). + Cases with constraint issues are still surfaced (listed in the summary) so the batched output is not misleading.""" + failed = [] + with_issues = [] + for c in inputs: + if not os.path.isfile(c): + failed.append(c) + continue + try: + case = run_input.load(c, do_print=False) + except MFCException: + failed.append(c) + continue + had_issue = False + for stage in ("pre_process", "simulation", "post_process"): + try: + if CaseValidator(case.params).validate(stage): # non-empty -> warnings + had_issue = True + except CaseConstraintError: + had_issue = True # constraint violation is a warning here, not a load failure + if had_issue: + with_issues.append(c) + n = len(inputs) + if failed: + cons.print(f"[bold red]✗ {len(failed)}/{n} case(s) failed to load:[/bold red]") + for c in failed: + cons.print(f" {c}") + sys.exit(1) + if with_issues: + cons.print(f"[bold green]✓[/bold green] Validated {n} case(s): all loaded " f"([yellow]{len(with_issues)} with constraint issues[/yellow]; " f"run './mfc.sh validate ' for details):") + for c in with_issues: + cons.print(f" [yellow]⚠[/yellow] {c}") + else: + cons.print(f"[bold green]✓[/bold green] Validated {n} case(s): all loaded, no constraint issues.") + + def validate(): - """Validate a case file without building or running.""" - input_file = ARG("input") + """Validate one or more case files without building or running.""" + inputs = ARG("input") + if isinstance(inputs, str): + inputs = [inputs] + if len(inputs) > 1: + if ARG("migrate"): # --migrate rewrites a file in place; only meaningful one case at a time + cons.print("[bold red]Error:[/bold red] --migrate operates on a single case file at a time.") + sys.exit(1) + _validate_batch(inputs) + return + input_file = inputs[0] if not os.path.isfile(input_file): cons.print(f"[bold red]Error:[/bold red] File not found: {input_file}")