From ed610c7bf374eeeb02c235e1285519015117f05a Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:55:50 +0200 Subject: [PATCH] fix: hold the consumer guards on fixtures The Dolphin-MMJR codehandler.bin and armsx2's game_controller_db.txt left the collection, and the two guards that read them skipped on every run. Both now build the trees they need, and the guards still reading the repository fail instead of skipping when their data is gone. --- tests/test_file_consumers.py | 74 +++++++++++++++++++++++----------- tests/test_owner_resolution.py | 69 +++++++++++++++++++++++++------ 2 files changed, 107 insertions(+), 36 deletions(-) diff --git a/tests/test_file_consumers.py b/tests/test_file_consumers.py index 94b23f13..74aa8aad 100644 --- a/tests/test_file_consumers.py +++ b/tests/test_file_consumers.py @@ -87,34 +87,60 @@ class ReadersFollowTheDataTrees(unittest.TestCase): """Dolphin names codehandler.bin without a hash. The pack takes it from data/dolphin-sys, and the site credited Dolphin and Triforce with the Dolphin-MMJR copy, another revision, because the consumers resolved - without the data trees. Reads the repository.""" + without the data trees.""" - def test_dolphin_does_not_read_the_mmjr_copy(self): + def setUp(self): import os - from common import load_database, load_emulator_profiles + self._previous = os.getcwd() + self._tree = tempfile.TemporaryDirectory() + root = Path(self._tree.name) + fork = root / "bios" / "Other" / "dolphin-mmjr" / "codehandler.bin" + fork.parent.mkdir(parents=True) + fork.write_bytes(b"dolphin-mmjr codehandler") + own = root / "data" / "dolphin-sys" / "codehandler.bin" + own.parent.mkdir(parents=True) + own.write_bytes(b"dolphin codehandler") + self.fork_sha1 = hashlib.sha1(fork.read_bytes()).hexdigest() + self.db = { + "files": {self.fork_sha1: { + "name": "codehandler.bin", "sha1": self.fork_sha1, + "path": "bios/Other/dolphin-mmjr/codehandler.bin", + "md5": hashlib.md5(fork.read_bytes()).hexdigest(), + "size": fork.stat().st_size, + }}, + "indexes": { + "by_name": {"codehandler.bin": [self.fork_sha1]}, + "by_md5": {}, "by_path_suffix": {}, + }, + } + self.registry = {"dolphin-sys": {"local_cache": str(own.parent)}} + self.profiles = {"dolphin": {"type": "standalone + libretro", "files": [ + {"name": "codehandler.bin", "path": None, "mode": "libretro"}, + ]}} + os.chdir(root) - if not (REPO_ROOT / "database.json").is_file() or not ( - REPO_ROOT / "data" / "dolphin-sys" / "codehandler.bin" - ).is_file(): - self.skipTest("database.json or data/dolphin-sys is absent") - previous = os.getcwd() - os.chdir(REPO_ROOT) - try: - db = load_database("database.json") - profiles = load_emulator_profiles("emulators") - consumers = FileConsumers(db) - consumers.add_emulators({"dolphin": profiles["dolphin"]}) - finally: - os.chdir(previous) - mmjr = next( - (sha1 for sha1, entry in db["files"].items() - if entry["path"] == "bios/Other/dolphin-mmjr/codehandler.bin"), - None, - ) - if mmjr is None: - self.skipTest("the Dolphin-MMJR copy is not collected") - self.assertNotIn("dolphin", consumers.emulators.get(mmjr, set())) + def tearDown(self): + import os + + from common import forget_data_dir_index + + os.chdir(self._previous) + forget_data_dir_index() + self._tree.cleanup() + + def _readers(self, registry: dict) -> set[str]: + consumers = FileConsumers(self.db, data_dir_registry=registry) + consumers.add_emulators(self.profiles) + return consumers.emulators.get(self.fork_sha1, set()) + + def test_dolphin_does_not_read_the_mmjr_copy(self): + self.assertNotIn("dolphin", self._readers(self.registry)) + + def test_without_the_data_trees_the_name_step_takes_the_fork_copy(self): + """The case above has to reach the data trees to pass: resolved + without them, the name alone hands Dolphin the fork's copy.""" + self.assertIn("dolphin", self._readers({})) def tearDownModule(): diff --git a/tests/test_owner_resolution.py b/tests/test_owner_resolution.py index b9ffcb8b..f135bd1f 100644 --- a/tests/test_owner_resolution.py +++ b/tests/test_owner_resolution.py @@ -9,7 +9,10 @@ armsx2's pack ships without a reader, and MAME's neogeo.zip without MAME. from __future__ import annotations +import hashlib +import os import sys +import tempfile import unittest from pathlib import Path @@ -30,7 +33,7 @@ class ReadersNameTheOwner(unittest.TestCase): def setUpClass(cls): db_path = REPO_ROOT / "database.json" if not db_path.exists(): - raise unittest.SkipTest("database.json is not built") + raise AssertionError("database.json is committed and missing") cls.db = load_database(str(db_path)) cls.profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) cls.sha1_by_path = {r["path"]: s for s, r in cls.db["files"].items()} @@ -40,15 +43,9 @@ class ReadersNameTheOwner(unittest.TestCase): self.db, build_zip_contents_index(self.db) ) consumers.add_emulators(self.profiles) - for path, owner in ( - ("bios/Other/armsx2/game_controller_db.txt", "armsx2"), - ("bios/Arcade/MAME/neogeo.zip", "mame"), - ): - with self.subTest(path=path): - sha1 = self.sha1_by_path.get(path) - if sha1 is None: - self.skipTest(f"{path} is not collected") - self.assertIn(owner, consumers.emulators.get(sha1, set())) + sha1 = self.sha1_by_path.get("bios/Arcade/MAME/neogeo.zip") + self.assertIsNotNone(sha1, "MAME's neogeo.zip is not collected") + self.assertIn("mame", consumers.emulators.get(sha1, set())) def test_slot_claims_resolve_the_owners_tree(self): claims = slots.profile_claims({"pcem": self.profiles["pcem"]}, self.db) @@ -57,12 +54,60 @@ class ReadersNameTheOwner(unittest.TestCase): } # pcem declares ide_xt.bin and the collection holds it: an unclaimed # file here is a claiming defect, not missing data. - if not any(e.get("path", "").endswith("/ide_xt.bin") for e in self.db["files"].values()): - self.skipTest("ide_xt.bin is not collected") + self.assertTrue( + any(e.get("path", "").endswith("/ide_xt.bin") + for e in self.db["files"].values()), + "ide_xt.bin is not collected", + ) local = held.get("ide_xt.bin") self.assertIsNotNone(local, "pcem's ide_xt.bin is not claimed") self.assertTrue(local.startswith("bios/Other/pcem/"), local) +class EachForkReadsItsOwnCopy(unittest.TestCase): + """Two emulators ship a file under one name in their own trees. Without + the owner, the site credited aethersx2's game_controller_db.txt to + armsx2 and left the copy armsx2's pack ships without a reader.""" + + NAME = "game_controller_db.txt" + + def setUp(self): + self._previous = os.getcwd() + self._tree = tempfile.TemporaryDirectory() + root = Path(self._tree.name) + files: dict[str, dict] = {} + self.sha1_of: dict[str, str] = {} + for owner in ("aethersx2", "armsx2"): + rel = f"bios/Other/{owner}/{self.NAME}" + payload = f"{owner} mappings".encode() + (root / rel).parent.mkdir(parents=True) + (root / rel).write_bytes(payload) + sha1 = hashlib.sha1(payload).hexdigest() + files[sha1] = { + "name": self.NAME, "sha1": sha1, "path": rel, + "md5": hashlib.md5(payload).hexdigest(), "size": len(payload), + } + self.sha1_of[owner] = sha1 + self.db = {"files": files, "indexes": { + "by_name": {self.NAME: sorted(files)}, + "by_md5": {}, "by_path_suffix": {}, + }} + os.chdir(root) + + def tearDown(self): + os.chdir(self._previous) + self._tree.cleanup() + + def test_the_site_credits_each_owner_with_its_copy(self): + consumers = cross_reference.FileConsumers(self.db, data_dir_registry={}) + consumers.add_emulators({ + owner: {"type": "standalone", "files": [{"name": self.NAME}]} + for owner in self.sha1_of + }) + for owner, sha1 in self.sha1_of.items(): + with self.subTest(owner=owner): + self.assertEqual(consumers.emulators.get(sha1), {owner}) + + if __name__ == "__main__": unittest.main()