| 1 | #!/usr/bin/env python3
|
| 2 | """Run local VCS correctness diagnostics; stdout is diagnostic JSONL, not timing rows.
|
| 3 |
|
| 4 | Suites: ``legacy`` (the five v1 restore/roundtrip scenarios), ``matrix`` (the
|
| 5 | roundtrip conformance matrix, commit -> push -> fresh clone/pull, compared via
|
| 6 | the independent reader and filesystem bytes), ``all`` (both, the default).
|
| 7 |
|
| 8 | ``--gate`` evaluates the Oak rows against config/conformance_expected_failures.json
|
| 9 | and exits 0 PASS / 1 REGRESSED / 2 UNMEASURED (the devloop convention).
|
| 10 | Without ``--gate`` the exit code is 1 when any row failed, else 0.
|
| 11 | """
|
| 12 | import argparse
|
| 13 | import json
|
| 14 | from pathlib import Path
|
| 15 | import sys
|
| 16 |
|
| 17 | from oakbench.conformance import SCENARIOS, run_scenario
|
| 18 | from oakbench import conformance_gate
|
| 19 | from oakbench.conformance_matrix import BEHAVIOURS, BY_ID, run_matrix
|
| 20 |
|
| 21 |
|
| 22 | def main():
|
| 23 | parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 24 | parser.add_argument("--workdir", type=Path, required=False)
|
| 25 | parser.add_argument("--subjects", default="git,oak")
|
| 26 | parser.add_argument("--git-bin", default="git")
|
| 27 | parser.add_argument("--oak-bin", default="oak")
|
| 28 | parser.add_argument("--suite", choices=("legacy", "matrix", "all"), default="all")
|
| 29 | parser.add_argument("--scenarios", default=",".join(SCENARIOS), help="legacy scenarios")
|
| 30 | parser.add_argument("--behaviours", default="", help="comma list of matrix behaviour IDs (default: all)")
|
| 31 | parser.add_argument("--include-gated", action="store_true", help="also run gated behaviours (huge files)")
|
| 32 | parser.add_argument("--output", type=Path, help="also write the JSONL rows here")
|
| 33 | parser.add_argument("--quiet", action="store_true", help="do not echo rows on stdout (use with --output)")
|
| 34 | parser.add_argument("--gate", action="store_true", help="exit by the conformance gate verdict")
|
| 35 | parser.add_argument("--expected-failures", type=Path, default=conformance_gate.EXPECTED_FAILURES)
|
| 36 | parser.add_argument("--list", action="store_true", help="print the behaviour catalog as JSONL and exit")
|
| 37 | args = parser.parse_args()
|
| 38 | if args.list:
|
| 39 | for behaviour in BEHAVIOURS:
|
| 40 | print(json.dumps(behaviour.describe(), sort_keys=True))
|
| 41 | return 0
|
| 42 | if args.workdir is None:
|
| 43 | parser.error("--workdir is required")
|
| 44 | subjects = args.subjects.split(",")
|
| 45 | scenarios = [s for s in args.scenarios.split(",") if s]
|
| 46 | behaviours = [b for b in args.behaviours.split(",") if b]
|
| 47 | if (not set(subjects) <= {"git", "oak"} or not set(scenarios) <= set(SCENARIOS)
|
| 48 | or not set(behaviours) <= set(BY_ID)):
|
| 49 | parser.error("unknown subject, scenario or behaviour")
|
| 50 | rows = []
|
| 51 | if args.output:
|
| 52 | args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 53 | sink = args.output.open("w") if args.output else None
|
| 54 |
|
| 55 | def emit(row):
|
| 56 | line = json.dumps(row, sort_keys=True)
|
| 57 | if not args.quiet:
|
| 58 | print(line, flush=True)
|
| 59 | if sink:
|
| 60 | sink.write(line + "\n")
|
| 61 | sink.flush()
|
| 62 | rows.append(row)
|
| 63 |
|
| 64 | try:
|
| 65 | if args.suite in ("legacy", "all"):
|
| 66 | for subject in subjects:
|
| 67 | for scenario in scenarios:
|
| 68 | emit(run_scenario(args.workdir.resolve(), subject, getattr(args, f"{subject}_bin"), scenario))
|
| 69 | if args.suite in ("matrix", "all"):
|
| 70 | run_matrix(args.workdir.resolve() / "matrix", {s: getattr(args, f"{s}_bin") for s in subjects},
|
| 71 | behaviours or None, include_gated=args.include_gated or None, progress=emit)
|
| 72 | finally:
|
| 73 | if sink:
|
| 74 | sink.close()
|
| 75 | if args.gate:
|
| 76 | expected = conformance_gate.load_expected(args.expected_failures)
|
| 77 | required = []
|
| 78 | if "oak" in subjects:
|
| 79 | if args.suite in ("legacy", "all"):
|
| 80 | required += scenarios
|
| 81 | if args.suite in ("matrix", "all"):
|
| 82 | required += behaviours or [b.id for b in BEHAVIOURS]
|
| 83 | result = conformance_gate.evaluate(rows, expected, required)
|
| 84 | print("\n".join(conformance_gate.summary_lines(result)), file=sys.stderr)
|
| 85 | return {"PASS": 0, "REGRESSED": 1}.get(result["verdict"], 2)
|
| 86 | return int(any(row["returncode"] == 1 for row in rows))
|
| 87 |
|
| 88 |
|
| 89 | if __name__ == "__main__":
|
| 90 | raise SystemExit(main())
|