From 748562e71d8f8c8aad3e11dd82e09898f403aba5 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 10 Oct 2026 07:02:38 +0200 Subject: [PATCH] fix: count a conflicting declaration by its file --- scripts/generate_pack.py | 21 +++++- tests/test_path_conflict_status.py | 100 +++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 tests/test_path_conflict_status.py diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 148791e4..a60158d0 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -780,9 +780,24 @@ def generate_pack( if _has_path_conflict(full_dest, seen_destinations, seen_parents): # Content ships under the conflicting shape (file vs dir, - # e.g. SGB1.sfc): count it so pack totals match verify.py, - # which resolves each declaration independently - file_status.setdefault(dedup_key, "ok") + # e.g. SGB1.sfc). verify.py resolves each declaration on + # its own, so this one is counted by what it resolves to: + # an unresolved one counted OK had the pack claim a file + # verify reports missing. + _conflict_path, conflict_status = resolve_file( + file_entry, db, bios_dir, zip_contents, + data_dir_registry=data_registry, offline=offline, + ) + if conflict_status == "not_found": + file_status[dedup_key] = "missing" + file_reasons[dedup_key] = "not found" + elif conflict_status == "hash_mismatch" and ( + hash_mismatch_excludes_file(verification_mode) + ): + file_status[dedup_key] = "excluded" + file_reasons[dedup_key] = "hash mismatch" + else: + file_status.setdefault(dedup_key, "ok") continue storage = file_entry.get("storage", "embedded") diff --git a/tests/test_path_conflict_status.py b/tests/test_path_conflict_status.py new file mode 100644 index 00000000..6b71d49b --- /dev/null +++ b/tests/test_path_conflict_status.py @@ -0,0 +1,100 @@ +"""A declaration that conflicts with a packed path is counted by what it resolves to. + +RetroArch declares SGB1.sfc as a file and SGB1.sfc/ as files inside it. +The builder ships one shape and counted every conflicting declaration OK, +resolved or not; verify.py resolves each on its own. A required file absent +from the collection read as covered in the pack report and as missing in verify. +""" + +from __future__ import annotations + +import contextlib +import hashlib +import io +import re +import sys +import tempfile +import unittest +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +import common # noqa: E402 +import generate_db # noqa: E402 +import generate_pack as builder # noqa: E402 +from verify import verify_platform # noqa: E402 + + +class ConflictingDeclarations(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.root = Path(self._tmp.name) + bios = self.root / "bios" / "Nintendo" + self.platforms = self.root / "platforms" + self.emulators = self.root / "emulators" + for directory in (bios, self.platforms, self.emulators): + directory.mkdir(parents=True) + payload = b"super game boy" + (bios / "SGB1.sfc").write_bytes(payload) + sha1 = hashlib.sha1(payload).hexdigest() + files = { + sha1: { + "path": str(bios / "SGB1.sfc"), "name": "SGB1.sfc", + "size": len(payload), "sha1": sha1, + "md5": hashlib.md5(payload).hexdigest(), + "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000001", + } + } + self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})} + platform = { + "platform": "Conflict", + "verification_mode": "existence", + "base_destination": "system", + "cores": [], + "systems": { + "nintendo-sgb": {"files": [ + {"name": "SGB1.sfc", "destination": "SGB1.sfc", "required": True}, + {"name": "nothere.rom", "destination": "SGB1.sfc/nothere.rom", + "required": True}, + ]}, + }, + } + (self.platforms / "conflict.yml").write_text(yaml.dump(platform)) + (self.platforms / "_registry.yml").write_text( + yaml.dump({"platforms": {"conflict": {"status": "active"}}}) + ) + common._platform_config_cache.clear() + common._emulator_profiles_cache.clear() + + def tearDown(self): + common._platform_config_cache.clear() + self._tmp.cleanup() + + def test_the_pack_and_verify_count_alike(self): + out = self.root / "dist" + out.mkdir() + report = io.StringIO() + with contextlib.redirect_stdout(report): + builder.generate_pack( + "conflict", str(self.platforms), self.db, str(self.root / "bios"), + str(out), emulators_dir=str(self.emulators), emu_profiles={}, + offline=True, + ) + packed = re.search(r"(\d+)/(\d+) files OK", report.getvalue()) + self.assertIsNotNone(packed, report.getvalue()) + + config = common.load_platform_config("conflict", str(self.platforms)) + verified = verify_platform(config, self.db, str(self.emulators)) + counts = verified["status_counts"] + self.assertEqual(counts.get("missing"), 1) + self.assertEqual( + (int(packed.group(1)), int(packed.group(2))), + (counts.get("ok", 0), verified["total_files"]), + ) + + +if __name__ == "__main__": + unittest.main()