diff --git a/scripts/packextras.py b/scripts/packextras.py index b0475562..3be6f835 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -137,6 +137,7 @@ def hidden_extras( shipped: dict[str, str], target_cores: set[str] | None = None, region_drops: set[str] | None = None, + placed: set[str] | None = None, ) -> dict[tuple[str, str, str], str]: """Report entries the builder holds and still leaves out, with the reason. @@ -150,7 +151,8 @@ def hidden_extras( while the report counted it in the pack. A twin carrying the same bytes serves the core on the filesystem that folds the names, so it is not a file left out. ``shipped`` maps the full destination of each platform - file the pack carries to its local copy. + file the pack carries to its local copy. ``placed``, when given, + receives the full destination of each held extra the pack carries. """ from packpaths import _register_path, placement_block @@ -198,6 +200,8 @@ def hidden_extras( content[full.lower()] = held if case_insensitive: lower.add(full.lower()) + if placed is not None and local: + placed.add(full) return hidden diff --git a/scripts/verify.py b/scripts/verify.py index 7c49f955..62fac910 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -763,15 +763,19 @@ def _mark_hidden( shipped: dict[str, str], target_cores: set[str] | None, region_drops: set[str] | None = None, -) -> None: - """What the builder holds but cannot place beside the files it packed.""" +) -> set[str]: + """What the builder holds but cannot place beside the files it packed. + + Returns the full destinations the builder fills with a core's file. + """ from packextras import hidden_extras + placed: set[str] = set() hidden = hidden_extras( config, emulators_dir, db, emu_profiles if emu_profiles is not None else load_emulator_profiles(emulators_dir), - shipped, target_cores, region_drops, + shipped, target_cores, region_drops, placed, ) for u in undeclared: reason = hidden.get( @@ -780,6 +784,7 @@ def _mark_hidden( if reason and u.get("in_repo") and u.get("in_pack", True): u["in_pack"] = False u["omitted"] = _HIDDEN_REASONS[reason] + return placed def _twin_index( @@ -987,6 +992,7 @@ def verify_platform( ) shipped: dict[str, str] = {} + results_at: dict[str, list[dict]] = {} for sys_id, system in verify_systems.items(): for file_entry in system.get("files", []): if region_drops and ( @@ -1055,6 +1061,7 @@ def verify_platform( dest = file_entry.get("destination", file_entry.get("name", "")) if not dest: dest = f"{sys_id}/{file_entry.get('name', '')}" + results_at.setdefault(dest, []).append(result) required = file_entry.get("required", True) cur = result["status"] prev = file_status.get(dest) @@ -1069,21 +1076,6 @@ def verify_platform( ): file_severity[dest] = sev - # Count by severity - counts = { - Severity.OK: 0, - Severity.INFO: 0, - Severity.WARNING: 0, - Severity.CRITICAL: 0, - } - for s in file_severity.values(): - counts[s] = counts.get(s, 0) + 1 - - # Count by file status (ok/untested/missing) - status_counts: dict[str, int] = {} - for s in file_status.values(): - status_counts[s] = status_counts.get(s, 0) + 1 - # Cross-reference undeclared files if supplemental_names is None: from cross_reference import _build_supplemental_index @@ -1107,10 +1099,36 @@ def verify_platform( not in region_drops ] _mark_unplaced(config, undeclared, profiles) - _mark_hidden( + placed = _mark_hidden( config, undeclared, emulators_dir, db, emu_profiles, shipped, target_cores, region_drops, ) + if not reads_file_contents(mode): + # An existence frontend is satisfied by any file at the path, and the + # builder fills a free platform destination with a core's file. + for dest, status in file_status.items(): + full = f"{base_dest}/{dest}" if base_dest else dest + if status == Status.MISSING and full in placed: + file_status[dest] = Status.OK + file_severity[dest] = Severity.OK + for result in results_at.get(dest, []): + result["status"] = Status.OK + + # Count by severity + counts = { + Severity.OK: 0, + Severity.INFO: 0, + Severity.WARNING: 0, + Severity.CRITICAL: 0, + } + for s in file_severity.values(): + counts[s] = counts.get(s, 0) + 1 + + # Count by file status (ok/untested/missing) + status_counts: dict[str, int] = {} + for s in file_status.values(): + status_counts[s] = status_counts.get(s, 0) + 1 + exclusions = find_exclusion_notes( config, emulators_dir, emu_profiles, target_cores=target_cores ) diff --git a/tests/test_core_file_fills_declaration.py b/tests/test_core_file_fills_declaration.py new file mode 100644 index 00000000..79097678 --- /dev/null +++ b/tests/test_core_file_fills_declaration.py @@ -0,0 +1,97 @@ +"""A core's file at a free platform destination answers an existence check. + +The builder packs a core extra at a destination the platform declares and +does not hold, and in existence mode counts the declaration met: the +frontend sees a file at the path. verify kept it MISSING, so the pack said +1/1 OK and verify 0/1 for the same platform. +""" + +from __future__ import annotations + +import hashlib +import io +import os +import shutil +import sys +import tempfile +import unittest +import zipfile +from contextlib import redirect_stdout +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from common import _emulator_profiles_cache, load_platform_config # noqa: E402 +from generate_pack import generate_pack # noqa: E402 +from verify import verify_platform # noqa: E402 + + +class ACoreFileFillsAFreeDeclaration(unittest.TestCase): + def setUp(self): + _emulator_profiles_cache.clear() + (REPO_ROOT / "tmp").mkdir(exist_ok=True) + self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp")) + self.addCleanup(shutil.rmtree, self.root, True) + bios = self.root / "bios" / "Maker" / "Console" + bios.mkdir(parents=True) + data = b"HELD BOOT ROM" + (bios / "boot.bin").write_bytes(data) + sha1 = hashlib.sha1(data).hexdigest() + md5 = hashlib.md5(data).hexdigest() + self.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": {}, + }, + } + self.platforms = self.root / "platforms" + self.emulators = self.root / "emulators" + self.platforms.mkdir() + self.emulators.mkdir() + (self.platforms / "fakeplat.yml").write_text(yaml.safe_dump({ + "platform": "FakePlat", + "verification_mode": "existence", + "base_destination": "system", + "cores": ["fakecore"], + "systems": {"console-a": {"files": [ + {"name": "zzz_absent.bin", "destination": "zzz_absent.bin", + "required": True}, + ]}}, + })) + # The entry's name differs from the destination it fills, as + # descriptive names do ("MDA font ROM" at mda.rom). + (self.emulators / "fakecore.yml").write_text(yaml.safe_dump({ + "emulator": "FakeCore", "type": "libretro", "cores": ["fakecore"], + "systems": ["console-a"], + "files": [{"name": "Fake boot ROM", "path": "zzz_absent.bin", + "sha1": sha1, "required": True}], + })) + self.config = load_platform_config("fakeplat", str(self.platforms)) + + def test_verify_counts_what_the_pack_counts(self): + result = verify_platform( + self.config, self.db, str(self.emulators), supplemental_names=set(), + ) + out = self.root / "out" + with redirect_stdout(io.StringIO()): + zip_path = generate_pack( + "fakeplat", str(self.platforms), self.db, str(self.root / "bios"), + str(out), emulators_dir=str(self.emulators), offline=True, + ) + with zipfile.ZipFile(zip_path) as zf: + self.assertIn("zzz_absent.bin", zf.namelist()) + self.assertEqual(result["status_counts"], {"ok": 1}) + self.assertEqual( + [d["status"] for d in result["details"]], ["ok"], + ) + + +if __name__ == "__main__": + unittest.main()