From f6b808b1d98c1ddb8f1988a47673c8f6c1107234 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:36:35 +0200 Subject: [PATCH] feat: let verify fail on a missing required file --- scripts/pipeline.py | 2 +- scripts/verify.py | 26 +++++++++++++++++++++++++ tests/test_system_selection.py | 35 ++++++++++++++++++++++++++++++++++ wiki/tools.md | 1 + 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/scripts/pipeline.py b/scripts/pipeline.py index e142d25d..535b4f0b 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -450,7 +450,7 @@ def main(): all_ok = _truth_steps(args, results) and all_ok # Step 3: Verify - verify_cmd = [sys.executable, "scripts/verify.py", "--all"] + verify_cmd = [sys.executable, "scripts/verify.py", "--all", "--strict"] if args.include_archived: verify_cmd.append("--include-archived") if args.target: diff --git a/scripts/verify.py b/scripts/verify.py index 09824202..a7d3b74c 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -2064,6 +2064,11 @@ def main(): help="Show emulator ground truth details", ) parser.add_argument("--json", action="store_true", help="JSON output") + parser.add_argument( + "--strict", + action="store_true", + help="Exit 1 when a platform that checks hashes misses a required file", + ) args = parser.parse_args() if args.platform: require_platform(args.platform, args.platforms_dir) @@ -2190,12 +2195,33 @@ def main(): for result, group in group_results: print_platform_result(result, group, verbose=args.verbose) print() + if len(group_results) > 1: + print(_overall_summary(group_results)) if args.json: for r in all_results.values(): r["details"] = [d for d in r["details"] if d["status"] != Status.OK] print(json.dumps(all_results, indent=2)) + critical = sum(r["severity_counts"].get(Severity.CRITICAL, 0) for r, _ in group_results) + if args.strict and critical: + print(f"ERROR: {critical} required file(s) missing where the frontend " + "checks hashes", file=sys.stderr) + sys.exit(1) + + +def _overall_summary(group_results: list[tuple[dict, list[str]]]) -> str: + """One closing line for a multi-platform run, worst severity first.""" + platforms = sum(len(group) for _, group in group_results) + totals = { + sev: sum(r["severity_counts"].get(sev, 0) for r, _ in group_results) + for sev in (Severity.CRITICAL, Severity.WARNING, Severity.INFO) + } + return ( + f"{platforms} platforms: {totals[Severity.CRITICAL]} critical, " + f"{totals[Severity.WARNING]} warning, {totals[Severity.INFO]} info" + ) + if __name__ == "__main__": main() diff --git a/tests/test_system_selection.py b/tests/test_system_selection.py index 54d6b3f4..f0b2c878 100644 --- a/tests/test_system_selection.py +++ b/tests/test_system_selection.py @@ -111,5 +111,40 @@ class AMistypedPlatformIsNamed(unittest.TestCase): self.assertIn("Did you mean 'batocera'?", proc.stderr) self.assertFalse(out.exists()) + +class StrictVerifyFailsOnACriticalMiss(unittest.TestCase): + """verify exited 0 with a required file missing on an md5 platform, so + the pipeline printed "verify OK" over a pack the frontend rejects.""" + + def test_strict_exits_nonzero_and_plain_does_not(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp: + root = Path(tmp) + (root / "plat").mkdir() + (root / "emu").mkdir() + (root / "plat" / "fx.yml").write_text( + "platform: Fx\nverification_mode: md5\nbase_destination: bios\n" + "systems:\n sony-playstation:\n files:\n - name: absent.bin\n" + " destination: absent.bin\n required: true\n" + " md5: 00000000000000000000000000000000\n" + ) + (root / "db.json").write_text( + '{"files": {}, "indexes": {"by_md5": {}, "by_name": {}, ' + '"by_crc32": {}, "by_path_suffix": {}}}' + ) + base = [sys.executable, "scripts/verify.py", "--platform", "fx", + "--platforms-dir", str(root / "plat"), "--emulators-dir", + str(root / "emu"), "--db", str(root / "db.json")] + plain = subprocess.run(base, capture_output=True, text=True, + cwd=str(REPO_ROOT), timeout=300, check=False) + strict = subprocess.run([*base, "--strict"], capture_output=True, text=True, + cwd=str(REPO_ROOT), timeout=300, check=False) + self.assertEqual(plain.returncode, 0, plain.stderr) + self.assertEqual(strict.returncode, 1, strict.stderr) + self.assertIn("1 required file(s) missing", strict.stderr) + + def test_the_pipeline_asks_for_it(self): + source = (REPO_ROOT / "scripts" / "pipeline.py").read_text() + self.assertIn('"scripts/verify.py", "--all", "--strict"', source) + if __name__ == "__main__": unittest.main() diff --git a/wiki/tools.md b/wiki/tools.md index f14feb13..02295c30 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -78,6 +78,7 @@ Check BIOS coverage for each platform using its native verification mode. ```bash python scripts/verify.py --all # all platforms +python scripts/verify.py --all --strict # exit 1 on a missing required file (md5/sha1 platforms) python scripts/verify.py --platform batocera # single platform python scripts/verify.py --platform retroarch --verbose # with ground truth details python scripts/verify.py --emulator dolphin # single emulator