"""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"]), ) class AnExtraDoesNotPassAHashCheck(unittest.TestCase): """A md5 platform declares scph1001.bin with a hash the collection lacks; a core declares our dump at the same path. The builder ships the core's file and called the declaration OK, while the frontend, which hashes the file, rejects it and verify reports it untested.""" def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.root = Path(self._tmp.name) bios = self.root / "bios" / "Sony" self.platforms = self.root / "platforms" self.emulators = self.root / "emulators" for directory in (bios, self.platforms, self.emulators): directory.mkdir(parents=True) payload = b"our psx dump" (bios / "psx-us.bin").write_bytes(payload) sha1 = hashlib.sha1(payload).hexdigest() md5 = hashlib.md5(payload).hexdigest() files = { sha1: { "path": str(bios / "psx-us.bin"), "name": "psx-us.bin", "size": len(payload), "sha1": sha1, "md5": md5, "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000002", } } self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})} platform = { "platform": "Hashed", "verification_mode": "md5", "cores": ["fxcore"], "systems": {"sony-playstation": {"files": [ {"name": "scph1001.bin", "destination": "scph1001.bin", "md5": "0123456789abcdef0123456789abcdef", "required": True}, ]}}, } (self.platforms / "hashed.yml").write_text(yaml.dump(platform)) (self.platforms / "_registry.yml").write_text( yaml.dump({"platforms": {"hashed": {"status": "active"}}}) ) profile = { "emulator": "FX", "type": "libretro", "cores": ["fxcore"], "systems": ["sony-playstation"], "files": [{"name": "psx-us.bin", "path": "scph1001.bin", "md5": md5, "required": True}], } (self.emulators / "fxcore.yml").write_text(yaml.dump(profile)) common._platform_config_cache.clear() common._emulator_profiles_cache.clear() def tearDown(self): common._platform_config_cache.clear() common._emulator_profiles_cache.clear() self._tmp.cleanup() def test_the_declaration_stays_failed(self): out = self.root / "dist" out.mkdir() report = io.StringIO() with contextlib.redirect_stdout(report): builder.generate_pack( "hashed", str(self.platforms), self.db, str(self.root / "bios"), str(out), emulators_dir=str(self.emulators), offline=True, ) packed = re.search(r"(\d+)/(\d+) files OK", report.getvalue()) self.assertIsNotNone(packed, report.getvalue()) config = common.load_platform_config("hashed", str(self.platforms)) verified = verify_platform(config, self.db, str(self.emulators)) self.assertEqual(verified["status_counts"].get("ok", 0), 0) self.assertEqual(int(packed.group(1)), 0, report.getvalue()) class TheEmulatorPackShipsWhatTheCoreAccepts(unittest.TestCase): """BasiliskII wants a 512 KB to 1 MB ROM. The only ROM held was 128 KB: the emulator pack shipped it silently and verify --emulator announced 1/1 OK above the line saying the core would refuse it.""" def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.root = Path(self._tmp.name) bios = self.root / "bios" / "Apple" self.emulators = self.root / "emulators" for directory in (bios, self.emulators): directory.mkdir(parents=True) payload = b"r" * 131072 (bios / "ROM").write_bytes(payload) sha1 = hashlib.sha1(payload).hexdigest() files = {sha1: { "path": str(bios / "ROM"), "name": "ROM", "size": len(payload), "sha1": sha1, "md5": hashlib.md5(payload).hexdigest(), "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000003", }} self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})} profile = { "emulator": "Mac", "type": "libretro", "cores": ["mac"], "systems": ["apple-macintosh"], "files": [{"name": "ROM", "required": True, "min_size": 524288, "max_size": 1048576, "validation": ["size"]}], } (self.emulators / "mac.yml").write_text(yaml.dump(profile)) common._emulator_profiles_cache.clear() def tearDown(self): common._emulator_profiles_cache.clear() self._tmp.cleanup() def test_the_refused_rom_is_named_not_shipped(self): import zipfile out = self.root / "dist" out.mkdir() report = io.StringIO() with contextlib.redirect_stdout(report): zip_path = builder.generate_emulator_pack( ["mac"], str(self.emulators), self.db, str(self.root / "bios"), str(out), offline=True, ) self.assertIn("REFUSED: ROM", report.getvalue()) with zipfile.ZipFile(zip_path) as archive: self.assertNotIn("ROM", archive.namelist()) def test_verify_does_not_call_it_ok(self): from verify import Severity, verify_emulator result = verify_emulator(["mac"], str(self.emulators), self.db) self.assertEqual(result["severity_counts"][Severity.OK], 0) if __name__ == "__main__": unittest.main()