diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 10d095de..d90177a5 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -964,6 +964,11 @@ def generate_pack( dedup_key, existence_discrepancy(file_entry, local_path, platform_display), ) + if file_status.get(dedup_key) == "missing": + # An earlier declaration of this path found nothing; this + # one fills it, as verify counts it. + del file_status[dedup_key] + file_reasons.pop(dedup_key, None) file_status.setdefault(dedup_key, "ok") # Emulator-level validation: informational only for platform packs. diff --git a/scripts/verify.py b/scripts/verify.py index f35713f6..dcd93e9a 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -814,6 +814,12 @@ def _mark_hidden( return placed +def _dest_key(file_entry: dict, sys_id: str) -> str: + """The path a declaration occupies, as the builder keys its pack.""" + dest = sanitize_pack_path(file_entry.get("destination", file_entry.get("name", ""))) + return dest or f"{sys_id}/{file_entry.get('name', '')}" + + def _twin_index( verify_systems: dict, db: dict, base_dest: str, zip_contents: dict, ) -> tuple[dict[str, int], dict[int, tuple[str, dict]]]: @@ -1098,19 +1104,25 @@ def verify_platform( if _twin_unmet(result, held_by): continue - # Aggregate by destination - dest = file_entry.get("destination", file_entry.get("name", "")) - if not dest: - dest = f"{sys_id}/{file_entry.get('name', '')}" + # Aggregate by destination, keyed as the builder keys its pack. + # A declaration that finds nothing leaves the path to one that + # holds a file: the builder packs that file there, whatever the + # order the two are read in. + dest = _dest_key(file_entry, sys_id) results_at.setdefault(dest, []).append(result) required = file_entry.get("required", True) cur = result["status"] prev = file_status.get(dest) + hle = hle_index.get(file_entry.get("name", ""), False) + sev = compute_severity(cur, required, mode, hle) + if prev is not None and (cur == Status.MISSING) != (prev == Status.MISSING): + if cur == Status.MISSING: + continue + del file_status[dest], file_severity[dest] + prev = None if prev is None or _STATUS_ORDER.get(cur, 0) > _STATUS_ORDER.get(prev, 0): file_status[dest] = cur file_required[dest] = required - hle = hle_index.get(file_entry.get("name", ""), False) - sev = compute_severity(cur, required, mode, hle) prev_sev = file_severity.get(dest) if prev_sev is None or _SEVERITY_ORDER.get(sev, 0) > _SEVERITY_ORDER.get( prev_sev, 0 @@ -1192,10 +1204,7 @@ def verify_platform( dest_to_name: dict[str, str] = {} for sys_id, system in verify_systems.items(): for fe in system.get("files", []): - dest = fe.get("destination", fe.get("name", "")) - if not dest: - dest = f"{sys_id}/{fe.get('name', '')}" - dest_to_name.setdefault(dest, fe.get("name", "")) + dest_to_name.setdefault(_dest_key(fe, sys_id), fe.get("name", "")) with_validation = sum( 1 for dest in file_status if dest_to_name.get(dest, "") in gt_filenames ) diff --git a/tests/test_core_file_fills_declaration.py b/tests/test_core_file_fills_declaration.py index 89530dc1..1e80a830 100644 --- a/tests/test_core_file_fills_declaration.py +++ b/tests/test_core_file_fills_declaration.py @@ -258,5 +258,57 @@ class AMemberNameIsNotALooseFile(unittest.TestCase): ) +class TwoDeclarationsOfOnePath(unittest.TestCase): + """Two systems declare one destination under two names, no hash, one + held. The builder packed the held file and kept the first verdict it + read; verify kept the worst, on the raw destination. Read either way + round, both must count the path once and met.""" + + def _run(self, first: str, second: str, spelling: str): + _emulator_profiles_cache.clear() + root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp")) + self.addCleanup(shutil.rmtree, root, True) + bios = root / "bios" / "Maker" / "Console" + bios.mkdir(parents=True) + data = b"SHARED BOOT ROM" + (bios / "boot.bin").write_bytes(data) + sha1, md5 = hashlib.sha1(data).hexdigest(), hashlib.md5(data).hexdigest() + db = { + "files": {sha1: {"path": str(bios / "boot.bin"), "name": "boot.bin", + "md5": md5, "crc32": "", "size": len(data)}}, + "indexes": {"by_md5": {md5: sha1}, "by_name": {"boot.bin": [sha1]}, + "by_crc32": {}, "by_path_suffix": {}}, + } + for folder in ("platforms", "emulators"): + (root / folder).mkdir() + (root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({ + "platform": "FakePlat", "verification_mode": "existence", + "base_destination": "system", "cores": [], + "systems": { + "a-system": {"files": [{"name": first, "destination": "shared.bin"}]}, + "b-system": {"files": [{"name": second, "destination": spelling}]}, + }, + })) + config = load_platform_config("fakeplat", str(root / "platforms")) + result = verify_platform(config, db, str(root / "emulators")) + out = io.StringIO() + with redirect_stdout(out): + zip_path = generate_pack( + "fakeplat", str(root / "platforms"), db, str(root / "bios"), + str(root / "out"), emulators_dir=str(root / "emulators"), offline=True, + ) + with zipfile.ZipFile(zip_path) as zf: + self.assertIn("shared.bin", zf.namelist()) + return result["status_counts"], out.getvalue() + + def test_either_order_counts_one_met_path(self): + for first, second in (("boot.bin", "absent.bin"), ("absent.bin", "boot.bin")): + for spelling in ("shared.bin", "./shared.bin"): + with self.subTest(first=first, spelling=spelling): + counts, printed = self._run(first, second, spelling) + self.assertEqual(counts, {"ok": 1}) + self.assertIn("1/1 files OK", printed) + + if __name__ == "__main__": unittest.main()