From 556e632efb120554da1df7e4ecec735cde3ec1af Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:41:06 +0200 Subject: [PATCH] fix: one held release fills a slot in emulator verify --- scripts/verify.py | 43 +++++++++++++++++++++----------- tests/test_e2e.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/scripts/verify.py b/scripts/verify.py index 4559065d..04e487d9 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1467,6 +1467,12 @@ def verify_emulator( ) > _SEVERITY_ORDER.get(prev_sev, 0): file_severity[dest] = sev + # Results of this emulator, by destination. Several entries of one + # profile on one destination are the releases the code accepts + # there (a shareware and a retail HOG, each GRP in a size table): + # holding one of them fills the slot, and the others are not gaps. + emu_results: dict[str, list[tuple[dict, bool, bool]]] = {} + for file_entry in files: # Skip archived files (verified as archive units above) if file_entry.get("archive"): @@ -1545,21 +1551,30 @@ def verify_emulator( result["system"] = file_entry.get("system", "") result["hle_fallback"] = hle result["ground_truth"] = build_ground_truth(name, validation_index) - details.append(result) - - # Aggregate by destination (path if available, else name) dest = file_entry.get("path", "") or name - dest_to_name[dest] = name - cur = result["status"] - prev = file_status.get(dest) - if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get(prev, 0): - file_status[dest] = cur - sev = compute_severity(cur, required, "existence", hle) - prev_sev = file_severity.get(dest) - if prev_sev is None or _SEVERITY_ORDER.get(sev, 0) > _SEVERITY_ORDER.get( - prev_sev, 0 - ): - file_severity[dest] = sev + emu_results.setdefault(dest, []).append((result, required, hle)) + + for dest, alternatives in emu_results.items(): + held = [a for a in alternatives if a[0]["status"] == Status.OK] + if held and len(alternatives) > 1: + alternatives = held[:1] + for result, required, hle in alternatives: + details.append(result) + + # Aggregate by destination (path if available, else name) + dest_to_name[dest] = result["name"] + cur = result["status"] + prev = file_status.get(dest) + if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get( + prev, 0 + ): + file_status[dest] = cur + sev = compute_severity(cur, required, "existence", hle) + prev_sev = file_severity.get(dest) + if prev_sev is None or _SEVERITY_ORDER.get( + sev, 0 + ) > _SEVERITY_ORDER.get(prev_sev, 0): + file_severity[dest] = sev counts = { Severity.OK: 0, diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 2f3a3508..05e819b9 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -1058,6 +1058,30 @@ class TestE2E(unittest.TestCase): self.assertEqual(len(groups), 1) self.assertEqual(len(groups[0][0]), 2) + def test_52_platform_grouping_splits_on_cores(self): + """Same baseline, different emulators: the core files differ.""" + declared = { + "cores_all": "all_libretro", + "cores_all_too": "all_libretro", + "cores_listed": ["retroarch", "openmsx"], + } + for name, cores in declared.items(): + config = { + "platform": name, + "verification_mode": "existence", + "cores": cores, + "systems": { + "s": {"files": [{"name": "x.bin", "destination": "x.bin"}]} + }, + } + with open(os.path.join(self.platforms_dir, f"{name}.yml"), "w") as fh: + yaml.dump(config, fh) + groups = group_identical_platforms(list(declared), self.platforms_dir) + self.assertEqual( + sorted(sorted(group) for group, _rep in groups), + [["cores_all", "cores_all_too"], ["cores_listed"]], + ) + def test_60_storage_external(self): from generate_pack import resolve_file @@ -1454,6 +1478,45 @@ class TestE2E(unittest.TestCase): ["shared-dir", "standalone-only-dir"], ) + def test_97c_one_held_release_fills_a_slot_several_are_accepted_for(self): + """Entries of one profile on one destination are alternatives. + + A profile lists each release the code accepts under a name (the + shareware and the retail archive, told apart by size). Holding one + is the slot filled: the releases not held are not gaps and must not + drag the destination down to the worst status among them. + """ + present = os.path.join(self.bios_dir, "present_req.bin") + size = os.path.getsize(present) + profile = { + "emulator": "Releases", + "type": "standalone", + "cores": ["releases"], + "systems": ["console-a"], + "files": [ + {"name": "present_req.bin", "path": "game/present_req.bin", + "required": False, "size": size + 7, "validation": ["size"], + "description": "retail"}, + {"name": "present_req.bin", "path": "game/present_req.bin", + "required": False, "size": size, "validation": ["size"], + "description": "shareware"}, + {"name": "absent_everywhere.bin", "path": "game/absent.bin", + "required": False}, + ], + } + with open(os.path.join(self.emulators_dir, "releases.yml"), "w") as fh: + yaml.dump(profile, fh) + result = verify_emulator( + ["releases"], self.emulators_dir, self.db, standalone=True + ) + statuses = {d["name"]: d["status"] for d in result["details"]} + self.assertEqual(statuses["present_req.bin"], "ok") + self.assertEqual(statuses["absent_everywhere.bin"], "missing") + self.assertEqual( + sum(1 for d in result["details"] if d["name"] == "present_req.bin"), 1 + ) + self.assertEqual(result["total_files"], 2) + def test_98_verify_emulator_validation_label(self): """Validation label reflects the checks used.""" result = verify_emulator(["test_validation"], self.emulators_dir, self.db)