Files
libretro/tests/test_e2e.py
T
Abdessamad Derraz b5fd643ccf refactor: give common.py's parts their own modules
common.py had grown to 1833 lines by accumulation. Six coherent pieces
move out - untrusted parsing, digests, archives, generated artefacts,
release assets, dump catalogues - and common.py re-exports them, so the
sixty existing import sites keep working and migrating them stays
optional.

The site build is now reproducible, which is what made the move
checkable. It deleted its generated directories first, so every page was
new and write_if_changed had no earlier version to compare against: a
deploy republished six hundred pages for the clock alone. Directories
are swept instead, a page is removed only once nothing produces it, and
the body pass compares against the body of the file on disk rather than
against the decorated page. Two consecutive builds on the same inputs
now produce identical bytes; before, 1034 files differed.
2026-08-12 11:49:54 +02:00

6038 lines
237 KiB
Python

"""End-to-end regression test.
ONE test scenario with YAML fixtures covering ALL code paths.
Run: python -m unittest tests.test_e2e -v
Covers:
Resolution: SHA1, MD5, name, alias, truncated MD5, md5_composite,
zip_contents, .variants deprio, not_found, hash_mismatch
Verification: existence mode, md5 mode, required/optional,
zipped_file (match/mismatch/missing inner), multi-hash
Severity: all combos per platform mode
Platform config: inheritance, shared groups, data_directories, grouping
Pack: storage tiers (external/user_provided/embedded), dedup, large file cache
Cross-reference: undeclared files, standalone skipped, alias profiles skipped,
data_dir suppresses gaps
"""
from __future__ import annotations
import hashlib
import os
import shutil
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
import largefiles
import yaml
from common import (
build_zip_contents_index,
check_inside_zip,
compute_hashes,
expand_platform_declared_names,
group_identical_platforms,
load_emulator_profiles,
load_platform_config,
md5_composite,
parse_md5_list,
resolve_local_file,
resolve_platform_cores,
safe_extract_zip,
)
from truth import diff_platform_truth, generate_platform_truth
from validation import (
_build_validation_index,
check_file_validation,
filter_files_by_mode,
)
from verify import (
Severity,
Status,
find_exclusion_notes,
find_undeclared_files,
verify_emulator,
verify_platform,
)
def _h(data: bytes) -> dict:
"""Return sha1, md5, crc32 for test data."""
return {
"sha1": hashlib.sha1(data).hexdigest(),
"md5": hashlib.md5(data).hexdigest(),
"crc32": format(hashlib.new("crc32", data).digest()[0], "08x")
if False
else "", # not needed for tests
}
class TestE2E(unittest.TestCase):
"""Single end-to-end scenario exercising every code path."""
# ---------------------------------------------------------------
# Fixture setup
# ---------------------------------------------------------------
def setUp(self):
# Clear emulator profile cache to avoid stale data between tests
from common import _emulator_profiles_cache
_emulator_profiles_cache.clear()
self.root = tempfile.mkdtemp()
self.bios_dir = os.path.join(self.root, "bios")
self.platforms_dir = os.path.join(self.root, "platforms")
self.emulators_dir = os.path.join(self.root, "emulators")
os.makedirs(self.bios_dir)
os.makedirs(self.platforms_dir)
os.makedirs(self.emulators_dir)
# -- Create synthetic BIOS files --
self.files = {}
self._make_file("present_req.bin", b"PRESENT_REQUIRED")
self._make_file("present_opt.bin", b"PRESENT_OPTIONAL")
self._make_file("correct_hash.bin", b"CORRECT_HASH_DATA")
self._make_file("wrong_hash.bin", b"WRONG_CONTENT_ON_DISK")
self._make_file("no_md5.bin", b"NO_MD5_CHECK")
self._make_file("truncated.bin", b"BATOCERA_TRUNCATED")
self._make_file("alias_target.bin", b"ALIAS_FILE_DATA")
self._make_file(
"leading_zero_crc.bin", b"LEADING_ZERO_CRC_12"
) # crc32=0179e92e
# Regional variant files (same name, different content, in subdirs)
os.makedirs(os.path.join(self.bios_dir, "TestConsole", "USA"), exist_ok=True)
os.makedirs(os.path.join(self.bios_dir, "TestConsole", "EUR"), exist_ok=True)
self._make_file("BIOS.bin", b"BIOS_USA_CONTENT", subdir="TestConsole/USA")
self._make_file("BIOS.bin", b"BIOS_EUR_CONTENT", subdir="TestConsole/EUR")
# .variants/ file (should be deprioritized)
variants_dir = os.path.join(self.bios_dir, ".variants")
os.makedirs(variants_dir)
self._make_file("present_req.bin", b"VARIANT_DATA", subdir=".variants")
# ZIP with correct inner ROM
self._make_zip("good.zip", {"inner.rom": b"GOOD_INNER_ROM"})
# ZIP with wrong inner ROM
self._make_zip("bad_inner.zip", {"inner.rom": b"BAD_INNER"})
# ZIP with missing inner ROM name
self._make_zip("missing_inner.zip", {"other.rom": b"OTHER_ROM"})
# ZIP for md5_composite (Recalbox)
self._make_zip("composite.zip", {"b.rom": b"BBBB", "a.rom": b"AAAA"})
# ZIP for multi-hash
self._make_zip("multi.zip", {"rom.bin": b"MULTI_HASH_DATA"})
# Archive BIOS ZIP (like neogeo.zip) containing multiple ROMs
self._make_zip(
"test_archive.zip",
{
"rom_a.bin": b"ARCHIVE_ROM_A",
"rom_b.bin": b"ARCHIVE_ROM_B",
},
)
# -- Build synthetic database --
self.db = self._build_db()
# -- Create platform YAMLs --
self._create_existence_platform()
self._create_md5_platform()
self._create_shared_groups()
self._create_inherited_platform()
self._create_sha1_platform()
# -- Create emulator YAMLs --
self._create_emulator_profiles()
def tearDown(self):
shutil.rmtree(self.root)
# ---------------------------------------------------------------
# File helpers
# ---------------------------------------------------------------
def _make_file(self, name: str, data: bytes, subdir: str = "") -> str:
d = os.path.join(self.bios_dir, subdir) if subdir else self.bios_dir
os.makedirs(d, exist_ok=True)
path = os.path.join(d, name)
with open(path, "wb") as f:
f.write(data)
h = _h(data)
self.files[f"{subdir}/{name}" if subdir else name] = {
"path": path,
"data": data,
**h,
}
return path
def _make_zip(self, name: str, contents: dict[str, bytes]) -> str:
path = os.path.join(self.bios_dir, name)
with zipfile.ZipFile(path, "w") as zf:
for fname, data in contents.items():
zf.writestr(fname, data)
with open(path, "rb") as f:
zdata = f.read()
h = _h(zdata)
inner_md5s = {fn: hashlib.md5(d).hexdigest() for fn, d in contents.items()}
self.files[name] = {"path": path, "data": zdata, "inner_md5s": inner_md5s, **h}
return path
def _build_db(self) -> dict:
files_db = {}
by_md5 = {}
by_name = {}
for key, info in self.files.items():
name = os.path.basename(key)
sha1 = info["sha1"]
files_db[sha1] = {
"path": info["path"],
"md5": info["md5"],
"name": name,
"crc32": info.get("crc32", ""),
"size": len(info["data"]),
}
by_md5[info["md5"]] = sha1
by_name.setdefault(name, []).append(sha1)
# Add alias name to by_name
alias_sha1 = self.files["alias_target.bin"]["sha1"]
by_name.setdefault("alias_alt.bin", []).append(alias_sha1)
# Build by_path_suffix for regional variant resolution
by_path_suffix = {}
for key, info in self.files.items():
if "/" in key:
# key is subdir/name, suffix is the subdir path
by_path_suffix.setdefault(key, []).append(info["sha1"])
return {
"files": files_db,
"indexes": {
"by_md5": by_md5,
"by_name": by_name,
"by_crc32": {},
"by_path_suffix": by_path_suffix,
},
}
# ---------------------------------------------------------------
# Platform YAML creators
# ---------------------------------------------------------------
def _create_existence_platform(self):
config = {
"platform": "TestExistence",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"console-a": {
"files": [
{
"name": "present_req.bin",
"destination": "present_req.bin",
"required": True,
},
{
"name": "missing_req.bin",
"destination": "missing_req.bin",
"required": True,
},
{
"name": "present_opt.bin",
"destination": "present_opt.bin",
"required": False,
},
{
"name": "missing_opt.bin",
"destination": "missing_opt.bin",
"required": False,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_existence.yml"), "w") as fh:
yaml.dump(config, fh)
def _create_md5_platform(self):
f = self.files
good_inner_md5 = f["good.zip"]["inner_md5s"]["inner.rom"]
bad_inner_md5 = "deadbeefdeadbeefdeadbeefdeadbeef"
composite_md5 = hashlib.md5(
b"AAAA" + b"BBBB"
).hexdigest() # sorted: a.rom, b.rom
multi_wrong = "0000000000000000000000000000000"
multi_right = f["multi.zip"]["inner_md5s"]["rom.bin"]
truncated_md5 = f["truncated.bin"]["md5"][:29] # Batocera 29-char
config = {
"platform": "TestMD5",
"verification_mode": "md5",
"systems": {
"sys-md5": {
"includes": ["test_shared"],
"files": [
# Correct hash
{
"name": "correct_hash.bin",
"destination": "correct_hash.bin",
"md5": f["correct_hash.bin"]["md5"],
"required": True,
},
# Wrong hash on disk ->untested
{
"name": "wrong_hash.bin",
"destination": "wrong_hash.bin",
"md5": "ffffffffffffffffffffffffffffffff",
"required": True,
},
# No MD5 ->OK (existence within md5 platform)
{
"name": "no_md5.bin",
"destination": "no_md5.bin",
"required": False,
},
# Missing required
{
"name": "gone_req.bin",
"destination": "gone_req.bin",
"md5": "abcd",
"required": True,
},
# Missing optional
{
"name": "gone_opt.bin",
"destination": "gone_opt.bin",
"md5": "abcd",
"required": False,
},
# zipped_file correct
{
"name": "good.zip",
"destination": "good.zip",
"md5": good_inner_md5,
"zipped_file": "inner.rom",
"required": True,
},
# zipped_file wrong inner
{
"name": "bad_inner.zip",
"destination": "bad_inner.zip",
"md5": bad_inner_md5,
"zipped_file": "inner.rom",
"required": False,
},
# zipped_file inner not found
{
"name": "missing_inner.zip",
"destination": "missing_inner.zip",
"md5": "abc",
"zipped_file": "nope.rom",
"required": False,
},
# md5_composite (Recalbox)
{
"name": "composite.zip",
"destination": "composite.zip",
"md5": composite_md5,
"required": True,
},
# Multi-hash comma-separated (Recalbox)
{
"name": "multi.zip",
"destination": "multi.zip",
"md5": f"{multi_wrong},{multi_right}",
"zipped_file": "rom.bin",
"required": True,
},
# Truncated MD5 (Batocera 29 chars)
{
"name": "truncated.bin",
"destination": "truncated.bin",
"md5": truncated_md5,
"required": True,
},
# Same destination from different entry ->worst status wins
{
"name": "correct_hash.bin",
"destination": "dedup_target.bin",
"md5": f["correct_hash.bin"]["md5"],
"required": True,
},
{
"name": "correct_hash.bin",
"destination": "dedup_target.bin",
"md5": "wrong_for_dedup_test",
"required": True,
},
],
"data_directories": [
{"ref": "test-data-dir", "destination": "TestData"},
],
},
"sys-renamed": {
"files": [
{
"name": "renamed_file.bin",
"destination": "renamed_file.bin",
"md5": f["correct_hash.bin"]["md5"],
"required": True,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_md5.yml"), "w") as fh:
yaml.dump(config, fh)
def _create_shared_groups(self):
shared = {
"shared_groups": {
"test_shared": [
{
"name": "shared_file.rom",
"destination": "shared_file.rom",
"required": False,
},
],
},
}
with open(os.path.join(self.platforms_dir, "_shared.yml"), "w") as fh:
yaml.dump(shared, fh)
def _create_inherited_platform(self):
child = {
"inherits": "test_existence",
"platform": "TestInherited",
"base_destination": "BIOS",
}
with open(os.path.join(self.platforms_dir, "test_inherited.yml"), "w") as fh:
yaml.dump(child, fh)
def _create_sha1_platform(self):
f = self.files
config = {
"platform": "TestSHA1",
"verification_mode": "sha1",
"base_destination": "system",
"systems": {
"sys-sha1": {
"files": [
{
"name": "correct_hash.bin",
"destination": "correct_hash.bin",
"sha1": f["correct_hash.bin"]["sha1"],
"required": True,
},
{
"name": "wrong_hash.bin",
"destination": "wrong_hash.bin",
"sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"required": True,
},
{
"name": "missing_sha1.bin",
"destination": "missing_sha1.bin",
"sha1": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"required": True,
},
{
"name": "optional_missing_sha1.bin",
"destination": "optional_missing_sha1.bin",
"sha1": "cccccccccccccccccccccccccccccccccccccccc",
"required": False,
},
{
"name": "no_md5.bin",
"destination": "no_md5.bin",
"required": True,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_sha1.yml"), "w") as fh:
yaml.dump(config, fh)
def _create_emulator_profiles(self):
# Regular emulator with aliases, standalone file, undeclared file
emu = {
"emulator": "TestEmu",
"type": "standalone + libretro",
"systems": ["console-a", "sys-md5"],
"data_directories": [{"ref": "test-data-dir"}],
"files": [
{"name": "present_req.bin", "required": True},
{
"name": "alias_target.bin",
"required": False,
"aliases": ["alias_alt.bin"],
},
{
"name": "standalone_only.bin",
"required": False,
"mode": "standalone",
},
{"name": "undeclared_req.bin", "required": True},
{"name": "undeclared_opt.bin", "required": False},
],
}
with open(os.path.join(self.emulators_dir, "test_emu.yml"), "w") as fh:
yaml.dump(emu, fh)
# Emulator with HLE fallback
emu_hle = {
"emulator": "TestHLE",
"type": "libretro",
"systems": ["console-a"],
"files": [
{"name": "present_req.bin", "required": True, "hle_fallback": True},
{"name": "hle_missing.bin", "required": True, "hle_fallback": True},
{"name": "no_hle_missing.bin", "required": True, "hle_fallback": False},
],
}
with open(os.path.join(self.emulators_dir, "test_hle.yml"), "w") as fh:
yaml.dump(emu_hle, fh)
# Launcher profile (should be excluded from cross-reference)
launcher = {
"emulator": "TestLauncher",
"type": "launcher",
"systems": ["console-a"],
"files": [{"name": "launcher_bios.bin", "required": True}],
}
with open(os.path.join(self.emulators_dir, "test_launcher.yml"), "w") as fh:
yaml.dump(launcher, fh)
# Alias profile (should be skipped)
alias = {
"emulator": "TestAlias",
"type": "alias",
"alias_of": "test_emu",
"files": [],
}
with open(os.path.join(self.emulators_dir, "test_alias.yml"), "w") as fh:
yaml.dump(alias, fh)
# Emulator with data_dir that matches platform ->gaps suppressed
emu_dd = {
"emulator": "TestEmuDD",
"type": "libretro",
"systems": ["sys-md5"],
"data_directories": [{"ref": "test-data-dir"}],
"files": [
{"name": "dd_covered.bin", "required": False},
],
}
with open(os.path.join(self.emulators_dir, "test_emu_dd.yml"), "w") as fh:
yaml.dump(emu_dd, fh)
# Emulator with archived files (like FBNeo with neogeo.zip)
emu_archive = {
"emulator": "TestArchiveEmu",
"type": "libretro",
"systems": ["console-a"],
"files": [
{"name": "rom_a.bin", "required": True, "archive": "test_archive.zip"},
{"name": "rom_b.bin", "required": False, "archive": "test_archive.zip"},
{
"name": "missing_rom.bin",
"required": True,
"archive": "missing_archive.zip",
},
],
}
with open(os.path.join(self.emulators_dir, "test_archive_emu.yml"), "w") as fh:
yaml.dump(emu_archive, fh)
# Emulator with descriptive name and path (like QEMU SeaBIOS)
emu_descriptive = {
"emulator": "TestDescriptive",
"type": "libretro",
"systems": ["console-a"],
"files": [
{
"name": "Descriptive BIOS Name",
"required": True,
"path": "present_req.bin",
},
{
"name": "Missing Descriptive",
"required": True,
"path": "nonexistent_path.bin",
},
],
}
with open(os.path.join(self.emulators_dir, "test_descriptive.yml"), "w") as fh:
yaml.dump(emu_descriptive, fh)
# Emulator with validation checks (size, crc32)
emu_val = {
"emulator": "TestValidation",
"type": "libretro",
"systems": ["console-a", "sys-md5"],
"files": [
# Size validation -correct size (16 bytes = len(b"PRESENT_REQUIRED"))
{
"name": "present_req.bin",
"required": True,
"validation": ["size"],
"size": 16,
"source_ref": "test.c:10-20",
},
# Size validation -wrong expected size
{
"name": "present_opt.bin",
"required": False,
"validation": ["size"],
"size": 9999,
},
# CRC32 validation -correct crc32
{
"name": "correct_hash.bin",
"required": True,
"validation": ["crc32"],
"crc32": "91d0b1d3",
"source_ref": "hash.c:42",
},
# CRC32 validation -wrong crc32
{
"name": "no_md5.bin",
"required": False,
"validation": ["crc32"],
"crc32": "deadbeef",
},
# CRC32 starting with '0' (regression: lstrip("0x") bug)
{
"name": "leading_zero_crc.bin",
"required": True,
"validation": ["crc32"],
"crc32": "0179e92e",
},
# MD5 validation -correct md5
{
"name": "correct_hash.bin",
"required": True,
"validation": ["md5"],
"md5": "4a8db431e3b1a1acacec60e3424c4ce8",
},
# SHA1 validation -correct sha1
{
"name": "correct_hash.bin",
"required": True,
"validation": ["sha1"],
"sha1": "a2ab6c95c5bbd191b9e87e8f4e85205a47be5764",
},
# MD5 validation -wrong md5
{
"name": "alias_target.bin",
"required": False,
"validation": ["md5"],
"md5": "0000000000000000000000000000dead",
},
# Adler32 -known_hash_adler32 field
{
"name": "present_req.bin",
"required": True,
"known_hash_adler32": None,
}, # placeholder, set below
# Min/max size range validation
{
"name": "present_req.bin",
"required": True,
"validation": ["size"],
"min_size": 10,
"max_size": 100,
},
# Signature -crypto check we can't reproduce, but size applies
{
"name": "correct_hash.bin",
"required": True,
"validation": ["size", "signature"],
"size": 17,
},
],
}
# Compute the actual adler32 of present_req.bin for the test fixture
import zlib as _zlib
with open(self.files["present_req.bin"]["path"], "rb") as _f:
_data = _f.read()
_adler = format(_zlib.adler32(_data) & 0xFFFFFFFF, "08x")
# Set the adler32 entry (the one with known_hash_adler32=None)
for entry in emu_val["files"]:
if (
entry.get("known_hash_adler32") is None
and "known_hash_adler32" in entry
):
entry["known_hash_adler32"] = f"0x{_adler}"
break
with open(os.path.join(self.emulators_dir, "test_validation.yml"), "w") as fh:
yaml.dump(emu_val, fh)
# Emulator A: declares present_req.bin at root (no path)
emu_root = {
"emulator": "TestRootCore",
"type": "libretro",
"systems": ["console-a"],
"files": [
{"name": "present_req.bin", "required": True},
],
}
with open(os.path.join(self.emulators_dir, "test_root_core.yml"), "w") as fh:
yaml.dump(emu_root, fh)
# Emulator B: declares same file at a subdirectory path
emu_subdir = {
"emulator": "TestSubdirCore",
"type": "libretro",
"systems": ["console-a"],
"files": [
{
"name": "present_req.bin",
"required": True,
"path": "subcore/bios/present_req.bin",
},
],
}
with open(os.path.join(self.emulators_dir, "test_subdir_core.yml"), "w") as fh:
yaml.dump(emu_subdir, fh)
# Emulator whose file is declared by platform under a different name
# (e.g. gsplus ROM vs Batocera ROM1) — hash-based matching should resolve
emu_renamed = {
"emulator": "TestRenamed",
"type": "standalone",
"systems": ["sys-renamed"],
"files": [
{"name": "correct_hash.bin", "required": True},
],
}
with open(os.path.join(self.emulators_dir, "test_renamed.yml"), "w") as fh:
yaml.dump(emu_renamed, fh)
# Agnostic profile (bios_mode: agnostic) — skipped by find_undeclared_files
emu_agnostic = {
"emulator": "TestAgnostic",
"type": "standalone",
"bios_mode": "agnostic",
"systems": ["console-a"],
"files": [
{
"name": "correct_hash.bin",
"required": True,
"min_size": 1,
"max_size": 999999,
},
],
}
with open(os.path.join(self.emulators_dir, "test_agnostic.yml"), "w") as fh:
yaml.dump(emu_agnostic, fh)
# Mixed profile with per-file agnostic
emu_mixed_agnostic = {
"emulator": "TestMixedAgnostic",
"type": "libretro",
"systems": ["console-a"],
"files": [
{"name": "undeclared_req.bin", "required": True},
{"name": "agnostic_file.bin", "required": True, "agnostic": True},
],
}
with open(
os.path.join(self.emulators_dir, "test_mixed_agnostic.yml"), "w"
) as fh:
yaml.dump(emu_mixed_agnostic, fh)
# ---------------------------------------------------------------
# THE TEST -one method per feature area, all using same fixtures
# ---------------------------------------------------------------
def test_01_resolve_sha1(self):
entry = {
"name": "present_req.bin",
"sha1": self.files["present_req.bin"]["sha1"],
}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "sha1_exact")
self.assertIn("present_req.bin", path)
def test_02_resolve_md5(self):
entry = {
"name": "correct_hash.bin",
"md5": self.files["correct_hash.bin"]["md5"],
}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "md5_exact")
def test_03_resolve_name_no_md5(self):
entry = {"name": "no_md5.bin"}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "name_exact")
def test_04_resolve_alias(self):
entry = {"name": "alias_alt.bin", "aliases": []}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "name_exact")
self.assertIn("alias_target.bin", path)
def test_05_resolve_truncated_md5(self):
truncated = self.files["truncated.bin"]["md5"][:29]
entry = {"name": "truncated.bin", "md5": truncated}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "md5_exact")
def test_06_resolve_not_found(self):
entry = {"name": "nonexistent.bin", "sha1": "0" * 40}
path, status = resolve_local_file(entry, self.db)
self.assertIsNone(path)
self.assertEqual(status, "not_found")
def test_07_resolve_hash_mismatch(self):
entry = {"name": "wrong_hash.bin", "md5": "ffffffffffffffffffffffffffffffff"}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "hash_mismatch")
def test_07b_resolve_unsourceable_ignores_homonym(self):
"""An unsourceable entry takes content, never a file of the same name."""
entry = {
"name": "wrong_hash.bin",
"md5": "ffffffffffffffffffffffffffffffff",
"unsourceable": "ships inside a paid application package",
}
path, status = resolve_local_file(entry, self.db)
self.assertIsNone(path)
self.assertEqual(status, "not_found")
entry = {"name": "no_md5.bin", "unsourceable": "written per title"}
path, status = resolve_local_file(entry, self.db)
self.assertIsNone(path)
self.assertEqual(status, "not_found")
# Collecting the real bytes still resolves it.
entry = {
"name": "present_req.bin",
"sha1": self.files["present_req.bin"]["sha1"],
"unsourceable": "ships inside a paid application package",
}
path, status = resolve_local_file(entry, self.db)
self.assertEqual(status, "sha1_exact")
self.assertIn("present_req.bin", path)
def test_08_resolve_variants_deprioritized(self):
entry = {"name": "present_req.bin"}
path, status = resolve_local_file(entry, self.db)
self.assertNotIn(".variants", path)
def test_09_resolve_zip_contents(self):
zc = build_zip_contents_index(self.db)
inner_md5 = self.files["good.zip"]["inner_md5s"]["inner.rom"]
entry = {"name": "good.zip", "md5": inner_md5, "zipped_file": "inner.rom"}
path, status = resolve_local_file(entry, self.db, zc)
# Should find via name match (hash_mismatch since container md5 != inner md5)
# then zip_contents would be fallback
self.assertIsNotNone(path)
def test_10_md5_composite(self):
expected = hashlib.md5(b"AAAA" + b"BBBB").hexdigest()
actual = md5_composite(self.files["composite.zip"]["path"])
self.assertEqual(actual, expected)
def test_11_check_inside_zip_match(self):
inner_md5 = self.files["good.zip"]["inner_md5s"]["inner.rom"]
r = check_inside_zip(self.files["good.zip"]["path"], "inner.rom", inner_md5)
self.assertEqual(r, "ok")
def test_12_check_inside_zip_mismatch(self):
r = check_inside_zip(self.files["bad_inner.zip"]["path"], "inner.rom", "wrong")
self.assertEqual(r, "untested")
def test_13_check_inside_zip_not_found(self):
r = check_inside_zip(self.files["missing_inner.zip"]["path"], "nope.rom", "abc")
self.assertEqual(r, "not_in_zip")
def test_14_check_inside_zip_casefold(self):
inner_md5 = self.files["good.zip"]["inner_md5s"]["inner.rom"]
r = check_inside_zip(self.files["good.zip"]["path"], "INNER.ROM", inner_md5)
self.assertEqual(r, "ok")
def test_20_verify_existence_platform(self):
config = load_platform_config("test_existence", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
c = result["severity_counts"]
total = result["total_files"]
# 2 present (1 req + 1 opt), 2 missing (1 req WARNING + 1 opt INFO)
self.assertEqual(c[Severity.OK], 2)
self.assertEqual(c[Severity.WARNING], 1) # required missing
self.assertEqual(c[Severity.INFO], 1) # optional missing
self.assertEqual(sum(c.values()), total)
def test_21_verify_md5_platform(self):
config = load_platform_config("test_md5", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
c = result["severity_counts"]
total = result["total_files"]
self.assertEqual(sum(c.values()), total)
# At least some OK and some non-OK
self.assertGreater(c[Severity.OK], 0)
self.assertGreater(total, c[Severity.OK])
def test_22_verify_required_propagated(self):
config = load_platform_config("test_md5", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
for d in result["details"]:
self.assertIn("required", d)
def test_23_verify_missing_required_is_critical(self):
config = load_platform_config("test_md5", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
c = result["severity_counts"]
self.assertGreater(c[Severity.CRITICAL], 0)
def test_24_verify_missing_optional_is_warning(self):
config = load_platform_config("test_md5", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
c = result["severity_counts"]
self.assertGreater(c[Severity.WARNING], 0)
def test_25_verify_sha1_platform(self):
config = load_platform_config("test_sha1", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
self.assertEqual(result["total_files"], 5)
self.assertEqual(result["verification_mode"], "sha1")
ok_count = result["severity_counts"][Severity.OK]
self.assertEqual(ok_count, 2)
def test_26_sha1_mismatch_is_warning(self):
config = load_platform_config("test_sha1", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
by_name = {d["name"]: d for d in result["details"]}
self.assertEqual(by_name["wrong_hash.bin"]["status"], Status.UNTESTED)
def test_27_sha1_missing_required_is_critical(self):
config = load_platform_config("test_sha1", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
c = result["severity_counts"]
self.assertGreater(c[Severity.CRITICAL], 0)
def test_28_sha1_missing_optional_is_warning(self):
config = load_platform_config("test_sha1", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
c = result["severity_counts"]
self.assertGreater(c[Severity.WARNING], 0)
def test_29_sha1_no_hash_is_existence_check(self):
config = load_platform_config("test_sha1", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
by_name = {d["name"]: d for d in result["details"]}
self.assertEqual(by_name["no_md5.bin"]["status"], Status.OK)
def test_30_inheritance_inherits_systems(self):
config = load_platform_config("test_inherited", self.platforms_dir)
self.assertEqual(config["platform"], "TestInherited")
self.assertEqual(config["base_destination"], "BIOS")
self.assertIn("console-a", config["systems"])
def test_31_shared_groups_injected(self):
config = load_platform_config("test_md5", self.platforms_dir)
names = [f["name"] for f in config["systems"]["sys-md5"]["files"]]
self.assertIn("shared_file.rom", names)
def test_40_cross_ref_finds_undeclared(self):
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
self.assertIn("undeclared_req.bin", names)
self.assertIn("undeclared_opt.bin", names)
def test_41_cross_ref_skips_standalone(self):
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
self.assertNotIn("standalone_only.bin", names)
def test_42_cross_ref_skips_alias_profiles(self):
profiles = load_emulator_profiles(self.emulators_dir)
self.assertNotIn("test_alias", profiles)
def test_42b_profile_hash_fields_load_as_str(self):
"""Unquoted digit-only hashes must load as strings, not YAML ints."""
with tempfile.TemporaryDirectory() as tmp:
Path(tmp, "numcrc.yml").write_text(
"emulator: NumCRC\n"
"type: libretro\n"
"files:\n"
" - name: kernal.bin\n"
" crc32: 70295038\n"
" md5: [12345678901234567890123456789012]\n"
)
profiles = load_emulator_profiles(tmp)
entry = profiles["numcrc"]["files"][0]
self.assertEqual(entry["crc32"], "70295038")
self.assertEqual(entry["md5"], ["12345678901234567890123456789012"])
def test_43_cross_ref_data_dir_does_not_suppress_files(self):
config = load_platform_config("test_md5", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
# dd_covered.bin is a file entry, not data_dir content -still undeclared
self.assertIn("dd_covered.bin", names)
def test_44_cross_ref_skips_launchers(self):
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
# launcher_bios.bin from TestLauncher should NOT appear
self.assertNotIn("launcher_bios.bin", names)
def test_45_hle_fallback_downgrades_severity(self):
"""Missing file with hle_fallback=true ->INFO severity, not CRITICAL."""
from verify import Severity, compute_severity
# required + missing + NO HLE = CRITICAL
sev = compute_severity("missing", True, "md5", hle_fallback=False)
self.assertEqual(sev, Severity.CRITICAL)
# required + missing + HLE = INFO
sev = compute_severity("missing", True, "md5", hle_fallback=True)
self.assertEqual(sev, Severity.INFO)
# required + missing + HLE + existence mode = INFO
sev = compute_severity("missing", True, "existence", hle_fallback=True)
self.assertEqual(sev, Severity.INFO)
def test_46_hle_index_built_from_emulator_profiles(self):
"""verify_platform reads hle_fallback from emulator profiles."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
# present_req.bin has hle_fallback: true in TestHLE profile
for d in result["details"]:
if d["name"] == "present_req.bin":
self.assertTrue(d.get("hle_fallback", False))
break
def test_47_cross_ref_shows_hle_on_undeclared(self):
"""Undeclared files include hle_fallback from emulator profile."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
hle_files = {u["name"] for u in undeclared if u.get("hle_fallback")}
self.assertIn("hle_missing.bin", hle_files)
def test_50_platform_grouping_identical(self):
groups = group_identical_platforms(
["test_existence", "test_inherited"], self.platforms_dir
)
# With flat ZIPs, base_destination no longer separates groups
# Platforms with same files (regardless of base_dest) are grouped
self.assertEqual(len(groups), 1)
self.assertEqual(len(groups[0][0]), 2)
def test_51_platform_grouping_same(self):
# Create two identical platforms
for name in ("dup_a", "dup_b"):
config = {
"platform": name,
"verification_mode": "existence",
"systems": {
"s": {"files": [{"name": "x.bin", "destination": "x.bin"}]}
},
}
with open(os.path.join(self.platforms_dir, f"{name}.yml"), "w") as fh:
yaml.dump(config, fh)
groups = group_identical_platforms(["dup_a", "dup_b"], self.platforms_dir)
self.assertEqual(len(groups), 1)
self.assertEqual(len(groups[0][0]), 2)
def test_60_storage_external(self):
from generate_pack import resolve_file
entry = {"name": "large.pup", "storage": "external"}
path, status = resolve_file(entry, self.db, self.bios_dir)
self.assertIsNone(path)
self.assertEqual(status, "external")
def test_61_storage_user_provided(self):
from generate_pack import resolve_file
entry = {"name": "user.bin", "storage": "user_provided"}
path, status = resolve_file(entry, self.db, self.bios_dir)
self.assertIsNone(path)
self.assertEqual(status, "user_provided")
def test_resolve_cores_all_libretro(self):
"""all_libretro resolves to all libretro-type profiles, excludes alias/standalone."""
config = {"cores": "all_libretro", "systems": {"nes": {"files": []}}}
profiles = {
"fceumm": {"type": "libretro", "systems": ["nes"], "files": []},
"dolphin_standalone": {
"type": "standalone",
"systems": ["gc"],
"files": [],
},
"gambatte": {"type": "pure_libretro", "systems": ["gb"], "files": []},
"mednafen_psx_hw": {"type": "alias", "alias_of": "beetle_psx", "files": []},
}
result = resolve_platform_cores(config, profiles)
self.assertEqual(result, {"fceumm", "gambatte"})
def test_resolve_cores_explicit_list(self):
"""Explicit cores list matches against profile dict keys."""
config = {"cores": ["fbneo", "opera"], "systems": {"arcade": {"files": []}}}
profiles = {
"fbneo": {"type": "pure_libretro", "systems": ["arcade"], "files": []},
"opera": {"type": "libretro", "systems": ["3do"], "files": []},
"mame": {"type": "libretro", "systems": ["arcade"], "files": []},
}
result = resolve_platform_cores(config, profiles)
self.assertEqual(result, {"fbneo", "opera"})
def test_resolve_cores_fallback_systems(self):
"""Missing cores: field falls back to system ID intersection."""
config = {"systems": {"nes": {"files": []}}}
profiles = {
"fceumm": {"type": "libretro", "systems": ["nes"], "files": []},
"dolphin": {"type": "libretro", "systems": ["gc"], "files": []},
}
result = resolve_platform_cores(config, profiles)
self.assertEqual(result, {"fceumm"})
def test_resolve_cores_excludes_alias(self):
"""Alias profiles never included even if name matches cores list."""
config = {"cores": ["mednafen_psx_hw"], "systems": {}}
profiles = {
"mednafen_psx_hw": {"type": "alias", "alias_of": "beetle_psx", "files": []},
}
result = resolve_platform_cores(config, profiles)
self.assertEqual(result, set())
def test_cross_reference_uses_core_resolution(self):
"""Cross-reference matches by cores: field, not system intersection."""
config = {
"cores": ["fbneo"],
"systems": {"arcade": {"files": [{"name": "neogeo.zip", "md5": "abc"}]}},
}
profiles = {
"fbneo": {
"emulator": "FBNeo",
"systems": ["snk-neogeo-mvs"],
"type": "pure_libretro",
"files": [
{"name": "neogeo.zip", "required": True},
{"name": "neocdz.zip", "required": True},
],
},
}
db = {"indexes": {"by_name": {"neocdz.zip": {"sha1": "x"}}}}
undeclared = find_undeclared_files(config, self.emulators_dir, db, profiles)
names = [u["name"] for u in undeclared]
self.assertIn("neocdz.zip", names)
self.assertNotIn("neogeo.zip", names)
def test_exclusion_notes_uses_core_resolution(self):
"""Exclusion notes match by cores: field, not system intersection."""
config = {"cores": ["desmume2015"], "systems": {"nds": {"files": []}}}
profiles = {
"desmume2015": {
"emulator": "DeSmuME 2015",
"type": "frozen_snapshot",
"systems": ["nintendo-ds"],
"files": [],
"exclusion_note": "Frozen snapshot, code never loads BIOS",
},
}
notes = find_exclusion_notes(config, self.emulators_dir, profiles)
emu_names = [n["emulator"] for n in notes]
self.assertIn("DeSmuME 2015", emu_names)
def test_70_validation_index_built(self):
"""Validation index extracts checks from emulator profiles."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
self.assertIn("present_req.bin", index)
self.assertIn("size", index["present_req.bin"]["checks"])
self.assertIn(16, index["present_req.bin"]["sizes"])
self.assertIn("correct_hash.bin", index)
self.assertIn("crc32", index["correct_hash.bin"]["checks"])
def test_71_validation_size_pass(self):
"""File with correct size passes validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["present_req.bin"]["path"]
reason = check_file_validation(path, "present_req.bin", index)
self.assertIsNone(reason)
def test_72_validation_size_fail(self):
"""File with wrong size fails validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["present_opt.bin"]["path"]
result = check_file_validation(path, "present_opt.bin", index)
self.assertIsNotNone(result)
reason, emus = result
self.assertIn("size mismatch", reason)
self.assertIsInstance(emus, list)
def test_73_validation_crc32_pass(self):
"""File with correct CRC32 passes validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["correct_hash.bin"]["path"]
reason = check_file_validation(path, "correct_hash.bin", index)
self.assertIsNone(reason)
def test_74_validation_crc32_fail(self):
"""File with wrong CRC32 fails validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["no_md5.bin"]["path"]
result = check_file_validation(path, "no_md5.bin", index)
self.assertIsNotNone(result)
reason, emus = result
self.assertIn("crc32 mismatch", reason)
self.assertIsInstance(emus, list)
def test_75_validation_applied_in_existence_mode(self):
"""Existence mode reports discrepancy when validation fails, keeps OK."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
# present_opt.bin exists but has wrong expected size - OK with discrepancy
for d in result["details"]:
if d["name"] == "present_opt.bin":
self.assertEqual(d["status"], Status.OK)
self.assertIn("size mismatch", d.get("discrepancy", ""))
break
else:
self.fail("present_opt.bin not found in details")
def test_77_validation_crc32_leading_zero(self):
"""CRC32 starting with '0' must not be truncated (lstrip regression)."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["leading_zero_crc.bin"]["path"]
reason = check_file_validation(path, "leading_zero_crc.bin", index)
self.assertIsNone(reason)
def test_78_validation_multi_size_accepted(self):
"""Multiple valid sizes from different profiles are collected as a set."""
profiles = {
"emu_a": {
"type": "libretro",
"files": [
{"name": "shared.bin", "validation": ["size"], "size": 512},
],
},
"emu_b": {
"type": "libretro",
"files": [
{"name": "shared.bin", "validation": ["size"], "size": 1024},
],
},
}
index = _build_validation_index(profiles)
self.assertEqual(index["shared.bin"]["sizes"], {512, 1024})
def test_79_validation_md5_pass(self):
"""File with correct MD5 passes validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["correct_hash.bin"]["path"]
reason = check_file_validation(path, "correct_hash.bin", index)
self.assertIsNone(reason)
def test_80_validation_md5_fail(self):
"""File with wrong MD5 fails validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["alias_target.bin"]["path"]
result = check_file_validation(path, "alias_target.bin", index)
self.assertIsNotNone(result)
reason, emus = result
self.assertIn("md5 mismatch", reason)
self.assertIsInstance(emus, list)
def test_81_validation_index_has_md5_sha1(self):
"""Validation index stores md5 and sha1 when declared."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
self.assertIn("md5", index["correct_hash.bin"]["checks"])
self.assertIn("sha1", index["correct_hash.bin"]["checks"])
self.assertIsNotNone(index["correct_hash.bin"]["md5"])
self.assertIsNotNone(index["correct_hash.bin"]["sha1"])
def test_82_validation_adler32_pass(self):
"""File with correct adler32 passes validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["present_req.bin"]["path"]
reason = check_file_validation(path, "present_req.bin", index)
self.assertIsNone(reason)
def test_83_validation_min_max_size_pass(self):
"""File within min/max size range passes validation."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
path = self.files["present_req.bin"]["path"]
reason = check_file_validation(path, "present_req.bin", index)
self.assertIsNone(reason)
# Verify the index has min/max
self.assertEqual(index["present_req.bin"]["min_size"], 10)
self.assertEqual(index["present_req.bin"]["max_size"], 100)
def test_84_validation_crypto_tracked(self):
"""Signature/crypto checks are tracked as non-reproducible."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
# correct_hash.bin has [size, signature]
self.assertIn("signature", index["correct_hash.bin"]["crypto_only"])
# Size check still applies despite signature being non-reproducible
path = self.files["correct_hash.bin"]["path"]
reason = check_file_validation(path, "correct_hash.bin", index)
self.assertIsNone(reason) # size=16 matches
def test_76_validation_no_effect_when_no_field(self):
"""Files without validation field are unaffected."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
# wrong_hash.bin has no validation in any profile
path = self.files["wrong_hash.bin"]["path"]
reason = check_file_validation(path, "wrong_hash.bin", index)
self.assertIsNone(reason)
# ---------------------------------------------------------------
# Emulator/system mode verification
# ---------------------------------------------------------------
def test_90_verify_emulator_basic(self):
"""verify_emulator returns correct counts for a profile with mixed present/missing."""
result = verify_emulator(["test_emu"], self.emulators_dir, self.db)
self.assertIn("test_emu", result["emulators"])
# present_req.bin and alias_target.bin are present, others missing
self.assertGreater(result["total_files"], 0)
self.assertGreater(result["severity_counts"][Severity.OK], 0)
def test_91_verify_emulator_standalone_filters(self):
"""Standalone mode includes mode:standalone files, excludes mode:libretro."""
result_lr = verify_emulator(
["test_emu"], self.emulators_dir, self.db, standalone=False
)
result_sa = verify_emulator(
["test_emu"], self.emulators_dir, self.db, standalone=True
)
lr_names = {d["name"] for d in result_lr["details"]}
sa_names = {d["name"] for d in result_sa["details"]}
# standalone_only.bin should be in standalone, not libretro
self.assertNotIn("standalone_only.bin", lr_names)
self.assertIn("standalone_only.bin", sa_names)
def test_102_resolve_dest_hint_disambiguates(self):
"""dest_hint resolves regional variants with same name to distinct files."""
usa_path, usa_status = resolve_local_file(
{"name": "BIOS.bin"},
self.db,
dest_hint="TestConsole/USA/BIOS.bin",
)
eur_path, eur_status = resolve_local_file(
{"name": "BIOS.bin"},
self.db,
dest_hint="TestConsole/EUR/BIOS.bin",
)
self.assertIsNotNone(usa_path)
self.assertIsNotNone(eur_path)
self.assertEqual(usa_status, "path_exact")
self.assertEqual(eur_status, "path_exact")
# Must be DIFFERENT files
self.assertNotEqual(usa_path, eur_path)
# Verify content
with open(usa_path, "rb") as f:
self.assertEqual(f.read(), b"BIOS_USA_CONTENT")
with open(eur_path, "rb") as f:
self.assertEqual(f.read(), b"BIOS_EUR_CONTENT")
def test_103_resolve_dest_hint_fallback_to_name(self):
"""Without dest_hint, falls back to by_name (first candidate)."""
path, status = resolve_local_file({"name": "BIOS.bin"}, self.db)
self.assertIsNotNone(path)
# Still finds something (first candidate by name)
def test_92_verify_emulator_libretro_only_rejects_standalone(self):
"""Libretro-only profile rejects --standalone."""
with self.assertRaises(SystemExit):
verify_emulator(["test_hle"], self.emulators_dir, self.db, standalone=True)
def test_92b_verify_emulator_game_type_rejects_standalone(self):
"""Game-type profile rejects --standalone."""
game = {
"emulator": "TestGame",
"type": "game",
"systems": ["console-a"],
"files": [],
}
with open(os.path.join(self.emulators_dir, "test_game.yml"), "w") as fh:
yaml.dump(game, fh)
with self.assertRaises(SystemExit):
verify_emulator(["test_game"], self.emulators_dir, self.db, standalone=True)
def test_93_verify_emulator_alias_rejected(self):
"""Alias profile produces error with redirect message."""
with self.assertRaises(SystemExit):
verify_emulator(["test_alias"], self.emulators_dir, self.db)
def test_94_verify_emulator_launcher_rejected(self):
"""Launcher profile produces error."""
with self.assertRaises(SystemExit):
verify_emulator(["test_launcher"], self.emulators_dir, self.db)
def test_95_verify_emulator_validation_applied(self):
"""Emulator mode applies validation checks as primary verification."""
result = verify_emulator(["test_validation"], self.emulators_dir, self.db)
# present_opt.bin has wrong size ->UNTESTED
for d in result["details"]:
if d["name"] == "present_opt.bin":
self.assertEqual(d["status"], Status.UNTESTED)
self.assertIn("size mismatch", d.get("reason", ""))
break
else:
self.fail("present_opt.bin not found in details")
def test_96_verify_emulator_multi(self):
"""Multi-emulator verify aggregates files."""
result = verify_emulator(
["test_emu", "test_hle"],
self.emulators_dir,
self.db,
)
self.assertEqual(len(result["emulators"]), 2)
all_names = {d["name"] for d in result["details"]}
# Files from both profiles
self.assertIn("present_req.bin", all_names)
self.assertIn("hle_missing.bin", all_names)
def test_97_verify_emulator_data_dir_notice(self):
"""Emulator with data_directories reports notice."""
result = verify_emulator(["test_emu"], self.emulators_dir, self.db)
self.assertIn("test-data-dir", result.get("data_dir_notices", []))
def test_98_verify_emulator_validation_label(self):
"""Validation label reflects the checks used."""
result = verify_emulator(["test_validation"], self.emulators_dir, self.db)
# test_validation has crc32, md5, sha1, size ->all listed
self.assertEqual(result["verification_mode"], "crc32+md5+sha1+signature+size")
def test_99filter_files_by_mode(self):
"""filter_files_by_mode correctly filters standalone/libretro."""
files = [
{"name": "a.bin"}, # no mode ->both
{"name": "b.bin", "mode": "libretro"}, # libretro only
{"name": "c.bin", "mode": "standalone"}, # standalone only
{"name": "d.bin", "mode": "both"}, # explicit both
]
lr = filter_files_by_mode(files, standalone=False)
sa = filter_files_by_mode(files, standalone=True)
lr_names = {f["name"] for f in lr}
sa_names = {f["name"] for f in sa}
self.assertEqual(lr_names, {"a.bin", "b.bin", "d.bin"})
self.assertEqual(sa_names, {"a.bin", "c.bin", "d.bin"})
def test_100_verify_emulator_empty_profile(self):
"""Profile with files:[] produces note, not error."""
empty = {
"emulator": "TestEmpty",
"type": "libretro",
"systems": ["console-a"],
"files": [],
"exclusion_note": "Code never loads BIOS",
}
with open(os.path.join(self.emulators_dir, "test_empty.yml"), "w") as fh:
yaml.dump(empty, fh)
result = verify_emulator(["test_empty"], self.emulators_dir, self.db)
# Should have a note entry, not crash
self.assertEqual(result["total_files"], 0)
notes = [d for d in result["details"] if d.get("note")]
self.assertTrue(len(notes) > 0)
def test_101_verify_emulator_severity_missing_required(self):
"""Missing required file in emulator mode ->WARNING severity."""
result = verify_emulator(["test_emu"], self.emulators_dir, self.db)
# undeclared_req.bin is required and missing
for d in result["details"]:
if d["name"] == "undeclared_req.bin":
self.assertEqual(d["status"], Status.MISSING)
self.assertTrue(d["required"])
break
else:
self.fail("undeclared_req.bin not found")
# Severity should be WARNING (existence mode base)
self.assertGreater(result["severity_counts"][Severity.WARNING], 0)
def test_102_safe_extract_zip_blocks_traversal(self):
"""safe_extract_zip must reject zip-slip path traversal."""
malicious_zip = os.path.join(self.root, "evil.zip")
with zipfile.ZipFile(malicious_zip, "w") as zf:
zf.writestr("../../etc/passwd", "root:x:0:0")
dest = os.path.join(self.root, "extract_dest")
os.makedirs(dest)
with self.assertRaises(ValueError):
safe_extract_zip(malicious_zip, dest)
def test_103_safe_extract_zip_normal(self):
"""safe_extract_zip extracts valid files correctly."""
normal_zip = os.path.join(self.root, "normal.zip")
with zipfile.ZipFile(normal_zip, "w") as zf:
zf.writestr("subdir/file.txt", "hello")
dest = os.path.join(self.root, "extract_normal")
os.makedirs(dest)
safe_extract_zip(normal_zip, dest)
extracted = os.path.join(dest, "subdir", "file.txt")
self.assertTrue(os.path.exists(extracted))
with open(extracted) as f:
self.assertEqual(f.read(), "hello")
def test_104_compute_hashes_correctness(self):
"""compute_hashes returns correct values for known content."""
test_file = os.path.join(self.root, "hash_test.bin")
data = b"retrobios test content"
with open(test_file, "wb") as f:
f.write(data)
import zlib
expected_sha1 = hashlib.sha1(data).hexdigest()
expected_md5 = hashlib.md5(data).hexdigest()
expected_sha256 = hashlib.sha256(data).hexdigest()
expected_crc32 = format(zlib.crc32(data) & 0xFFFFFFFF, "08x")
result = compute_hashes(test_file)
self.assertEqual(result["sha1"], expected_sha1)
self.assertEqual(result["md5"], expected_md5)
self.assertEqual(result["sha256"], expected_sha256)
self.assertEqual(result["crc32"], expected_crc32)
def test_105_resolve_with_empty_database(self):
"""resolve_local_file handles empty database gracefully."""
empty_db = {
"files": {},
"indexes": {"by_md5": {}, "by_name": {}, "by_path_suffix": {}},
}
entry = {"name": "nonexistent.bin", "sha1": "abc123"}
path, status = resolve_local_file(entry, empty_db)
self.assertIsNone(path)
self.assertEqual(status, "not_found")
def test_106_parse_md5_list(self):
"""parse_md5_list normalizes comma-separated MD5s."""
self.assertEqual(parse_md5_list(""), [])
self.assertEqual(parse_md5_list("ABC123"), ["abc123"])
self.assertEqual(parse_md5_list("abc, DEF , ghi"), ["abc", "def", "ghi"])
self.assertEqual(parse_md5_list(",,,"), [])
def test_107filter_files_by_mode(self):
"""filter_files_by_mode filters standalone/libretro correctly."""
files = [
{"name": "a.bin", "mode": "standalone"},
{"name": "b.bin", "mode": "libretro"},
{"name": "c.bin", "mode": "both"},
{"name": "d.bin"}, # no mode
]
# Libretro mode: exclude standalone
result = filter_files_by_mode(files, standalone=False)
names = [f["name"] for f in result]
self.assertNotIn("a.bin", names)
self.assertIn("b.bin", names)
self.assertIn("c.bin", names)
self.assertIn("d.bin", names)
# Standalone mode: exclude libretro
result = filter_files_by_mode(files, standalone=True)
names = [f["name"] for f in result]
self.assertIn("a.bin", names)
self.assertNotIn("b.bin", names)
self.assertIn("c.bin", names)
self.assertIn("d.bin", names)
def test_108_standalone_path_in_undeclared(self):
"""Undeclared files use standalone_path when core is in standalone_cores."""
# Create a platform with standalone_cores
config = {
"platform": "TestStandalone",
"verification_mode": "existence",
"cores": ["test_emu"],
"standalone_cores": ["test_emu"],
"systems": {
"console-a": {
"files": [
{
"name": "present_req.bin",
"destination": "present_req.bin",
"required": True,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_standalone.yml"), "w") as fh:
yaml.dump(config, fh)
# Create emulator with standalone_path divergence
emu = {
"emulator": "TestStandaloneEmu",
"type": "standalone + libretro",
"cores": ["test_emu"],
"systems": ["console-a"],
"files": [
{
"name": "libretro_file.bin",
"path": "subdir/libretro_file.bin",
"standalone_path": "flat_file.bin",
"required": True,
},
{
"name": "standalone_only.bin",
"mode": "standalone",
"required": False,
},
{"name": "libretro_only.bin", "mode": "libretro", "required": False},
],
}
with open(
os.path.join(self.emulators_dir, "test_standalone_emu.yml"), "w"
) as fh:
yaml.dump(emu, fh)
config = load_platform_config("test_standalone", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
by_name = {u["name"]: u for u in undeclared}
# standalone_path used for undeclared file (core is standalone)
self.assertIn("libretro_file.bin", by_name)
self.assertEqual(by_name["libretro_file.bin"]["path"], "flat_file.bin")
# standalone-only file IS included (core is standalone)
self.assertIn("standalone_only.bin", by_name)
# libretro-only file is EXCLUDED (core is standalone)
self.assertNotIn("libretro_only.bin", by_name)
def test_109_no_standalone_cores_uses_libretro_path(self):
"""Without standalone_cores, undeclared files use path: (libretro)."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
# standalone_only.bin should be excluded (platform has no standalone_cores)
names = {u["name"] for u in undeclared}
self.assertNotIn("standalone_only.bin", names)
def test_110_cores_alias_reverse_index(self):
"""resolve_platform_cores matches via cores: field aliases."""
emu = {
"emulator": "TestAliasCore",
"type": "libretro",
"cores": ["test_alias_core", "shortname"],
"systems": ["console-a"],
"files": [],
}
with open(os.path.join(self.emulators_dir, "test_alias_core.yml"), "w") as fh:
yaml.dump(emu, fh)
config = {"cores": ["shortname"]}
profiles = load_emulator_profiles(self.emulators_dir)
resolved = resolve_platform_cores(config, profiles)
self.assertIn("test_alias_core", resolved)
# ---------------------------------------------------------------
# Target config tests (Task 1)
# ---------------------------------------------------------------
def _write_target_fixtures(self):
"""Create target config fixtures for testing."""
targets_dir = os.path.join(self.platforms_dir, "targets")
os.makedirs(targets_dir, exist_ok=True)
target_config = {
"platform": "testplatform",
"source": "test",
"scraped_at": "2026-01-01T00:00:00Z",
"targets": {
"target-full": {
"architecture": "x86_64",
"cores": ["core_a", "core_b", "core_c"],
},
"target-minimal": {
"architecture": "armv7",
"cores": ["core_a"],
},
},
}
with open(os.path.join(targets_dir, "testplatform.yml"), "w") as f:
yaml.dump(target_config, f)
single_config = {
"platform": "singleplatform",
"source": "test",
"scraped_at": "2026-01-01T00:00:00Z",
"targets": {
"only-target": {
"architecture": "x86_64",
"cores": ["core_a", "core_b"],
},
},
}
with open(os.path.join(targets_dir, "singleplatform.yml"), "w") as f:
yaml.dump(single_config, f)
overrides = {
"testplatform": {
"targets": {
"target-full": {
"aliases": ["full", "pc", "desktop"],
"add_cores": ["core_d"],
"remove_cores": ["core_c"],
},
"target-minimal": {
"aliases": ["minimal", "arm"],
},
},
},
}
with open(os.path.join(targets_dir, "_overrides.yml"), "w") as f:
yaml.dump(overrides, f)
def test_load_target_config(self):
self._write_target_fixtures()
from common import load_target_config
cores = load_target_config("testplatform", "target-minimal", self.platforms_dir)
self.assertEqual(cores, {"core_a"})
def test_target_alias_resolution(self):
self._write_target_fixtures()
from common import load_target_config
cores = load_target_config("testplatform", "full", self.platforms_dir)
self.assertEqual(cores, {"core_a", "core_b", "core_d"})
def test_target_unknown_error(self):
self._write_target_fixtures()
from common import load_target_config
with self.assertRaises(ValueError) as ctx:
load_target_config("testplatform", "nonexistent", self.platforms_dir)
self.assertIn("target-full", str(ctx.exception))
self.assertIn("target-minimal", str(ctx.exception))
def test_target_override_add_remove(self):
self._write_target_fixtures()
from common import load_target_config
cores = load_target_config("testplatform", "full", self.platforms_dir)
self.assertIn("core_d", cores)
self.assertNotIn("core_c", cores)
self.assertIn("core_a", cores)
self.assertIn("core_b", cores)
def test_target_single_target_noop(self):
self._write_target_fixtures()
from common import load_target_config
cores = load_target_config("singleplatform", "only-target", self.platforms_dir)
self.assertEqual(cores, {"core_a", "core_b"})
def test_target_inherits(self):
self._write_target_fixtures()
targets_dir = os.path.join(self.platforms_dir, "targets")
child_config = {
"platform": "childplatform",
"source": "test",
"scraped_at": "2026-01-01T00:00:00Z",
"targets": {
"target-full": {
"architecture": "x86_64",
"cores": ["core_a"],
},
},
}
with open(os.path.join(targets_dir, "childplatform.yml"), "w") as f:
yaml.dump(child_config, f)
from common import load_target_config
parent = load_target_config(
"testplatform", "target-minimal", self.platforms_dir
)
child = load_target_config("childplatform", "target-full", self.platforms_dir)
self.assertEqual(parent, {"core_a"})
self.assertEqual(child, {"core_a"})
self.assertNotEqual(
load_target_config("testplatform", "full", self.platforms_dir),
child,
)
# ---------------------------------------------------------------
# Target filtering in resolve_platform_cores (Task 2)
# ---------------------------------------------------------------
def test_target_core_intersection(self):
self._write_target_fixtures()
profiles = {
"core_a": {"type": "libretro", "systems": ["sys1"]},
"core_b": {"type": "libretro", "systems": ["sys1"]},
"core_c": {"type": "libretro", "systems": ["sys2"]},
"core_d": {"type": "libretro", "systems": ["sys2"]},
}
config = {"cores": "all_libretro"}
result = resolve_platform_cores(config, profiles)
self.assertEqual(result, {"core_a", "core_b", "core_c", "core_d"})
result = resolve_platform_cores(
config, profiles, target_cores={"core_a", "core_b"}
)
self.assertEqual(result, {"core_a", "core_b"})
def test_target_none_no_filter(self):
profiles = {
"core_a": {"type": "libretro", "systems": ["sys1"]},
"core_b": {"type": "libretro", "systems": ["sys1"]},
}
config = {"cores": "all_libretro"}
result = resolve_platform_cores(config, profiles, target_cores=None)
self.assertEqual(result, {"core_a", "core_b"})
def test_verify_target_filtered(self):
"""Verify with target_cores only reports files from filtered cores."""
self._write_target_fixtures()
core_a_path = os.path.join(self.emulators_dir, "core_a.yml")
core_b_path = os.path.join(self.emulators_dir, "core_b.yml")
with open(core_a_path, "w") as f:
yaml.dump(
{
"emulator": "CoreA",
"type": "libretro",
"systems": ["sys1"],
"files": [{"name": "bios_a.bin", "required": True}],
},
f,
)
with open(core_b_path, "w") as f:
yaml.dump(
{
"emulator": "CoreB",
"type": "libretro",
"systems": ["sys1"],
"files": [{"name": "bios_b.bin", "required": True}],
},
f,
)
config = {"cores": "all_libretro", "systems": {"sys1": {"files": []}}}
profiles = load_emulator_profiles(self.emulators_dir)
# Without target: both cores' files are undeclared
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
self.assertIn("bios_a.bin", names)
self.assertIn("bios_b.bin", names)
# With target filtering to core_a only
undeclared = find_undeclared_files(
config,
self.emulators_dir,
self.db,
profiles,
target_cores={"core_a"},
)
names = {u["name"] for u in undeclared}
self.assertIn("bios_a.bin", names)
self.assertNotIn("bios_b.bin", names)
# ---------------------------------------------------------------
# Validation index per-emulator ground truth (Task: ground truth)
# ---------------------------------------------------------------
def test_111_validation_index_per_emulator(self):
"""Validation index includes per-emulator detail for ground truth."""
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
entry = index["present_req.bin"]
self.assertIn("per_emulator", entry)
pe = entry["per_emulator"]
self.assertIn("test_validation", pe)
detail = pe["test_validation"]
self.assertIn("size", detail["checks"])
self.assertEqual(detail["expected"]["size"], 16)
def test_112_build_ground_truth(self):
"""build_ground_truth returns per-emulator detail for a filename."""
from validation import build_ground_truth
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
gt = build_ground_truth("present_req.bin", index)
self.assertIsInstance(gt, list)
self.assertTrue(len(gt) >= 1)
emu_names = {g["emulator"] for g in gt}
self.assertIn("test_validation", emu_names)
for g in gt:
if g["emulator"] == "test_validation":
self.assertIn("size", g["checks"])
self.assertIn("source_ref", g)
self.assertIn("expected", g)
def test_113_build_ground_truth_empty(self):
"""build_ground_truth returns [] for unknown filename."""
from validation import build_ground_truth
profiles = load_emulator_profiles(self.emulators_dir)
index = _build_validation_index(profiles)
gt = build_ground_truth("nonexistent.bin", index)
self.assertEqual(gt, [])
def test_114_platform_result_has_ground_truth(self):
"""verify_platform attaches ground_truth to each detail entry."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
for d in result["details"]:
self.assertIn("ground_truth", d)
# present_req.bin has validation in test_validation profile
for d in result["details"]:
if d["name"] == "present_req.bin":
self.assertTrue(len(d["ground_truth"]) >= 1)
emu_names = {g["emulator"] for g in d["ground_truth"]}
self.assertIn("test_validation", emu_names)
break
else:
self.fail("present_req.bin not found in details")
def test_116_undeclared_files_have_ground_truth(self):
"""find_undeclared_files attaches ground truth fields."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
for u in undeclared:
self.assertIn("checks", u)
self.assertIn("source_ref", u)
self.assertIn("expected", u)
def test_117_platform_result_ground_truth_coverage(self):
"""verify_platform includes ground truth coverage counts."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
gt = result["ground_truth_coverage"]
self.assertIn("with_validation", gt)
self.assertIn("total", gt)
self.assertIn("platform_only", gt)
self.assertEqual(gt["total"], result["total_files"])
self.assertEqual(gt["platform_only"], gt["total"] - gt["with_validation"])
self.assertGreaterEqual(gt["with_validation"], 1)
def test_118_emulator_result_has_ground_truth(self):
"""verify_emulator attaches ground_truth to each detail entry."""
result = verify_emulator(["test_validation"], self.emulators_dir, self.db)
for d in result["details"]:
self.assertIn("ground_truth", d)
# present_req.bin should have ground truth from test_validation
for d in result["details"]:
if d["name"] == "present_req.bin":
self.assertTrue(len(d["ground_truth"]) >= 1)
break
def test_119_emulator_result_ground_truth_coverage(self):
"""verify_emulator includes ground truth coverage counts."""
result = verify_emulator(["test_validation"], self.emulators_dir, self.db)
gt = result["ground_truth_coverage"]
self.assertEqual(gt["total"], result["total_files"])
def test_115_platform_result_ground_truth_empty_for_unknown(self):
"""Files with no emulator validation get ground_truth=[]."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
for d in result["details"]:
if d["name"] == "missing_opt.bin":
self.assertEqual(d["ground_truth"], [])
break
def test_120_format_ground_truth_aggregate(self):
"""Aggregate format: one line with all cores."""
from verify import _format_ground_truth_aggregate
gt = [
{
"emulator": "beetle_psx",
"checks": ["md5"],
"source_ref": "libretro.cpp:252",
"expected": {"md5": "abc"},
},
{
"emulator": "pcsx_rearmed",
"checks": ["existence"],
"source_ref": None,
"expected": {},
},
]
line = _format_ground_truth_aggregate(gt)
self.assertIn("beetle_psx", line)
self.assertIn("[md5]", line)
self.assertIn("pcsx_rearmed", line)
self.assertIn("[existence]", line)
def test_121_format_ground_truth_verbose(self):
"""Verbose format: one line per core with expected values and source ref."""
from verify import _format_ground_truth_verbose
gt = [
{
"emulator": "handy",
"checks": ["size", "crc32"],
"source_ref": "rom.h:48-49",
"expected": {"size": 512, "crc32": "0d973c9d"},
},
]
lines = _format_ground_truth_verbose(gt)
self.assertEqual(len(lines), 1)
self.assertIn("handy", lines[0])
self.assertIn("size=512", lines[0])
self.assertIn("crc32=0d973c9d", lines[0])
self.assertIn("[rom.h:48-49]", lines[0])
def test_122_format_ground_truth_verbose_no_source_ref(self):
"""Verbose format omits bracket when source_ref is None."""
from verify import _format_ground_truth_verbose
gt = [
{
"emulator": "core_a",
"checks": ["existence"],
"source_ref": None,
"expected": {},
},
]
lines = _format_ground_truth_verbose(gt)
self.assertEqual(len(lines), 1)
self.assertNotIn("[", lines[0])
def test_123_ground_truth_full_chain_verbose(self):
"""Full chain: file -> platform -> emulator -> source_ref visible in ground_truth."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
for d in result["details"]:
if d["name"] == "present_req.bin":
gt = d["ground_truth"]
for g in gt:
if g["emulator"] == "test_validation":
self.assertIn("size", g["checks"])
self.assertEqual(g["source_ref"], "test.c:10-20")
self.assertEqual(g["expected"]["size"], 16)
return
self.fail("present_req.bin / test_validation ground truth not found")
def test_124_ground_truth_json_includes_all(self):
"""JSON output includes ground_truth on all detail entries."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
# Simulate --json filtering (non-OK only) -ground_truth must survive
filtered = [d for d in result["details"] if d["status"] != Status.OK]
for d in filtered:
self.assertIn("ground_truth", d)
# Also check OK entries have it (before filtering)
ok_entries = [d for d in result["details"] if d["status"] == Status.OK]
for d in ok_entries:
self.assertIn("ground_truth", d)
def test_125_ground_truth_coverage_in_md5_mode(self):
"""MD5 platform also gets ground truth coverage."""
config = load_platform_config("test_md5", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
result = verify_platform(config, self.db, self.emulators_dir, profiles)
gt = result["ground_truth_coverage"]
self.assertEqual(gt["total"], result["total_files"])
self.assertGreaterEqual(gt["with_validation"], 1)
def test_130_required_only_excludes_optional(self):
"""--required-only excludes files with required: false from pack."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_reqonly")
os.makedirs(output_dir, exist_ok=True)
# Create a platform with one required and one optional file
config = {
"platform": "ReqOnlyTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"test-sys": {
"files": [
{
"name": "present_req.bin",
"destination": "present_req.bin",
"required": True,
},
{
"name": "present_opt.bin",
"destination": "present_opt.bin",
"required": False,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_reqonly.yml"), "w") as fh:
yaml.dump(config, fh)
zip_path = generate_pack(
"test_reqonly",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
required_only=True,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("present_req.bin" in n for n in names))
self.assertFalse(any("present_opt.bin" in n for n in names))
# Verify _Required tag in filename
self.assertIn("_Required_", os.path.basename(zip_path))
def test_130b_existence_pack_reports_hash_mismatch(self):
"""Existence mode packs a mismatched file but prints the proof."""
import contextlib
import io
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_mismatch")
os.makedirs(output_dir, exist_ok=True)
config = {
"platform": "MismatchTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"test-sys": {
"files": [
{
"name": "wrong_hash.bin",
"destination": "wrong_hash.bin",
"md5": "ffffffffffffffffffffffffffffffff",
"required": True,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_mismatch.yml"), "w") as fh:
yaml.dump(config, fh)
out = io.StringIO()
with contextlib.redirect_stdout(out):
zip_path = generate_pack(
"test_mismatch",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
offline=True,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("wrong_hash.bin" in n for n in names))
report = out.getvalue()
self.assertIn("DISCREPANCY", report)
self.assertIn("declared hash ffffffffffffffffffffffffffffffff", report)
def test_131_required_only_keeps_default_required(self):
"""--required-only keeps files with no required field (default = required)."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_reqdef")
os.makedirs(output_dir, exist_ok=True)
# File with no required field
config = {
"platform": "ReqDefTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"test-sys": {
"files": [
{"name": "present_req.bin", "destination": "present_req.bin"},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_reqdef.yml"), "w") as fh:
yaml.dump(config, fh)
zip_path = generate_pack(
"test_reqdef",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
required_only=True,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("present_req.bin" in n for n in names))
def test_132_platform_system_filter(self):
"""--platform + --system filters systems within a platform pack."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_sysfilter")
os.makedirs(output_dir, exist_ok=True)
config = {
"platform": "SysFilterTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"system-a": {
"files": [
{"name": "present_req.bin", "destination": "present_req.bin"},
],
},
"system-b": {
"files": [
{"name": "present_opt.bin", "destination": "present_opt.bin"},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_sysfilter.yml"), "w") as fh:
yaml.dump(config, fh)
zip_path = generate_pack(
"test_sysfilter",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
system_filter=["system-a"],
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("present_req.bin" in n for n in names))
self.assertFalse(any("present_opt.bin" in n for n in names))
def test_133_platform_system_filter_normalized(self):
"""_norm_system_id normalization matches with manufacturer prefix."""
from common import _norm_system_id
self.assertEqual(
_norm_system_id("sony-playstation"),
_norm_system_id("playstation"),
)
def test_134_list_systems_platform_context(self):
"""list_platform_system_ids lists systems from a platform YAML."""
import io
from common import list_platform_system_ids
config = {
"platform": "ListSysTest",
"verification_mode": "existence",
"systems": {
"alpha-sys": {
"files": [
{"name": "a.bin", "destination": "a.bin"},
],
},
"beta-sys": {
"files": [
{"name": "b1.bin", "destination": "b1.bin"},
{"name": "b2.bin", "destination": "b2.bin"},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_listsys.yml"), "w") as fh:
yaml.dump(config, fh)
captured = io.StringIO()
old_stdout = sys.stdout
sys.stdout = captured
try:
list_platform_system_ids("test_listsys", self.platforms_dir)
finally:
sys.stdout = old_stdout
output = captured.getvalue()
self.assertIn("alpha-sys", output)
self.assertIn("beta-sys", output)
self.assertIn("1 file", output)
self.assertIn("2 files", output)
def test_135_split_by_system(self):
"""--split generates one ZIP per system in a subdirectory."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir = os.path.join(tmpdir, "platforms")
os.makedirs(plat_dir)
bios_dir = os.path.join(tmpdir, "bios", "Test")
os.makedirs(os.path.join(bios_dir, "SysA"))
os.makedirs(os.path.join(bios_dir, "SysB"))
emu_dir = os.path.join(tmpdir, "emulators")
os.makedirs(emu_dir)
out_dir = os.path.join(tmpdir, "dist")
file_a = os.path.join(bios_dir, "SysA", "bios_a.bin")
file_b = os.path.join(bios_dir, "SysB", "bios_b.bin")
with open(file_a, "wb") as f:
f.write(b"system_a")
with open(file_b, "wb") as f:
f.write(b"system_b")
from common import compute_hashes
ha = compute_hashes(file_a)
hb = compute_hashes(file_b)
db = {
"files": {
ha["sha1"]: {
"name": "bios_a.bin",
"md5": ha["md5"],
"sha1": ha["sha1"],
"sha256": ha["sha256"],
"path": file_a,
"paths": [file_a],
},
hb["sha1"]: {
"name": "bios_b.bin",
"md5": hb["md5"],
"sha1": hb["sha1"],
"sha256": hb["sha256"],
"path": file_b,
"paths": [file_b],
},
},
"indexes": {
"by_md5": {ha["md5"]: ha["sha1"], hb["md5"]: hb["sha1"]},
"by_name": {"bios_a.bin": [ha["sha1"]], "bios_b.bin": [hb["sha1"]]},
"by_crc32": {},
"by_path_suffix": {},
},
}
registry = {"platforms": {"splitplat": {"status": "active"}}}
with open(os.path.join(plat_dir, "_registry.yml"), "w") as f:
yaml.dump(registry, f)
plat_cfg = {
"platform": "SplitTest",
"verification_mode": "existence",
"systems": {
"test-system-a": {
"files": [{"name": "bios_a.bin", "sha1": ha["sha1"]}]
},
"test-system-b": {
"files": [{"name": "bios_b.bin", "sha1": hb["sha1"]}]
},
},
}
with open(os.path.join(plat_dir, "splitplat.yml"), "w") as f:
yaml.dump(plat_cfg, f)
from common import build_zip_contents_index, load_emulator_profiles
from generate_pack import generate_split_packs
zip_contents = build_zip_contents_index(db)
emu_profiles = load_emulator_profiles(emu_dir)
zip_paths = generate_split_packs(
"splitplat",
plat_dir,
db,
os.path.join(tmpdir, "bios"),
out_dir,
emulators_dir=emu_dir,
zip_contents=zip_contents,
emu_profiles=emu_profiles,
group_by="system",
)
self.assertEqual(len(zip_paths), 2)
# Check subdirectory exists
split_dir = os.path.join(out_dir, "SplitTest_Split")
self.assertTrue(os.path.isdir(split_dir))
# Verify each ZIP contains only its system's files
for zp in zip_paths:
with zipfile.ZipFile(zp) as zf:
names = zf.namelist()
basename = os.path.basename(zp)
if "System_A" in basename:
self.assertIn("bios_a.bin", names)
self.assertNotIn("bios_b.bin", names)
elif "System_B" in basename:
self.assertIn("bios_b.bin", names)
self.assertNotIn("bios_a.bin", names)
def test_136_derive_manufacturer(self):
"""derive_manufacturer extracts manufacturer correctly."""
from common import derive_manufacturer
# From system ID prefix
self.assertEqual(derive_manufacturer("sony-playstation", {}), "Sony")
self.assertEqual(derive_manufacturer("nintendo-snes", {}), "Nintendo")
self.assertEqual(derive_manufacturer("sega-saturn", {}), "Sega")
self.assertEqual(derive_manufacturer("atari-5200", {}), "Atari")
# From explicit manufacturer field
self.assertEqual(
derive_manufacturer("3do", {"manufacturer": "Panasonic|GoldStar"}),
"Panasonic",
)
# Various = skip to prefix check, then Other
self.assertEqual(
derive_manufacturer("arcade", {"manufacturer": "Various"}), "Other"
)
# Fallback
self.assertEqual(derive_manufacturer("dos", {}), "Other")
def test_137_group_systems_by_manufacturer(self):
"""_group_systems_by_manufacturer groups correctly."""
from generate_pack import _group_systems_by_manufacturer
systems = {
"sony-playstation": {"files": [{"name": "a.bin"}]},
"sony-psp": {"files": [{"name": "b.bin"}]},
"nintendo-snes": {"files": [{"name": "c.bin"}]},
"arcade": {"manufacturer": "Various", "files": [{"name": "d.bin"}]},
}
groups = _group_systems_by_manufacturer(systems, {}, "")
self.assertIn("Sony", groups)
self.assertEqual(sorted(groups["Sony"]), ["sony-playstation", "sony-psp"])
self.assertIn("Nintendo", groups)
self.assertEqual(groups["Nintendo"], ["nintendo-snes"])
self.assertIn("Other", groups)
self.assertEqual(groups["Other"], ["arcade"])
def test_138_parse_hash_input(self):
"""parse_hash_input handles various formats."""
from generate_pack import parse_hash_input
# Plain MD5
result = parse_hash_input("d8f1206299c48946e6ec5ef96d014eaa")
self.assertEqual(result, [("md5", "d8f1206299c48946e6ec5ef96d014eaa")])
# Comma-separated
result = parse_hash_input(
"d8f1206299c48946e6ec5ef96d014eaa,d8f1206299c48946e6ec5ef96d014eab"
)
self.assertEqual(len(result), 2)
# SHA1
sha1 = "a" * 40
result = parse_hash_input(sha1)
self.assertEqual(result, [("sha1", sha1)])
# CRC32
result = parse_hash_input("abcd1234")
self.assertEqual(result, [("crc32", "abcd1234")])
def test_139_parse_hash_file(self):
"""parse_hash_file handles comments, empty lines, various formats."""
from generate_pack import parse_hash_file
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("# PS1 BIOS files\n")
f.write("\n")
f.write("d8f1206299c48946e6ec5ef96d014eaa\n")
f.write("d8f1206299c48946e6ec5ef96d014eab scph5501.bin\n")
f.write("scph5502.bin d8f1206299c48946e6ec5ef96d014eac OK\n")
tmp_path = f.name
try:
result = parse_hash_file(tmp_path)
self.assertEqual(len(result), 3)
self.assertTrue(all(t == "md5" for t, _ in result))
finally:
os.unlink(tmp_path)
def test_140_lookup_hashes_found(self):
"""lookup_hashes returns file info for known hashes."""
import contextlib
import io
from generate_pack import lookup_hashes
db = {
"files": {
"sha1abc": {
"name": "test.bin",
"md5": "md5abc",
"sha1": "sha1abc",
"sha256": "sha256abc",
"paths": ["Mfr/Console/test.bin"],
"aliases": ["alt.bin"],
},
},
"indexes": {
"by_md5": {"md5abc": "sha1abc"},
"by_crc32": {},
},
}
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
lookup_hashes([("md5", "md5abc")], db, "bios", "emulators", "platforms")
output = buf.getvalue()
self.assertIn("test.bin", output)
self.assertIn("sha1abc", output)
self.assertIn("alt.bin", output)
def test_141_lookup_hashes_not_found(self):
"""lookup_hashes reports unknown hashes."""
import contextlib
import io
from generate_pack import lookup_hashes
db = {"files": {}, "indexes": {"by_md5": {}, "by_crc32": {}}}
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
lookup_hashes(
[("md5", "unknown123" + "0" * 22)], db, "bios", "emulators", "platforms"
)
output = buf.getvalue()
self.assertIn("NOT FOUND", output)
def test_142_from_md5_platform_pack(self):
"""--from-md5 with --platform generates correctly laid out ZIP."""
import tempfile
import zipfile
import yaml
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir = os.path.join(tmpdir, "platforms")
os.makedirs(plat_dir)
bios_dir = os.path.join(tmpdir, "bios", "Sony", "PS1")
os.makedirs(bios_dir)
emu_dir = os.path.join(tmpdir, "emulators")
os.makedirs(emu_dir)
out_dir = os.path.join(tmpdir, "dist")
bios_file = os.path.join(bios_dir, "scph5501.bin")
with open(bios_file, "wb") as f:
f.write(b"ps1_bios_content")
from common import compute_hashes
h = compute_hashes(bios_file)
db = {
"files": {
h["sha1"]: {
"name": "scph5501.bin",
"md5": h["md5"],
"sha1": h["sha1"],
"sha256": h["sha256"],
"path": bios_file,
"paths": ["Sony/PS1/scph5501.bin"],
},
},
"indexes": {
"by_md5": {h["md5"]: h["sha1"]},
"by_name": {"scph5501.bin": [h["sha1"]]},
"by_crc32": {},
"by_path_suffix": {},
},
}
registry = {"platforms": {"testplat": {"status": "active"}}}
with open(os.path.join(plat_dir, "_registry.yml"), "w") as f:
yaml.dump(registry, f)
plat_cfg = {
"platform": "TestPlat",
"verification_mode": "md5",
"base_destination": "bios",
"systems": {
"sony-playstation": {
"files": [
{
"name": "scph5501.bin",
"md5": h["md5"],
"destination": "scph5501.bin",
},
]
}
},
}
with open(os.path.join(plat_dir, "testplat.yml"), "w") as f:
yaml.dump(plat_cfg, f)
from common import build_zip_contents_index
from generate_pack import generate_md5_pack
zip_contents = build_zip_contents_index(db)
zip_path = generate_md5_pack(
hashes=[("md5", h["md5"])],
db=db,
bios_dir=bios_dir,
output_dir=out_dir,
zip_contents=zip_contents,
platform_name="testplat",
platforms_dir=plat_dir,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertIn("bios/scph5501.bin", names)
self.assertIn("Custom", os.path.basename(zip_path))
def test_143_from_md5_not_in_repo(self):
"""--from-md5 reports files in DB but missing from repo."""
import contextlib
import io
import tempfile
from generate_pack import generate_md5_pack
db = {
"files": {
"sha1known": {
"name": "missing.bin",
"md5": "md5known" + "0" * 25,
"sha1": "sha1known",
"sha256": "sha256known",
"path": "/nonexistent/missing.bin",
"paths": ["Test/missing.bin"],
},
},
"indexes": {
"by_md5": {"md5known" + "0" * 25: "sha1known"},
"by_crc32": {},
},
}
with tempfile.TemporaryDirectory() as tmpdir:
out_dir = os.path.join(tmpdir, "dist")
bios_dir = os.path.join(tmpdir, "bios")
os.makedirs(bios_dir)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
result = generate_md5_pack(
hashes=[("md5", "md5known" + "0" * 25)],
db=db,
bios_dir=bios_dir,
output_dir=out_dir,
zip_contents={},
)
output = buf.getvalue()
self.assertIn("NOT IN REPO", output)
self.assertIsNone(result)
def test_144_invalid_split_emulator(self):
"""--split + --emulator is rejected."""
import subprocess
result = subprocess.run(
["python", "scripts/generate_pack.py", "--emulator", "test", "--split"],
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("error", result.stderr.lower())
def test_145_invalid_from_md5_all(self):
"""--from-md5 + --all is rejected."""
import subprocess
result = subprocess.run(
[
"python",
"scripts/generate_pack.py",
"--all",
"--from-md5",
"abc123" + "0" * 26,
],
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
def test_146_invalid_from_md5_system(self):
"""--from-md5 + --system is rejected."""
import subprocess
result = subprocess.run(
[
"python",
"scripts/generate_pack.py",
"--system",
"psx",
"--from-md5",
"abc123" + "0" * 26,
],
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
def test_147_invalid_group_by_without_split(self):
"""--group-by without --split is rejected."""
import subprocess
result = subprocess.run(
[
"python",
"scripts/generate_pack.py",
"--platform",
"retroarch",
"--group-by",
"manufacturer",
],
capture_output=True,
text=True,
)
self.assertNotEqual(result.returncode, 0)
def test_148_valid_platform_system(self):
"""--platform + --system is accepted (not rejected at validation stage)."""
import argparse
sys.path.insert(0, "scripts")
# Build the same parser as generate_pack.main()
parser = argparse.ArgumentParser()
parser.add_argument("--platform", "-p")
parser.add_argument("--all", action="store_true")
parser.add_argument("--emulator", "-e")
parser.add_argument("--system", "-s")
parser.add_argument("--standalone", action="store_true")
parser.add_argument("--split", action="store_true")
parser.add_argument(
"--group-by", choices=["system", "manufacturer"], default="system"
)
parser.add_argument("--target", "-t")
parser.add_argument("--from-md5")
parser.add_argument("--from-md5-file")
parser.add_argument("--required-only", action="store_true")
args = parser.parse_args(["--platform", "retroarch", "--system", "psx"])
# Replicate validation logic from main()
has_platform = bool(args.platform)
has_all = args.all
has_emulator = bool(args.emulator)
has_system = bool(args.system)
has_from_md5 = bool(args.from_md5 or args.from_md5_file)
# These should NOT raise
self.assertFalse(has_emulator and (has_platform or has_all or has_system))
self.assertFalse(has_platform and has_all)
self.assertTrue(
has_platform or has_all or has_emulator or has_system or has_from_md5
)
# --platform + --system is a valid combination
self.assertTrue(has_platform and has_system)
# ── BizHawk scraper tests ──────────────────────────────────────
def test_150_bizhawk_scraper_parse_firmware_and_option(self):
"""Parse FirmwareAndOption() one-liner pattern."""
from scraper.bizhawk_scraper import parse_firmware_database
fragment = """
FirmwareAndOption("DBEBD76A448447CB6E524AC3CB0FD19FC065D944", 256, "32X", "G", "32X_G_BIOS.BIN", "32x 68k BIOS");
FirmwareAndOption("1E5B0B2441A4979B6966D942B20CC76C413B8C5E", 2048, "32X", "M", "32X_M_BIOS.BIN", "32x SH2 MASTER BIOS");
"""
records, files = parse_firmware_database(fragment)
self.assertEqual(len(records), 2)
self.assertEqual(records[0]["system"], "32X")
self.assertEqual(records[0]["firmware_id"], "G")
self.assertEqual(records[0]["sha1"], "DBEBD76A448447CB6E524AC3CB0FD19FC065D944")
self.assertEqual(records[0]["name"], "32X_G_BIOS.BIN")
self.assertEqual(records[0]["size"], 256)
def test_151_bizhawk_scraper_parse_variable_refs(self):
"""Parse var = File() + Firmware() + Option() pattern."""
from scraper.bizhawk_scraper import parse_firmware_database
fragment = """
var gbaNormal = File("300C20DF6731A33952DED8C436F7F186D25D3492", 16384, "GBA_bios.rom", "Bios (World)");
Firmware("GBA", "Bios", "Bios");
Option("GBA", "Bios", in gbaNormal, FirmwareOptionStatus.Ideal);
"""
records, files = parse_firmware_database(fragment)
self.assertEqual(len(records), 1)
self.assertEqual(records[0]["system"], "GBA")
self.assertEqual(records[0]["sha1"], "300C20DF6731A33952DED8C436F7F186D25D3492")
self.assertEqual(records[0]["name"], "GBA_bios.rom")
self.assertEqual(records[0]["status"], "Ideal")
def test_152_bizhawk_scraper_skips_comments(self):
"""Commented-out blocks (PS2) are skipped."""
from scraper.bizhawk_scraper import parse_firmware_database
fragment = """
FirmwareAndOption("DBEBD76A448447CB6E524AC3CB0FD19FC065D944", 256, "32X", "G", "32X_G_BIOS.BIN", "32x 68k BIOS");
/*
Firmware("PS2", "BIOS", "PS2 Bios");
Option("PS2", "BIOS", File("FBD54BFC020AF34008B317DCB80B812DD29B3759", 4194304, "ps2.bin", "PS2 Bios"));
*/
"""
records, files = parse_firmware_database(fragment)
systems = {r["system"] for r in records}
self.assertNotIn("PS2", systems)
self.assertEqual(len(records), 1)
def test_153_bizhawk_scraper_arithmetic_size(self):
"""Size expressions like 4 * 1024 * 1024 are evaluated."""
from scraper.bizhawk_scraper import parse_firmware_database
fragment = """
FirmwareAndOption("BF861922DCB78C316360E3E742F4F70FF63C9BC3", 4 * 1024 * 1024, "N64DD", "IPL_JPN", "64DD_IPL.bin", "N64DD JPN IPL");
"""
records, _ = parse_firmware_database(fragment)
self.assertEqual(records[0]["size"], 4194304)
def test_154_bizhawk_scraper_dummy_hash(self):
"""SHA1Checksum.Dummy entries get no sha1 field."""
from scraper.bizhawk_scraper import parse_firmware_database
fragment = """
FirmwareAndOption(SHA1Checksum.Dummy, 0, "3DS", "aes_keys", "aes_keys.txt", "AES Keys");
"""
records, _ = parse_firmware_database(fragment)
self.assertEqual(len(records), 1)
self.assertIsNone(records[0]["sha1"])
def test_155_bizhawk_scraper_multi_option_picks_ideal(self):
"""When multiple options exist, Ideal is selected as canonical."""
from scraper.bizhawk_scraper import parse_firmware_database
fragment = """
var ss_100_j = File("2B8CB4F87580683EB4D760E4ED210813D667F0A2", 524288, "SAT_1.00-(J).bin", "Bios v1.00 (J)");
var ss_101_j = File("DF94C5B4D47EB3CC404D88B33A8FDA237EAF4720", 524288, "SAT_1.01-(J).bin", "Bios v1.01 (J)");
Firmware("SAT", "J", "Bios (J)");
Option("SAT", "J", in ss_100_j);
Option("SAT", "J", in ss_101_j, FirmwareOptionStatus.Ideal);
"""
records, _ = parse_firmware_database(fragment)
self.assertEqual(len(records), 1)
self.assertEqual(records[0]["sha1"], "DF94C5B4D47EB3CC404D88B33A8FDA237EAF4720")
self.assertEqual(records[0]["name"], "SAT_1.01-(J).bin")
def test_156_bizhawk_scraper_is_bad_excluded(self):
"""Files with isBad: true are not selected as canonical."""
from scraper.bizhawk_scraper import parse_firmware_database
fragment = """
var good = File("AAAA", 100, "good.bin", "Good");
var bad = File("BBBB", 100, "bad.bin", "Bad", isBad: true);
Firmware("TEST", "X", "Test");
Option("TEST", "X", in bad);
Option("TEST", "X", in good, FirmwareOptionStatus.Ideal);
"""
records, _ = parse_firmware_database(fragment)
self.assertEqual(records[0]["name"], "good.bin")
def test_157_path_conflict_helpers(self):
"""_has_path_conflict detects file/directory naming collisions."""
from generate_pack import _has_path_conflict, _register_path
seen_files: set[str] = set()
seen_parents: set[str] = set()
# Register system/SGB1.sfc as a file
_register_path("system/SGB1.sfc", seen_files, seen_parents)
# Adding system/SGB1.sfc/program.rom should conflict (parent is a file)
self.assertTrue(
_has_path_conflict("system/SGB1.sfc/program.rom", seen_files, seen_parents)
)
# Adding system/other.bin should not conflict
self.assertFalse(
_has_path_conflict("system/other.bin", seen_files, seen_parents)
)
# Reverse: register a nested path first, then check flat
seen_files2: set[str] = set()
seen_parents2: set[str] = set()
_register_path("system/SGB2.sfc/program.rom", seen_files2, seen_parents2)
# Adding system/SGB2.sfc as a file should conflict (it's a directory)
self.assertTrue(
_has_path_conflict("system/SGB2.sfc", seen_files2, seen_parents2)
)
# Adding system/SGB2.sfc/boot.rom should not conflict (sibling in same dir)
self.assertFalse(
_has_path_conflict("system/SGB2.sfc/boot.rom", seen_files2, seen_parents2)
)
def test_158_pack_skips_file_directory_conflict(self):
"""Pack generation skips entries that conflict with existing paths."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_conflict")
os.makedirs(output_dir, exist_ok=True)
# Platform declares SGB1.sfc as a flat file
config = {
"platform": "ConflictTest",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"test-sys": {
"files": [
{
"name": "present_req.bin",
"destination": "present_req.bin",
"required": True,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_conflict.yml"), "w") as fh:
yaml.dump(config, fh)
# Create an emulator profile with a nested path that conflicts
emu = {
"emulator": "ConflictCore",
"type": "libretro",
"systems": ["test-sys"],
"files": [
{"name": "present_req.bin/nested.rom", "required": False},
],
}
with open(os.path.join(self.emulators_dir, "conflict_core.yml"), "w") as fh:
yaml.dump(emu, fh)
zip_path = generate_pack(
"test_conflict",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
emulators_dir=self.emulators_dir,
offline=True,
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
# Flat file should be present
self.assertTrue(
any("present_req.bin" in n and "/" + "nested" not in n for n in names)
)
# Nested conflict should NOT be present
self.assertFalse(any("nested.rom" in n for n in names))
# ---------------------------------------------------------------
# Archive cross-reference and descriptive name tests
# ---------------------------------------------------------------
def test_159_cross_ref_archive_in_repo(self):
"""Archived files group by archive; in_repo=True when archive exists."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
# test_archive.zip should appear as a single grouped entry
archive_entries = [
u for u in undeclared if u.get("archive") == "test_archive.zip"
]
self.assertEqual(len(archive_entries), 1)
entry = archive_entries[0]
self.assertTrue(entry["in_repo"])
self.assertEqual(entry["name"], "test_archive.zip")
self.assertEqual(entry["archive_file_count"], 2)
self.assertTrue(entry["required"]) # at least one file is required
def test_160_cross_ref_archive_missing(self):
"""Missing archive reported as single entry with in_repo=False."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
missing_entries = [
u for u in undeclared if u.get("archive") == "missing_archive.zip"
]
self.assertEqual(len(missing_entries), 1)
entry = missing_entries[0]
self.assertFalse(entry["in_repo"])
self.assertEqual(entry["name"], "missing_archive.zip")
self.assertEqual(entry["archive_file_count"], 1)
def test_161_cross_ref_archive_not_individual_roms(self):
"""Individual ROM names from archived files should NOT appear as separate entries."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
# Individual ROMs should NOT be in the undeclared list
self.assertNotIn("rom_a.bin", names)
self.assertNotIn("rom_b.bin", names)
self.assertNotIn("missing_rom.bin", names)
def test_162_cross_ref_descriptive_name_resolved_by_path(self):
"""Descriptive name with path: fallback resolves via path basename."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
desc_entries = {
u["name"]: u for u in undeclared if u["emulator"] == "TestDescriptive"
}
# "Descriptive BIOS Name" has path: "present_req.bin" which IS in by_name
self.assertIn("Descriptive BIOS Name", desc_entries)
self.assertTrue(desc_entries["Descriptive BIOS Name"]["in_repo"])
# "Missing Descriptive" has path: "nonexistent_path.bin" which is NOT in by_name
self.assertIn("Missing Descriptive", desc_entries)
self.assertFalse(desc_entries["Missing Descriptive"]["in_repo"])
def test_163_cross_ref_archive_declared_by_platform_skipped(self):
"""Archive files whose archive is declared by platform are skipped."""
# Create a platform that declares test_archive.zip
config = {
"platform": "TestArchivePlatform",
"verification_mode": "existence",
"systems": {
"console-a": {
"files": [
{
"name": "test_archive.zip",
"destination": "test_archive.zip",
"required": True,
},
],
},
},
}
with open(
os.path.join(self.platforms_dir, "test_archive_platform.yml"), "w"
) as fh:
yaml.dump(config, fh)
config = load_platform_config("test_archive_platform", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
# test_archive.zip is declared ->its archived ROMs should be skipped
archive_entries = [
u for u in undeclared if u.get("archive") == "test_archive.zip"
]
self.assertEqual(len(archive_entries), 0)
def test_164_pack_extras_use_archive_name(self):
"""Pack extras for archived files use archive name, not individual ROM."""
from generate_pack import _collect_emulator_extras
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
extras = _collect_emulator_extras(
config,
self.emulators_dir,
self.db,
set(),
"",
profiles,
)
extra_names = {e["name"] for e in extras}
# Archive name should be present, not individual ROMs
self.assertIn("test_archive.zip", extra_names)
self.assertNotIn("rom_a.bin", extra_names)
self.assertNotIn("rom_b.bin", extra_names)
# Missing archive should NOT be in extras (in_repo=False)
self.assertNotIn("missing_archive.zip", extra_names)
def test_165_pack_extras_multi_dest_cross_ref(self):
"""Same file at different paths from two profiles produces both destinations."""
from generate_pack import _collect_emulator_extras
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
extras = _collect_emulator_extras(
config,
self.emulators_dir,
self.db,
set(),
"",
profiles,
)
extra_dests = {e["destination"] for e in extras}
# Root destination (from test_emu or test_root_core, no path)
self.assertIn("present_req.bin", extra_dests)
# Subdirectory destination (from test_subdir_core)
self.assertIn("subcore/bios/present_req.bin", extra_dests)
def test_166_pack_extras_multi_dest_platform_declared(self):
"""Profile with path different from platform destination adds alternative."""
from generate_pack import _collect_emulator_extras
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
# Simulate platform already having present_req.bin at root
seen = {"present_req.bin"}
extras = _collect_emulator_extras(
config,
self.emulators_dir,
self.db,
seen,
"",
profiles,
)
extra_dests = {e["destination"] for e in extras}
# Root is already in pack (in seen), should NOT be duplicated
self.assertNotIn("present_req.bin", extra_dests)
# Subdirectory destination should be added
self.assertIn("subcore/bios/present_req.bin", extra_dests)
def test_167_resolve_local_file_data_dir_fallback(self):
"""resolve_local_file finds files in data directories when not in bios/."""
data_dir = os.path.join(self.root, "data", "test-data")
os.makedirs(data_dir, exist_ok=True)
data_file = os.path.join(data_dir, "data_only.bin")
with open(data_file, "wb") as f:
f.write(b"DATA_DIR_CONTENT")
registry = {"test-data": {"local_cache": data_dir}}
fe = {"name": "data_only.bin"}
path, status = resolve_local_file(fe, self.db, data_dir_registry=registry)
self.assertIsNotNone(path)
self.assertEqual(os.path.basename(path), "data_only.bin")
self.assertEqual(status, "data_dir")
def test_167b_unsourceable_skips_data_dir_name_match(self):
"""The cache walk matches on filename, so unsourceable has to skip it."""
data_dir = os.path.join(self.root, "data", "other-core")
os.makedirs(data_dir, exist_ok=True)
with open(os.path.join(data_dir, "shared_name.dat"), "wb") as f:
f.write(b"ANOTHER CORE DEFINITIONS")
registry = {"other-core": {"local_cache": data_dir}}
fe = {"name": "shared_name.dat", "unsourceable": "ships inside the package"}
path, status = resolve_local_file(fe, self.db, data_dir_registry=registry)
self.assertIsNone(path)
self.assertEqual(status, "not_found")
# A declared hash is content proof, so the cache still answers for it.
import hashlib
digest = hashlib.sha1(b"ANOTHER CORE DEFINITIONS").hexdigest()
fe = {
"name": "shared_name.dat",
"sha1": digest,
"unsourceable": "ships inside the package",
}
path, status = resolve_local_file(fe, self.db, data_dir_registry=registry)
self.assertIsNotNone(path)
self.assertEqual(status, "data_dir_hash_exact")
def test_168_generate_truth_basic(self):
"""generate_platform_truth resolves cores and builds system truth."""
import yaml as _yaml
profile = {
"emulator": "TestCore",
"type": "libretro",
"systems": ["test-system"],
"cores": ["testcore"],
"files": [
{
"name": "bios.bin",
"system": "test-system",
"required": True,
"sha1": "aabbccdd" * 5,
"md5": "11223344" * 4,
"size": 1024,
"path": "TestConsole/bios.bin",
"source_ref": "main.cpp:42",
},
],
}
profile_path = os.path.join(self.emulators_dir, "testcore.yml")
with open(profile_path, "w") as f:
_yaml.dump(profile, f)
# Clear profile cache so fresh load picks up our file
from common import _emulator_profiles_cache
_emulator_profiles_cache.clear()
profiles = load_emulator_profiles(self.emulators_dir)
config = {"cores": ["testcore"]}
result = generate_platform_truth(
"testplat",
config,
{},
profiles,
db=None,
)
self.assertEqual(result["platform"], "testplat")
self.assertTrue(result["generated"])
self.assertIn("test-system", result["systems"])
sys_files = result["systems"]["test-system"]["files"]
self.assertEqual(len(sys_files), 1)
fe = sys_files[0]
self.assertEqual(fe["name"], "bios.bin")
self.assertTrue(fe["required"])
self.assertEqual(fe["sha1"], "aabbccdd" * 5)
self.assertIn("testcore", fe["_cores"])
self.assertIn("main.cpp:42", fe["_source_refs"])
def test_169_generate_truth_mode_filtering(self):
"""generate_platform_truth excludes standalone-only files for all_libretro."""
import yaml as _yaml
from common import _emulator_profiles_cache
profile = {
"emulator": "DualMode",
"type": "standalone + libretro",
"systems": ["test-system"],
"cores": ["dualmode"],
"files": [
{
"name": "both.bin",
"system": "test-system",
"required": True,
"mode": "both",
"sha1": "aaaa",
},
{
"name": "lr_only.bin",
"system": "test-system",
"required": True,
"mode": "libretro",
"sha1": "bbbb",
},
{
"name": "sa_only.bin",
"system": "test-system",
"required": True,
"mode": "standalone",
"sha1": "cccc",
},
{"name": "nomode.bin", "system": "test-system", "required": True, "sha1": "dddd"},
],
}
with open(os.path.join(self.emulators_dir, "dualmode.yml"), "w") as f:
_yaml.dump(profile, f)
_emulator_profiles_cache.clear()
profiles = load_emulator_profiles(self.emulators_dir)
config = {"cores": "all_libretro"}
result = generate_platform_truth("testplat", config, {}, profiles)
names = {fe["name"] for fe in result["systems"]["test-system"]["files"]}
self.assertIn("both.bin", names)
self.assertIn("lr_only.bin", names)
self.assertIn("nomode.bin", names)
self.assertNotIn("sa_only.bin", names)
def test_170_generate_truth_standalone_cores(self):
"""generate_platform_truth uses standalone mode for standalone_cores."""
import yaml as _yaml
from common import _emulator_profiles_cache
profile = {
"emulator": "DualCore",
"type": "standalone + libretro",
"systems": ["test-system"],
"cores": ["dualcore"],
"files": [
{
"name": "lr_file.bin",
"system": "test-system",
"required": True,
"mode": "libretro",
"sha1": "aaaa",
},
{
"name": "sa_file.bin",
"system": "test-system",
"required": True,
"mode": "standalone",
"sha1": "bbbb",
},
],
}
with open(os.path.join(self.emulators_dir, "dualcore.yml"), "w") as f:
_yaml.dump(profile, f)
_emulator_profiles_cache.clear()
profiles = load_emulator_profiles(self.emulators_dir)
config = {
"cores": ["dualcore"],
"standalone_cores": ["dualcore"],
}
result = generate_platform_truth("testplat", config, {}, profiles)
names = {fe["name"] for fe in result["systems"]["test-system"]["files"]}
self.assertIn("sa_file.bin", names)
self.assertNotIn("lr_file.bin", names)
def test_171_generate_truth_dedup_required_wins(self):
"""Dedup merges cores; required=True wins over required=False."""
import yaml as _yaml
from common import _emulator_profiles_cache
core_a = {
"emulator": "CoreA",
"type": "libretro",
"systems": ["test-system"],
"cores": ["core_a"],
"files": [
{
"name": "shared.bin",
"system": "test-system",
"required": False,
"source_ref": "a.cpp:10",
"sha1": "aaaa",
},
],
}
core_b = {
"emulator": "CoreB",
"type": "libretro",
"systems": ["test-system"],
"cores": ["core_b"],
"files": [
{
"name": "shared.bin",
"system": "test-system",
"required": True,
"source_ref": "b.cpp:20",
"sha1": "aaaa",
},
],
}
for name, data in [("core_a", core_a), ("core_b", core_b)]:
with open(os.path.join(self.emulators_dir, f"{name}.yml"), "w") as f:
_yaml.dump(data, f)
_emulator_profiles_cache.clear()
profiles = load_emulator_profiles(self.emulators_dir)
config = {"cores": ["core_a", "core_b"]}
result = generate_platform_truth("testplat", config, {}, profiles)
sys_files = result["systems"]["test-system"]["files"]
self.assertEqual(len(sys_files), 1)
fe = sys_files[0]
self.assertEqual(fe["name"], "shared.bin")
self.assertTrue(fe["required"])
self.assertIn("core_a", fe["_cores"])
self.assertIn("core_b", fe["_cores"])
self.assertIn("a.cpp:10", fe["_source_refs"])
self.assertIn("b.cpp:20", fe["_source_refs"])
def test_172_generate_truth_coverage_metadata(self):
"""Coverage tracks profiled vs unprofiled cores."""
import yaml as _yaml
from common import _emulator_profiles_cache
profile = {
"emulator": "ProfiledCore",
"type": "libretro",
"systems": ["test-system"],
"cores": ["profiled_core"],
"files": [
{"name": "fw.bin", "system": "test-system", "required": True},
],
}
with open(os.path.join(self.emulators_dir, "profiled_core.yml"), "w") as f:
_yaml.dump(profile, f)
_emulator_profiles_cache.clear()
profiles = load_emulator_profiles(self.emulators_dir)
config = {"cores": ["profiled_core", "unprofiled_core"]}
result = generate_platform_truth("testplat", config, {}, profiles)
cov = result["_coverage"]
self.assertEqual(cov["cores_profiled"], 1)
self.assertNotIn(
"unprofiled_core", [name for name in profiles if name == "unprofiled_core"]
)
# unprofiled_core has no profile YAML so resolve_platform_cores
# won't include it; cores_resolved reflects only matched profiles
self.assertEqual(cov["cores_resolved"], 1)
self.assertNotIn("unprofiled_core", cov["cores_unprofiled"])
def test_90_registry_install_metadata(self):
"""Registry install section is accessible."""
import yaml
with open("platforms/_registry.yml") as f:
registry = yaml.safe_load(f)
for name in (
"retroarch",
"batocera",
"emudeck",
"recalbox",
"retrobat",
"retrodeck",
"lakka",
"romm",
"bizhawk",
):
plat = registry["platforms"][name]
self.assertIn("install", plat, f"{name} missing install section")
self.assertIn("detect", plat["install"])
self.assertIsInstance(plat["install"]["detect"], list)
for hint in plat["install"]["detect"]:
self.assertIn("os", hint)
# EmuDeck has standalone_copies
self.assertIn(
"standalone_copies",
registry["platforms"]["emudeck"]["install"],
)
def test_91_generate_manifest(self):
"""generate_manifest returns valid manifest dict with expected fields."""
from generate_pack import generate_manifest
# Create a minimal registry file for the test
registry_path = os.path.join(self.platforms_dir, "_test_registry.yml")
registry_data = {
"platforms": {
"test_existence": {
"install": {
"detect": [
{
"os": "linux",
"method": "path_exists",
"path": "/test/bios",
}
],
},
},
},
}
with open(registry_path, "w") as fh:
yaml.dump(registry_data, fh)
manifest = generate_manifest(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
registry_path,
emulators_dir=self.emulators_dir,
offline=True,
)
self.assertEqual(manifest["manifest_version"], 2)
self.assertEqual(manifest["regions"], [])
self.assertEqual(manifest["platform"], "test_existence")
self.assertEqual(manifest["display_name"], "TestExistence")
self.assertIn("generated", manifest)
self.assertIn("files", manifest)
self.assertIsInstance(manifest["files"], list)
self.assertEqual(manifest["total_files"], len(manifest["files"]))
self.assertEqual(manifest["total_omitted"], len(manifest["omitted_files"]))
self.assertGreater(len(manifest["files"]), 0)
self.assertEqual(manifest["base_destination"], "system")
self.assertEqual(
manifest["detect"],
registry_data["platforms"]["test_existence"]["install"]["detect"],
)
for f in manifest["files"]:
self.assertIn("dest", f)
self.assertIn("sha1", f)
self.assertIn("size", f)
self.assertIn("repo_path", f)
self.assertIn("cores", f)
self.assertIsInstance(f["size"], int)
self.assertGreater(len(f["sha1"]), 0)
def test_91b_manifest_hash_points_at_a_fetchable_file(self):
"""A declared hash no local file carries must not blank repo_path.
install.py builds its download URL from repo_path, so an upstream
hash that resolves to nothing leaves the file unfetchable.
"""
from generate_pack import generate_manifest
registry_path = os.path.join(self.platforms_dir, "_test_registry2.yml")
with open(registry_path, "w") as fh:
yaml.dump({"platforms": {"test_existence": {"install": {}}}}, fh)
config_path = os.path.join(self.platforms_dir, "test_existence.yml")
with open(config_path) as fh:
config = yaml.safe_load(fh)
for system in config.get("systems", {}).values():
for entry in system.get("files", []):
entry["sha1"] = "0" * 40 # upstream hash, absent from the repo
with open(config_path, "w") as fh:
yaml.dump(config, fh)
from common import _platform_config_cache
_platform_config_cache.clear()
manifest = generate_manifest(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
registry_path,
emulators_dir=self.emulators_dir,
offline=True,
)
self.assertGreater(len(manifest["files"]), 0)
self.assertGreater(len(manifest["omitted_files"]), 0)
self.assertTrue(
all(
entry["reason"] in {"hash_mismatch", "not_found"}
for entry in manifest["omitted_files"]
if entry["cores"] is None
)
)
for f in manifest["files"]:
if f.get("release_asset"):
continue
self.assertTrue(
f["repo_path"],
f"{f['dest']} has no repo_path, install.py cannot fetch it",
)
self.assertNotEqual(f["sha1"], "0" * 40)
def test_92_manifest_matches_zip(self):
"""Manifest file destinations match ZIP contents (excluding metadata)."""
from generate_pack import generate_manifest, generate_pack
registry_path = os.path.join(self.platforms_dir, "_test_registry.yml")
registry_data = {
"platforms": {
"test_existence": {
"install": {"detect": []},
},
},
}
with open(registry_path, "w") as fh:
yaml.dump(registry_data, fh)
# Generate ZIP
output_dir = os.path.join(self.root, "pack_manifest_cmp")
os.makedirs(output_dir, exist_ok=True)
zip_path = generate_pack(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
emulators_dir=self.emulators_dir,
offline=True,
)
self.assertIsNotNone(zip_path)
# Get ZIP file destinations (exclude metadata)
with zipfile.ZipFile(zip_path) as zf:
zip_names = {
n
for n in zf.namelist()
if not n.startswith("INSTRUCTIONS_")
and n != "manifest.json"
and n != "README.txt"
}
# Generate manifest
manifest = generate_manifest(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
registry_path,
emulators_dir=self.emulators_dir,
offline=True,
)
# Detect flat vs nested ZIP to build expected paths
base = manifest.get("base_destination", "")
is_flat = bool(base) and not any(
n.startswith(base + "/") for n in zip_names
)
manifest_dests = set()
for f in manifest["files"]:
if base and not is_flat:
d = f"{base}/{f['dest']}"
else:
d = f["dest"]
manifest_dests.add(d)
self.assertEqual(manifest_dests, zip_names)
# ---------------------------------------------------------------
# install.py tests
# ---------------------------------------------------------------
def test_93_parse_retroarch_cfg(self):
"""Parse system_directory from retroarch.cfg."""
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from install import _parse_retroarch_system_dir
cfg = os.path.join(self.root, "retroarch.cfg")
# Quoted absolute path
with open(cfg, "w") as f:
f.write('system_directory = "/home/user/ra/system"\n')
result = _parse_retroarch_system_dir(Path(cfg))
self.assertEqual(result, Path("/home/user/ra/system"))
# Default value
with open(cfg, "w") as f:
f.write('system_directory = "default"\n')
result = _parse_retroarch_system_dir(Path(cfg))
self.assertEqual(result, Path(self.root) / "system")
# Unquoted
with open(cfg, "w") as f:
f.write("system_directory = /tmp/ra_system\n")
result = _parse_retroarch_system_dir(Path(cfg))
self.assertEqual(result, Path("/tmp/ra_system"))
def test_94_parse_emudeck_settings(self):
"""Parse emulationPath from EmuDeck settings.sh."""
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from install import _parse_bash_var
settings = os.path.join(self.root, "settings.sh")
with open(settings, "w") as f:
f.write(
'emulationPath="/home/deck/Emulation"\nromsPath="/home/deck/Emulation/roms"\n'
)
result = _parse_bash_var(Path(settings), "emulationPath")
self.assertEqual(result, "/home/deck/Emulation")
def test_95_parse_ps1_var(self):
"""Parse $emulationPath from EmuDeck settings.ps1."""
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from install import _parse_ps1_var
settings = os.path.join(self.root, "settings.ps1")
with open(settings, "w") as f:
f.write('$emulationPath="C:\\Emulation"\n')
result = _parse_ps1_var(Path(settings), "$emulationPath")
self.assertEqual(result, "C:\\Emulation")
def test_96_target_filtering(self):
"""--target filters files by cores field."""
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from install import _filter_by_target
files = [
{"dest": "a.bin", "cores": None},
{"dest": "b.bin", "cores": ["flycast", "redream"]},
{"dest": "c.bin", "cores": ["dolphin"]},
]
filtered = _filter_by_target(files, ["flycast", "snes9x"])
dests = [f["dest"] for f in filtered]
self.assertIn("a.bin", dests)
self.assertIn("b.bin", dests)
self.assertNotIn("c.bin", dests)
def test_97_standalone_copies(self):
"""Standalone keys copied to existing emulator dirs."""
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from install import do_standalone_copies
bios_dir = Path(self.root) / "bios"
bios_dir.mkdir(exist_ok=True)
(bios_dir / "prod.keys").write_bytes(b"KEYS")
yuzu_dir = Path(self.root) / "yuzu_keys"
yuzu_dir.mkdir()
missing_dir = Path(self.root) / "nonexistent"
manifest = {
"base_destination": "bios",
"standalone_copies": [
{
"file": "prod.keys",
"targets": {"linux": [str(yuzu_dir), str(missing_dir)]},
}
],
}
copied, skipped = do_standalone_copies(manifest, bios_dir, "linux")
self.assertEqual(copied, 1)
self.assertEqual(skipped, 1)
self.assertTrue((yuzu_dir / "prod.keys").exists())
self.assertFalse((missing_dir / "prod.keys").exists())
# ---------------------------------------------------------------
# diff_platform_truth tests
# ---------------------------------------------------------------
def test_98_diff_truth_missing(self):
"""Truth has 2 files, scraped has 1 -> 1 missing with cores/source_refs."""
truth = {
"systems": {
"test-sys": {
"_coverage": {"cores_profiled": ["core_a"], "cores_unprofiled": []},
"files": [
{
"name": "bios_a.bin",
"required": True,
"md5": "aaa",
"_cores": ["core_a"],
"_source_refs": ["src/a.c:10"],
},
{
"name": "bios_b.bin",
"required": False,
"md5": "bbb",
"_cores": ["core_a"],
"_source_refs": ["src/b.c:20"],
},
],
}
}
}
scraped = {
"systems": {
"test-sys": {
"files": [{"name": "bios_a.bin", "md5": "aaa"}],
}
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["total_missing"], 1)
div = result["divergences"]["test-sys"]
self.assertEqual(len(div["missing"]), 1)
m = div["missing"][0]
self.assertEqual(m["name"], "bios_b.bin")
self.assertEqual(m["cores"], ["core_a"])
self.assertEqual(m["source_refs"], ["src/b.c:20"])
def test_99_diff_truth_extra_phantom(self):
"""All cores profiled, scraped has extra file -> extra_phantom."""
truth = {
"systems": {
"test-sys": {
"_coverage": {"cores_profiled": ["core_a"], "cores_unprofiled": []},
"files": [
{
"name": "bios.bin",
"md5": "aaa",
"_cores": ["core_a"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"test-sys": {
"files": [
{"name": "bios.bin", "md5": "aaa"},
{"name": "phantom.bin", "md5": "zzz"},
],
}
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["total_extra_phantom"], 1)
div = result["divergences"]["test-sys"]
self.assertEqual(len(div["extra_phantom"]), 1)
self.assertEqual(div["extra_phantom"][0]["name"], "phantom.bin")
def test_100_diff_truth_extra_unprofiled(self):
"""Some cores unprofiled, scraped has extra -> extra_unprofiled."""
truth = {
"systems": {
"test-sys": {
"_coverage": {
"cores_profiled": ["core_a"],
"cores_unprofiled": ["core_b"],
},
"files": [
{
"name": "bios.bin",
"md5": "aaa",
"_cores": ["core_a"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"test-sys": {
"files": [
{"name": "bios.bin", "md5": "aaa"},
{"name": "extra.bin", "md5": "yyy"},
],
}
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["total_extra_unprofiled"], 1)
div = result["divergences"]["test-sys"]
self.assertEqual(len(div["extra_unprofiled"]), 1)
self.assertEqual(div["extra_unprofiled"][0]["name"], "extra.bin")
def test_101_diff_truth_alias_matching(self):
"""Truth file with aliases, scraped uses alias -> not extra or missing."""
truth = {
"systems": {
"test-sys": {
"_coverage": {"cores_profiled": ["core_a"], "cores_unprofiled": []},
"files": [
{
"name": "bios.bin",
"md5": "aaa",
"aliases": ["alt.bin"],
"_cores": ["core_a"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"test-sys": {
"files": [{"name": "alt.bin", "md5": "aaa"}],
}
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["total_missing"], 0)
self.assertEqual(result["summary"]["total_extra_phantom"], 0)
self.assertNotIn("test-sys", result.get("divergences", {}))
def test_102_diff_truth_case_insensitive(self):
"""Truth 'BIOS.ROM', scraped 'bios.rom' -> match, no missing."""
truth = {
"systems": {
"test-sys": {
"_coverage": {"cores_profiled": ["core_a"], "cores_unprofiled": []},
"files": [
{
"name": "BIOS.ROM",
"md5": "aaa",
"_cores": ["core_a"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"test-sys": {
"files": [{"name": "bios.rom", "md5": "aaa"}],
}
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["total_missing"], 0)
self.assertNotIn("test-sys", result.get("divergences", {}))
def test_103_diff_truth_hash_mismatch(self):
"""Same file, different md5 -> hash_mismatch with truth_cores."""
truth = {
"systems": {
"test-sys": {
"_coverage": {"cores_profiled": ["core_a"], "cores_unprofiled": []},
"files": [
{
"name": "bios.bin",
"md5": "truth_hash",
"_cores": ["core_a", "core_b"],
"_source_refs": ["src/x.c:5"],
},
],
}
}
}
scraped = {
"systems": {
"test-sys": {
"files": [{"name": "bios.bin", "md5": "scraped_hash"}],
}
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["total_hash_mismatch"], 1)
div = result["divergences"]["test-sys"]
self.assertEqual(len(div["hash_mismatch"]), 1)
hm = div["hash_mismatch"][0]
self.assertEqual(hm["name"], "bios.bin")
self.assertEqual(hm["truth_cores"], ["core_a", "core_b"])
self.assertEqual(hm["truth_md5"], "truth_hash")
self.assertEqual(hm["scraped_md5"], "scraped_hash")
def test_104_diff_truth_normalized_system_ids(self):
"""Diff matches systems with different ID formats via normalization."""
from truth import diff_platform_truth
truth = {
"systems": {
"sega-gamegear": {
"_coverage": {"cores_profiled": ["c"], "cores_unprofiled": []},
"files": [
{
"name": "bios.gg",
"required": True,
"md5": "a" * 32,
"_cores": ["c"],
"_source_refs": [],
},
],
},
}
}
scraped = {
"systems": {
"sega-game-gear": {
"files": [
{"name": "bios.gg", "required": True, "md5": "a" * 32},
],
},
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["systems_uncovered"], 0)
self.assertEqual(result["summary"]["total_missing"], 0)
self.assertEqual(result["summary"]["systems_compared"], 1)
# ---------------------------------------------------------------
# native_id preservation
# ---------------------------------------------------------------
def test_native_id_preserved_in_platform_config(self):
"""load_platform_config preserves native_id at the system level."""
config = {
"platform": "TestNativeId",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
"sony-playstation": {
"native_id": "Sony - PlayStation",
"files": [
{
"name": "scph5501.bin",
"destination": "scph5501.bin",
"required": True,
},
],
},
"nintendo-snes": {
"native_id": "snes",
"files": [
{
"name": "bs-x.bin",
"destination": "bs-x.bin",
"required": False,
},
],
},
},
}
with open(os.path.join(self.platforms_dir, "test_native_id.yml"), "w") as fh:
yaml.dump(config, fh)
loaded = load_platform_config("test_native_id", self.platforms_dir)
psx = loaded["systems"]["sony-playstation"]
self.assertEqual(psx["native_id"], "Sony - PlayStation")
snes = loaded["systems"]["nintendo-snes"]
self.assertEqual(snes["native_id"], "snes")
# ---------------------------------------------------------------
# Exporter: System.dat round-trip
# ---------------------------------------------------------------
def test_systemdat_exporter_round_trip(self):
"""Export truth data to System.dat and validate round-trip."""
from exporter import discover_exporters
from exporter.systemdat_exporter import Exporter as SystemDatExporter
truth = {
"platform": "retroarch",
"systems": {
"sony-playstation": {
"files": [
{
"name": "scph5501.bin",
"path": "scph5501.bin",
"size": 524288,
"md5": "490f666e1afb15ed6c63b88fc7571f2f",
"sha1": "b056ee5a4d65937e1a3a17e1e78f3258ea49c38e",
"crc32": "71af80b4",
"required": True,
"_cores": ["beetle_psx"],
"_source_refs": ["libretro.c:50"],
},
],
},
},
}
scraped = {
"systems": {
"sony-playstation": {
"native_id": "Sony - PlayStation",
"files": [
{"name": "scph5501.bin", "destination": "scph5501.bin"},
],
},
},
}
out_path = os.path.join(self.root, "System.dat")
exporter = SystemDatExporter()
exporter.export(truth, out_path, scraped_data=scraped)
with open(out_path) as fh:
content = fh.read()
self.assertIn("Sony - PlayStation", content)
self.assertIn("scph5501.bin", content)
self.assertIn("b056ee5a4d65937e1a3a17e1e78f3258ea49c38e", content)
self.assertIn('name "System"', content)
self.assertIn("71AF80B4", content) # CRC uppercase
issues = exporter.validate(truth, out_path)
self.assertEqual(issues, [])
# Discovery finds the systemdat exporter
exporters = discover_exporters()
self.assertIn("retroarch", exporters)
self.assertIs(exporters["retroarch"], SystemDatExporter)
def test_systemdat_exporter_numeric_crc(self):
"""Digit-only crc32 typed as int by YAML must not break the export."""
from exporter.systemdat_exporter import Exporter as SystemDatExporter
truth = {
"platform": "retroarch",
"systems": {
"commodore-plus4": {
"files": [
{
"name": "kernal-318005-05.bin",
"path": "kernal-318005-05.bin",
"size": 16384,
"crc32": 70295038,
"required": False,
},
],
},
},
}
out_path = os.path.join(self.root, "System_numeric.dat")
SystemDatExporter().export(truth, out_path, scraped_data={"systems": {}})
with open(out_path) as fh:
content = fh.read()
self.assertIn("crc 70295038", content)
# ---------------------------------------------------------------
# Full truth + diff integration test
# ---------------------------------------------------------------
def test_truth_diff_integration(self):
"""Full chain: generate truth from profiles, diff against scraped data."""
# Config: platform with two cores, only core_a has a profile
config = {"cores": ["core_a", "core_b"]}
registry_entry = {
"hash_type": "md5",
"verification_mode": "md5",
}
# Emulator profile for core_a with 2 files
core_a_profile = {
"emulator": "CoreA",
"type": "libretro",
"systems": ["test-system"],
"files": [
{
"name": "bios_a.bin",
"system": "test-system",
"required": True,
"md5": "a" * 32,
"sha1": "b" * 40,
"size": 1024,
"path": "bios_a.bin",
"source_ref": "src/a.c:10",
},
{
"name": "shared.bin",
"system": "test-system",
"required": True,
"md5": "c" * 32,
"path": "shared.bin",
"source_ref": "src/a.c:20",
},
],
}
profile_path = os.path.join(self.emulators_dir, "core_a.yml")
with open(profile_path, "w") as f:
yaml.dump(core_a_profile, f)
# No profile for core_b (unprofiled)
# Clear cache so the new profile is picked up
from common import _emulator_profiles_cache
_emulator_profiles_cache.clear()
profiles = load_emulator_profiles(self.emulators_dir)
self.assertIn("core_a", profiles)
self.assertNotIn("core_b", profiles)
# Generate truth
truth = generate_platform_truth(
"testplat", config, registry_entry, profiles, db=None
)
# Verify truth structure
self.assertIn("test-system", truth["systems"])
sys_files = truth["systems"]["test-system"]["files"]
self.assertEqual(len(sys_files), 2)
file_names = {f["name"] for f in sys_files}
self.assertEqual(file_names, {"bios_a.bin", "shared.bin"})
# Verify coverage metadata
cov = truth["_coverage"]
self.assertEqual(cov["cores_profiled"], 1)
# core_b has no profile YAML, so resolve_platform_cores never
# includes it; cores_resolved reflects only matched profiles
self.assertEqual(cov["cores_resolved"], 1)
# Inject unprofiled info into system-level coverage for diff.
# In production, core_b would be tracked as unprofiled by a
# higher-level orchestrator that knows the declared core list.
injected_cov = {
"cores_profiled": ["core_a"],
"cores_unprofiled": ["core_b"],
}
for sys_data in truth["systems"].values():
sys_data["_coverage"] = injected_cov
# Build scraped dict: shared.bin with wrong hash, phantom.bin extra,
# bios_a.bin missing
scraped = {
"systems": {
"test-system": {
"files": [
{
"name": "shared.bin",
"required": True,
"md5": "d" * 32,
},
{
"name": "phantom.bin",
"required": False,
"md5": "e" * 32,
},
],
},
},
}
# Diff
result = diff_platform_truth(truth, scraped)
summary = result["summary"]
# bios_a.bin not in scraped -> missing
self.assertEqual(summary["total_missing"], 1)
# shared.bin md5 mismatch (truth "c"*32 vs scraped "d"*32)
self.assertEqual(summary["total_hash_mismatch"], 1)
# phantom.bin extra, core_b unprofiled -> extra_unprofiled
self.assertEqual(summary["total_extra_unprofiled"], 1)
# Unprofiled cores present, so extras are unprofiled not phantom
self.assertEqual(summary["total_extra_phantom"], 0)
# Verify divergence details
div = result["divergences"]["test-system"]
self.assertEqual(div["missing"][0]["name"], "bios_a.bin")
self.assertEqual(div["hash_mismatch"][0]["name"], "shared.bin")
self.assertEqual(div["extra_unprofiled"][0]["name"], "phantom.bin")
self.assertNotIn("extra_phantom", div)
def test_173_cross_ref_hash_matching(self):
"""Platform file under different name matched by MD5 is not undeclared."""
config = load_platform_config("test_md5", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
# correct_hash.bin is declared by platform as renamed_file.bin with same MD5
# hash-based matching should suppress it from undeclared
self.assertNotIn("correct_hash.bin", names)
def test_174_expand_platform_declared_names(self):
"""expand_platform_declared_names enriches with DB canonical names."""
config = load_platform_config("test_md5", self.platforms_dir)
result = expand_platform_declared_names(config, self.db)
# renamed_file.bin is declared directly
self.assertIn("renamed_file.bin", result)
# correct_hash.bin is the DB canonical name for the same MD5
self.assertIn("correct_hash.bin", result)
# ---------------------------------------------------------------
# Registry merge + all_libretro expansion + diff hash fallback
# ---------------------------------------------------------------
def test_175_registry_merge_cores(self):
"""load_platform_config merges cores from _registry.yml."""
from common import _platform_config_cache
# Platform YAML with 1 core
config = {
"platform": "TestMerge",
"cores": ["core_a"],
"systems": {"test-system": {"files": []}},
}
with open(os.path.join(self.platforms_dir, "testmerge.yml"), "w") as f:
yaml.dump(config, f)
# Registry with 2 cores (superset)
registry = {
"platforms": {
"testmerge": {
"config": "testmerge.yml",
"status": "active",
"cores": ["core_a", "core_b"],
}
}
}
with open(os.path.join(self.platforms_dir, "_registry.yml"), "w") as f:
yaml.dump(registry, f)
_platform_config_cache.clear()
loaded = load_platform_config("testmerge", self.platforms_dir)
cores = [str(c) for c in loaded["cores"]]
self.assertIn("core_a", cores)
self.assertIn("core_b", cores)
def test_176_all_libretro_in_list(self):
"""resolve_platform_cores expands all_libretro/retroarch in a list."""
from common import load_emulator_profiles, resolve_platform_cores
# Create a libretro profile and a standalone profile
for name, ptype in [("lr_core", "libretro"), ("sa_core", "standalone")]:
profile = {
"emulator": name,
"type": ptype,
"cores": [name],
"systems": ["test-system"],
"files": [],
}
with open(os.path.join(self.emulators_dir, f"{name}.yml"), "w") as f:
yaml.dump(profile, f)
profiles = load_emulator_profiles(self.emulators_dir)
# Config with retroarch + sa_core in cores list
config = {"cores": ["retroarch", "sa_core"]}
resolved = resolve_platform_cores(config, profiles)
self.assertIn("lr_core", resolved) # expanded via retroarch
self.assertIn("sa_core", resolved) # explicit
def test_177_diff_hash_fallback_rename(self):
"""Diff detects platform renames via hash fallback."""
from truth import diff_platform_truth
truth = {
"systems": {
"test-system": {
"_coverage": {"cores_profiled": ["c"], "cores_unprofiled": []},
"files": [
{
"name": "ROM",
"required": True,
"md5": "abcd1234" * 4,
"_cores": ["c"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"test-system": {
"files": [
{"name": "ROM1", "required": True, "md5": "abcd1234" * 4},
],
}
}
}
result = diff_platform_truth(truth, scraped)
# ROM and ROM1 share the same hash — rename, not missing+phantom
self.assertEqual(result["summary"]["total_missing"], 0)
self.assertEqual(result["summary"]["total_extra_phantom"], 0)
def test_178_diff_system_normalization(self):
"""Diff matches systems with different IDs via normalization."""
from truth import diff_platform_truth
truth = {
"systems": {
"sega-gamegear": {
"_coverage": {"cores_profiled": ["c"], "cores_unprofiled": []},
"files": [
{
"name": "bios.gg",
"required": True,
"md5": "a" * 32,
"_cores": ["c"],
"_source_refs": [],
},
],
},
}
}
scraped = {
"systems": {
"sega-game-gear": {
"files": [
{"name": "bios.gg", "required": True, "md5": "a" * 32},
],
},
}
}
result = diff_platform_truth(truth, scraped)
self.assertEqual(result["summary"]["systems_uncovered"], 0)
self.assertEqual(result["summary"]["total_missing"], 0)
self.assertEqual(result["summary"]["systems_compared"], 1)
def test_179_agnostic_profile_skipped_in_undeclared(self):
"""bios_mode: agnostic profiles are skipped entirely by find_undeclared_files."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
emulators = {u["emulator"] for u in undeclared}
# TestAgnostic should NOT appear in undeclared (bios_mode: agnostic)
self.assertNotIn("TestAgnostic", emulators)
def test_180_agnostic_file_skipped_in_undeclared(self):
"""Files with agnostic: true are skipped, others in same profile are not."""
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
names = {u["name"] for u in undeclared}
# agnostic_file.bin should NOT be in undeclared (agnostic: true)
self.assertNotIn("agnostic_file.bin", names)
# undeclared_req.bin should still be in undeclared (not agnostic)
self.assertIn("undeclared_req.bin", names)
def test_181_agnostic_extras_scan(self):
"""Agnostic profiles add all matching DB files as extras."""
from generate_pack import _collect_emulator_extras
config = load_platform_config("test_existence", self.platforms_dir)
profiles = load_emulator_profiles(self.emulators_dir)
extras = _collect_emulator_extras(
config,
self.emulators_dir,
self.db,
set(),
"system",
profiles,
)
agnostic_extras = [
e for e in extras if e.get("source_emulator") == "TestAgnostic"
]
# Agnostic scan should find files in the same directory as correct_hash.bin
self.assertTrue(len(agnostic_extras) > 0, "Agnostic scan should produce extras")
# All agnostic extras should have agnostic_scan flag
for e in agnostic_extras:
self.assertTrue(e.get("agnostic_scan", False))
def test_182_agnostic_rename_readme(self):
"""_build_agnostic_rename_readme generates correct text."""
from generate_pack import _build_agnostic_rename_readme
result = _build_agnostic_rename_readme(
"dsi_nand.bin",
"DSi_Nand_AUS.bin",
["DSi_Nand_EUR.bin", "DSi_Nand_USA.bin"],
)
self.assertIn("dsi_nand.bin <- DSi_Nand_AUS.bin", result)
self.assertIn("DSi_Nand_EUR.bin", result)
self.assertIn("DSi_Nand_USA.bin", result)
self.assertIn("rename it to: dsi_nand.bin", result)
def test_183_agnostic_resolve_fallback(self):
"""resolve_local_file with agnostic fallback finds a system file."""
file_entry = {
"name": "nonexistent_agnostic.bin",
"agnostic": True,
"min_size": 1,
"max_size": 999999,
"agnostic_path_prefix": self.bios_dir + "/",
}
path, status = resolve_local_file(file_entry, self.db)
self.assertIsNotNone(path)
self.assertEqual(status, "agnostic_fallback")
def test_179_batocera_exporter_round_trip(self):
"""Batocera exporter produces valid Python dict format."""
from exporter.batocera_exporter import Exporter
truth = {
"systems": {
"sony-playstation": {
"_coverage": {"cores_profiled": ["c"]},
"files": [
{
"name": "scph5501.bin",
"destination": "scph5501.bin",
"required": True,
"md5": "b" * 32,
"_cores": ["c"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"sony-playstation": {"native_id": "psx", "files": []},
}
}
out = os.path.join(self.root, "batocera-systems")
exp = Exporter()
exp.export(truth, out, scraped_data=scraped)
content = Path(out).read_text(encoding="utf-8")
self.assertIn('"psx"', content)
self.assertIn("scph5501.bin", content)
self.assertIn("b" * 32, content)
self.assertEqual(exp.validate(truth, out), [])
def test_180_recalbox_exporter_round_trip(self):
"""Recalbox exporter produces valid es_bios.xml."""
from exporter.recalbox_exporter import Exporter
truth = {
"systems": {
"sony-playstation": {
"_coverage": {"cores_profiled": ["c"]},
"files": [
{
"name": "scph5501.bin",
"destination": "scph5501.bin",
"required": True,
"md5": "b" * 32,
"_cores": ["c"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"sony-playstation": {"native_id": "psx", "files": []},
}
}
out = os.path.join(self.root, "es_bios.xml")
exp = Exporter()
exp.export(truth, out, scraped_data=scraped)
content = Path(out).read_text(encoding="utf-8")
self.assertIn("<biosList", content)
self.assertIn('platform="psx"', content)
self.assertIn("fullname=", content)
self.assertIn("scph5501.bin", content)
# mandatory="true" is the default, not emitted (matching Recalbox format)
self.assertNotIn('mandatory="false"', content)
self.assertIn('core="libretro/c"', content)
self.assertEqual(exp.validate(truth, out), [])
def test_181_retrobat_exporter_round_trip(self):
"""RetroBat exporter produces valid JSON."""
import json as _json
from exporter.retrobat_exporter import Exporter
truth = {
"systems": {
"sony-playstation": {
"_coverage": {"cores_profiled": ["c"]},
"files": [
{
"name": "scph5501.bin",
"destination": "scph5501.bin",
"required": True,
"md5": "b" * 32,
"_cores": ["c"],
"_source_refs": [],
},
],
}
}
}
scraped = {
"systems": {
"sony-playstation": {"native_id": "psx", "files": []},
}
}
out = os.path.join(self.root, "batocera-systems.json")
exp = Exporter()
exp.export(truth, out, scraped_data=scraped)
data = _json.loads(Path(out).read_text(encoding="utf-8"))
self.assertIn("psx", data)
self.assertTrue(
any("scph5501" in bf["file"] for bf in data["psx"]["biosFiles"])
)
self.assertEqual(exp.validate(truth, out), [])
def test_182_exporter_discovery(self):
"""All exporters are discovered by the plugin system."""
from exporter import discover_exporters
exporters = discover_exporters()
self.assertIn("retroarch", exporters)
self.assertIn("batocera", exporters)
self.assertIn("recalbox", exporters)
self.assertIn("retrobat", exporters)
# ---------------------------------------------------------------
# Hash scraper: parsers + merge
# ---------------------------------------------------------------
def test_mame_parser_finds_bios_root_sets(self):
from scripts.scraper.mame_parser import find_bios_root_sets, parse_rom_block
source = """
ROM_START( neogeo )
ROM_REGION( 0x020000, "mainbios", 0 )
ROM_LOAD( "sp-s2.sp1", 0x00000, 0x020000, CRC(9036d879) SHA1(4f834c580f3471ce40c3210ef5e7491df38d8851) )
ROM_END
GAME( 1990, neogeo, 0, ng, neogeo, ng_state, empty_init, ROT0, "SNK", "Neo Geo", MACHINE_IS_BIOS_ROOT )
ROM_START( pacman )
ROM_REGION( 0x10000, "maincpu", 0 )
ROM_LOAD( "pacman.6e", 0x0000, 0x1000, CRC(c1e6ab10) SHA1(e87e059c5be45753f7e9f33dff851f16d6751181) )
ROM_END
GAME( 1980, pacman, 0, pacman, pacman, pacman_state, empty_init, ROT90, "Namco", "Pac-Man", 0 )
"""
sets = find_bios_root_sets(source, "neogeo.cpp")
self.assertIn("neogeo", sets)
self.assertNotIn("pacman", sets)
roms = parse_rom_block(source, "neogeo")
self.assertEqual(len(roms), 1)
self.assertEqual(roms[0]["crc32"], "9036d879")
def test_fbneo_parser_finds_bios_sets(self):
from scripts.scraper.fbneo_parser import find_bios_sets, parse_rom_info
source = """
static struct BurnRomInfo neogeoRomDesc[] = {
{ "sp-s2.sp1", 0x020000, 0x9036d879, BRF_ESS | BRF_BIOS },
{ "", 0, 0, 0 }
};
STD_ROM_PICK(neogeo)
STD_ROM_FN(neogeo)
struct BurnDriver BurnDrvneogeo = {
"neogeo", NULL, NULL, NULL, "1990",
"Neo Geo\\0", "BIOS only", "SNK", "Neo Geo MVS",
NULL, NULL, NULL, NULL, BDF_BOARDROM, 0, 0,
0, 0, 0, NULL, neogeoRomInfo, neogeoRomName, NULL, NULL,
NULL, NULL, NULL, NULL, 0
};
"""
sets = find_bios_sets(source, "d_neogeo.cpp")
self.assertIn("neogeo", sets)
roms = parse_rom_info(source, "neogeo")
self.assertEqual(len(roms), 1)
self.assertEqual(roms[0]["crc32"], "9036d879")
def test_mame_merge_preserves_manual_fields(self):
import json as json_mod
from scripts.scraper._hash_merge import merge_mame_profile
merge_dir = os.path.join(self.root, "merge_mame")
os.makedirs(merge_dir)
profile = {
"emulator": "Test",
"type": "libretro",
"upstream": "https://github.com/mamedev/mame",
"core_version": "0.285",
"files": [
{
"name": "neogeo.zip",
"required": True,
"category": "bios_zip",
"system": "snk-neogeo-mvs",
"note": "MVS BIOS",
"source_ref": "old.cpp:1",
"contents": [
{"name": "sp-s2.sp1", "size": 131072, "crc32": "oldcrc"}
],
}
],
}
profile_path = os.path.join(merge_dir, "test.yml")
with open(profile_path, "w") as f:
yaml.dump(profile, f, sort_keys=False)
hashes = {
"source": "mamedev/mame",
"version": "0.286",
"commit": "abc",
"fetched_at": "2026-03-30T00:00:00Z",
"bios_sets": {
"neogeo": {
"source_file": "neo.cpp",
"source_line": 42,
"roms": [
{
"name": "sp-s2.sp1",
"size": 131072,
"crc32": "newcrc",
"sha1": "abc123",
}
],
}
},
}
hashes_path = os.path.join(merge_dir, "hashes.json")
with open(hashes_path, "w") as f:
json_mod.dump(hashes, f)
result = merge_mame_profile(profile_path, hashes_path)
neo = next(f for f in result["files"] if f["name"] == "neogeo.zip")
self.assertEqual(neo["contents"][0]["crc32"], "newcrc")
self.assertEqual(neo["system"], "snk-neogeo-mvs")
self.assertEqual(neo["note"], "MVS BIOS")
self.assertEqual(neo["source_ref"], "neo.cpp:42")
self.assertEqual(result["core_version"], "0.286")
def test_fbneo_merge_updates_individual_roms(self):
import json as json_mod
from scripts.scraper._hash_merge import merge_fbneo_profile
merge_dir = os.path.join(self.root, "merge_fbneo")
os.makedirs(merge_dir)
profile = {
"emulator": "FBNeo",
"type": "libretro",
"upstream": "https://github.com/finalburnneo/FBNeo",
"core_version": "v1.0.0.02",
"files": [
{
"name": "sp-s2.sp1",
"archive": "neogeo.zip",
"system": "snk-neogeo-mvs",
"required": True,
"size": 131072,
"crc32": "oldcrc",
}
],
}
profile_path = os.path.join(merge_dir, "fbneo.yml")
with open(profile_path, "w") as f:
yaml.dump(profile, f, sort_keys=False)
hashes = {
"source": "finalburnneo/FBNeo",
"version": "v1.0.0.03",
"commit": "def",
"fetched_at": "2026-03-30T00:00:00Z",
"bios_sets": {
"neogeo": {
"source_file": "neo.cpp",
"source_line": 10,
"roms": [{"name": "sp-s2.sp1", "size": 131072, "crc32": "newcrc"}],
}
},
}
hashes_path = os.path.join(merge_dir, "hashes.json")
with open(hashes_path, "w") as f:
json_mod.dump(hashes, f)
result = merge_fbneo_profile(profile_path, hashes_path)
rom = next(f for f in result["files"] if f["name"] == "sp-s2.sp1")
self.assertEqual(rom["crc32"], "newcrc")
self.assertEqual(rom["system"], "snk-neogeo-mvs")
self.assertEqual(result["core_version"], "v1.0.0.03")
def _load_config(self, platform_name: str) -> dict:
return load_platform_config(platform_name, self.platforms_dir)
def test_200_find_undeclared_include_all(self):
"""include_all=True returns ALL core files, including declared ones."""
from verify import find_undeclared_files
config = self._load_config("test_existence")
profiles = load_emulator_profiles(self.emulators_dir)
# Without include_all: only undeclared files returned
undeclared = find_undeclared_files(
config, self.emulators_dir, self.db, profiles
)
undeclared_names = {u["name"] for u in undeclared}
# present_req.bin is declared in platform YAML, should NOT be in undeclared
self.assertNotIn("present_req.bin", undeclared_names)
# With include_all: ALL core files returned, including declared ones
all_files = find_undeclared_files(
config, self.emulators_dir, self.db, profiles, include_all=True
)
all_names = {u["name"] for u in all_files}
# present_req.bin IS declared but should be returned with include_all
self.assertIn("present_req.bin", all_names)
# undeclared files should still be present
self.assertIn("undeclared_req.bin", all_names)
# Launcher/alias files should still be excluded
self.assertNotIn("launcher_bios.bin", all_names)
def test_201_collect_emulator_extras_include_all(self):
"""include_all=True passes through to find_undeclared_files."""
from generate_pack import _collect_emulator_extras
config = self._load_config("test_existence")
profiles = load_emulator_profiles(self.emulators_dir)
base_dest = config.get("base_destination", "")
# Default call must succeed without TypeError
extras = _collect_emulator_extras(
config, self.emulators_dir, self.db, set(), base_dest, profiles
)
self.assertIsInstance(extras, list)
# include_all=True must be accepted and return a list
all_extras = _collect_emulator_extras(
config, self.emulators_dir, self.db, set(), base_dest, profiles,
include_all=True,
)
self.assertIsInstance(all_extras, list)
# include_all=True is a superset: at least as many entries as default
all_names = {e["name"] for e in all_extras}
default_names = {e["name"] for e in extras}
self.assertGreaterEqual(len(all_names), len(default_names))
# All default entries are present in include_all result
for name in default_names:
self.assertIn(name, all_names)
def test_202_pack_source_platform(self):
"""source='platform' skips core extras."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_platform")
os.makedirs(output_dir, exist_ok=True)
profiles = load_emulator_profiles(self.emulators_dir)
zip_path = generate_pack(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
emu_profiles=profiles,
emulators_dir=self.emulators_dir,
source="platform",
)
self.assertIsNotNone(zip_path)
self.assertIn("_Platform_", os.path.basename(zip_path))
def test_203_pack_source_truth(self):
"""source='truth' uses emulator profile files."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_truth")
os.makedirs(output_dir, exist_ok=True)
profiles = load_emulator_profiles(self.emulators_dir)
zip_path = generate_pack(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
emu_profiles=profiles,
emulators_dir=self.emulators_dir,
source="truth",
)
self.assertIsNotNone(zip_path)
self.assertIn("_Truth_", os.path.basename(zip_path))
def test_204_pack_source_full_unchanged(self):
"""source='full' (default) has no source tag in filename."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_full")
os.makedirs(output_dir, exist_ok=True)
profiles = load_emulator_profiles(self.emulators_dir)
zip_path = generate_pack(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
emu_profiles=profiles,
emulators_dir=self.emulators_dir,
source="full",
)
self.assertIsNotNone(zip_path)
bn = os.path.basename(zip_path)
self.assertNotIn("_Platform_", bn)
self.assertNotIn("_Truth_", bn)
def test_205_pack_source_platform_required(self):
"""source='platform' + required_only=True."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_plat_req")
os.makedirs(output_dir, exist_ok=True)
profiles = load_emulator_profiles(self.emulators_dir)
zip_path = generate_pack(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
emu_profiles=profiles,
emulators_dir=self.emulators_dir,
source="platform",
required_only=True,
)
self.assertIsNotNone(zip_path)
self.assertIn("_Platform_Required_", os.path.basename(zip_path))
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
self.assertTrue(any("present_req.bin" in n for n in names))
self.assertFalse(any("present_opt.bin" in n for n in names))
def test_206_pack_source_truth_required(self):
"""source='truth' + required_only=True."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_truth_req")
os.makedirs(output_dir, exist_ok=True)
profiles = load_emulator_profiles(self.emulators_dir)
zip_path = generate_pack(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
output_dir,
emu_profiles=profiles,
emulators_dir=self.emulators_dir,
source="truth",
required_only=True,
)
self.assertIsNotNone(zip_path)
self.assertIn("_Truth_Required_", os.path.basename(zip_path))
def test_207_manifest_source(self):
"""generate_manifest respects source param."""
from generate_pack import generate_manifest
profiles = load_emulator_profiles(self.emulators_dir)
registry_path = os.path.join(self.platforms_dir, "_registry.yml")
if not os.path.exists(registry_path):
with open(registry_path, "w") as fh:
yaml.dump({"platforms": {}}, fh)
manifest_full = generate_manifest(
"test_existence", self.platforms_dir, self.db, self.bios_dir,
registry_path, emulators_dir=self.emulators_dir, emu_profiles=profiles,
source="full", offline=True,
)
manifest_plat = generate_manifest(
"test_existence", self.platforms_dir, self.db, self.bios_dir,
registry_path, emulators_dir=self.emulators_dir, emu_profiles=profiles,
source="platform", offline=True,
)
self.assertLessEqual(manifest_plat["total_files"], manifest_full["total_files"])
self.assertEqual(manifest_plat.get("source"), "platform")
self.assertEqual(manifest_full.get("source"), "full")
def test_208_split_source_tag_in_dirname(self):
"""generate_split_packs uses source tag in split directory name."""
from generate_pack import generate_split_packs
output_dir = os.path.join(self.root, "split_src")
os.makedirs(output_dir, exist_ok=True)
profiles = load_emulator_profiles(self.emulators_dir)
generate_split_packs(
"test_existence", self.platforms_dir, self.db, self.bios_dir,
output_dir, emulators_dir=self.emulators_dir, emu_profiles=profiles,
source="platform",
)
entries = os.listdir(output_dir)
platform_dirs = [e for e in entries if "_Platform_" in e and "Split" in e]
self.assertTrue(len(platform_dirs) > 0, f"No _Platform_ split dir in {entries}")
def test_209_all_variants_generates_6_zips(self):
"""All 6 source x required combinations produce unique ZIPs."""
from generate_pack import generate_pack
output_dir = os.path.join(self.root, "pack_allvar")
os.makedirs(output_dir, exist_ok=True)
profiles = load_emulator_profiles(self.emulators_dir)
variants = [
("full", False), ("full", True),
("platform", False), ("platform", True),
("truth", False), ("truth", True),
]
for source, required_only in variants:
generate_pack(
"test_existence", self.platforms_dir, self.db, self.bios_dir,
output_dir, emu_profiles=profiles, emulators_dir=self.emulators_dir,
source=source, required_only=required_only,
)
zips = [f for f in os.listdir(output_dir) if f.endswith(".zip")]
self.assertEqual(len(zips), 6, f"Expected 6 ZIPs, got {len(zips)}: {zips}")
self.assertEqual(len(set(zips)), 6)
def _make_hash_zip_fixture(self, tmpdir, declare_md5=True):
"""Platform fixture with one ZIP file, optionally md5-declared."""
import hashlib as hl
plat_dir = os.path.join(tmpdir, "platforms")
bios_dir = os.path.join(tmpdir, "bios", "Arcade")
out_dir = os.path.join(tmpdir, "dist")
os.makedirs(plat_dir)
os.makedirs(bios_dir)
zip_src = os.path.join(bios_dir, "testbios.zip")
with zipfile.ZipFile(zip_src, "w", zipfile.ZIP_DEFLATED) as zf:
info = zipfile.ZipInfo("rom.bin", date_time=(2020, 6, 15, 12, 0, 0))
zf.writestr(info, b"rom_content_bytes")
from common import compute_hashes
h = compute_hashes(zip_src)
db = {
"files": {
h["sha1"]: {
"name": "testbios.zip",
"md5": h["md5"],
"sha1": h["sha1"],
"path": zip_src,
"paths": ["Arcade/testbios.zip"],
},
},
"indexes": {
"by_md5": {h["md5"]: h["sha1"]},
"by_name": {"testbios.zip": [h["sha1"]]},
"by_crc32": {},
"by_path_suffix": {},
},
}
entry = {"name": "testbios.zip", "destination": "dc/testbios.zip"}
if declare_md5:
entry["md5"] = h["md5"]
config = {
"platform": "HashZipTest",
"verification_mode": "md5",
"base_destination": "bios",
"systems": {"arcade-sys": {"files": [entry]}},
}
with open(os.path.join(plat_dir, "test_hashzip.yml"), "w") as fh:
yaml.dump(config, fh)
return plat_dir, os.path.join(tmpdir, "bios"), out_dir, db, h, hl
def test_210_pack_copies_md5_declared_zip_byte_identical(self):
"""md5-declared ZIPs are copied unchanged, never rebuilt."""
import tempfile
from generate_pack import generate_pack
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir, bios_root, out_dir, db, h, hl = self._make_hash_zip_fixture(
tmpdir, declare_md5=True
)
zip_path = generate_pack(
"test_hashzip", plat_dir, db, bios_root, out_dir
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
data = zf.read("dc/testbios.zip")
self.assertEqual(hl.md5(data).hexdigest(), h["md5"])
def test_211_pack_rebuilds_zip_without_declared_hash(self):
"""ZIPs without declared hash are rebuilt deterministically."""
import io
import tempfile
from generate_pack import generate_pack
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir, bios_root, out_dir, db, h, hl = self._make_hash_zip_fixture(
tmpdir, declare_md5=False
)
zip_path = generate_pack(
"test_hashzip", plat_dir, db, bios_root, out_dir
)
self.assertIsNotNone(zip_path)
with zipfile.ZipFile(zip_path) as zf:
data = zf.read("dc/testbios.zip")
# Outer hash differs (1980 timestamps), inner content identical
self.assertNotEqual(hl.md5(data).hexdigest(), h["md5"])
with zipfile.ZipFile(io.BytesIO(data)) as inner:
self.assertEqual(inner.read("rom.bin"), b"rom_content_bytes")
def test_212_verify_pack_flags_native_md5_mismatch(self):
"""Pack verification reproduces the native md5 check per member."""
import tempfile
from generate_pack import verify_pack_against_platform
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir, bios_root, out_dir, db, h, hl = self._make_hash_zip_fixture(
tmpdir, declare_md5=True
)
os.makedirs(out_dir)
bad_pack = os.path.join(out_dir, "HashZipTest_BIOS_Pack.zip")
with zipfile.ZipFile(bad_pack, "w") as zf:
zf.writestr("dc/testbios.zip", b"wrong bytes entirely")
result = verify_pack_against_platform(
bad_pack, "test_hashzip", plat_dir, db=None
)
errors = result[3]
self.assertTrue(
any("md5" in e for e in errors),
f"expected md5 mismatch error, got: {errors}",
)
def test_213_verify_pack_passes_correct_member_hash(self):
"""Pack verification passes when member bytes match declared md5."""
import tempfile
from generate_pack import verify_pack_against_platform
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir, bios_root, out_dir, db, h, hl = self._make_hash_zip_fixture(
tmpdir, declare_md5=True
)
os.makedirs(out_dir)
good_pack = os.path.join(out_dir, "HashZipTest_BIOS_Pack.zip")
src = os.path.join(bios_root, "Arcade", "testbios.zip")
with zipfile.ZipFile(good_pack, "w") as zf:
zf.write(src, "dc/testbios.zip")
result = verify_pack_against_platform(
good_pack, "test_hashzip", plat_dir, db=None
)
self.assertEqual(result[3], [], f"unexpected errors: {result[3]}")
def test_214_hash_matches_truncated_and_multi(self):
"""Declared-hash compare handles truncation, case, multi-values."""
from generate_pack import _hash_matches
full = "85254fbe320ca82a768ec2c26bb08def"
self.assertTrue(_hash_matches(full, full))
self.assertTrue(_hash_matches(full.upper(), full))
self.assertTrue(_hash_matches(full[:29], full)) # Batocera truncated
self.assertTrue(_hash_matches(f"deadbeef, {full}", full))
self.assertFalse(_hash_matches("0" * 32, full))
self.assertFalse(_hash_matches("", full))
def test_216_resolve_by_sha256(self):
"""resolve_local_file matches files by declared sha256."""
import tempfile
from common import resolve_local_file
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "SNES_dsp1.rom")
with open(path, "wb") as fh:
fh.write(b"dsp firmware bytes")
import hashlib as hl
sha256 = hl.sha256(b"dsp firmware bytes").hexdigest()
db = {
"files": {
"sha1x": {
"name": "SNES_dsp1.rom",
"path": path,
"sha256": sha256,
},
},
"indexes": {
"by_md5": {},
"by_name": {"SNES_dsp1.rom": ["sha1x"]},
"by_sha256": {sha256: "sha1x"},
"by_crc32": {},
"by_path_suffix": {},
},
}
# Name differs, only sha256 identifies the content
entry = {"name": "dsp1.rom", "sha256": sha256}
local, status = resolve_local_file(entry, db)
self.assertEqual(local, path)
self.assertEqual(status, "sha256_exact")
# Multi-hash list also resolves
entry = {"name": "dsp1.rom", "sha256": f"{'0' * 64},{sha256}"}
local, status = resolve_local_file(entry, db)
self.assertEqual(local, path)
# Unknown sha256 falls through to not_found
entry = {"name": "dsp1.rom", "sha256": "f" * 64}
local, status = resolve_local_file(entry, db)
self.assertIsNone(local)
self.assertEqual(status, "not_found")
def test_220_large_file_dotted_asset_fallback(self):
"""Files with spaces resolve to GitHub's dotted asset name."""
import tempfile
import urllib.error
import common
attempts = []
class FakeResponse:
def __init__(self, payload):
self._payload = payload
def read(self, _size=None):
data, self._payload = self._payload, b""
return data
def __enter__(self):
return self
def __exit__(self, *_):
return False
def fake_urlopen(req, timeout=None):
url = req.full_url if hasattr(req, "full_url") else str(req)
attempts.append(url)
if "MAME%200.174" in url:
raise urllib.error.HTTPError(url, 404, "Not Found", {}, None)
return FakeResponse(b"payload")
original = largefiles.urllib.request.urlopen
largefiles.urllib.request.urlopen = fake_urlopen
try:
with tempfile.TemporaryDirectory() as tmpdir:
path = common.fetch_large_file(
"MAME 0.174 Arcade XML.dat", dest_dir=tmpdir
)
self.assertIsNotNone(path)
with open(path, "rb") as fh:
self.assertEqual(fh.read(), b"payload")
finally:
largefiles.urllib.request.urlopen = original
self.assertEqual(len(attempts), 2, attempts)
self.assertIn("MAME%200.174", attempts[0])
self.assertIn("MAME.0.174.Arcade.XML.dat", attempts[1])
def test_219_batocera_stable_tag_selection(self):
"""Stable tag picker takes the highest batocera-N(.M) tag."""
from scripts.scraper.batocera_scraper import pick_stable_tag
tags = [
"uboot-rockpro64", "batocera-pre-br2-external",
"batocera-41", "batocera-42", "batocera-43", "batocera-43.1",
"master_20180518",
]
self.assertEqual(pick_stable_tag(tags), "batocera-43.1")
self.assertEqual(pick_stable_tag(["batocera-9", "batocera-10"]), "batocera-10")
self.assertIsNone(pick_stable_tag(["uboot-x", "random"]))
def test_218_resolve_by_crc32_and_size(self):
"""crc-only profile entries resolve by crc32 confirmed by size."""
import tempfile
import zlib
from common import resolve_local_file
with tempfile.TemporaryDirectory() as tmpdir:
data = b"exos rom bytes" * 100
path = os.path.join(tmpdir, "exos21.rom")
with open(path, "wb") as fh:
fh.write(data)
crc = f"{zlib.crc32(data) & 0xffffffff:08x}"
db = {
"files": {
"sha1e": {
"name": "exos21.rom",
"path": path,
"size": len(data),
"crc32": crc,
},
},
"indexes": {
"by_md5": {},
"by_name": {"exos21.rom": ["sha1e"]},
"by_crc32": {crc: "sha1e"},
"by_path_suffix": {},
},
}
# Different extension in the profile, crc32+size identify it
local, status = resolve_local_file(
{"name": "exos21.bin", "crc32": crc, "size": len(data)}, db
)
self.assertEqual(local, path)
self.assertEqual(status, "crc32_exact")
# Wrong size rejects the crc match
local, status = resolve_local_file(
{"name": "exos21.bin", "crc32": crc, "size": 1}, db
)
self.assertIsNone(local)
def test_217_resolve_name_casefold_fallback(self):
"""Name resolution falls back to a case-insensitive match."""
import tempfile
from common import resolve_local_file
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "VEC_Minestorm.vec")
with open(path, "wb") as fh:
fh.write(b"minestorm rom")
db = {
"files": {"sha1v": {"name": "VEC_Minestorm.vec", "path": path}},
"indexes": {
"by_md5": {},
"by_name": {"VEC_Minestorm.vec": ["sha1v"]},
"by_crc32": {},
"by_path_suffix": {},
},
}
# Upstream declares a different casing, no hash
local, status = resolve_local_file(
{"name": "VEC_MineStorm.vec"}, db
)
self.assertEqual(local, path)
self.assertEqual(status, "name_exact")
def test_215_check_member_hash_inside_zip(self):
"""zipped_file entries verify the ROM inside the ZIP, not the ZIP."""
import hashlib as hl
import tempfile
from generate_pack import _check_member_hash
rom = b"inner rom bytes"
rom_md5 = hl.md5(rom).hexdigest()
with tempfile.TemporaryDirectory() as tmpdir:
pack = os.path.join(tmpdir, "pack.zip")
import io
inner_buf = io.BytesIO()
with zipfile.ZipFile(inner_buf, "w") as inner:
inner.writestr("EPR-123.IC27", rom)
with zipfile.ZipFile(pack, "w") as zf:
zf.writestr("dc/game.zip", inner_buf.getvalue())
with zipfile.ZipFile(pack) as zf:
# Case-insensitive inner match (Batocera casefold)
ok = _check_member_hash(
zf,
"dc/game.zip",
{"zipped_file": "epr-123.ic27", "md5": rom_md5},
"md5",
)
self.assertIsNone(ok)
bad = _check_member_hash(
zf,
"dc/game.zip",
{"zipped_file": "epr-123.ic27", "md5": "0" * 32},
"md5",
)
self.assertIsNotNone(bad)
# -- MiSTer FPGA scraper (BiosDB) -------------------------------------
MISTER_DB = {
"db_id": "bios_db",
"timestamp": 1785183064,
"files": {
"games/3DO/boot.rom": {
"hash": "f47264dd47fe30f73ab3c010015c155b",
"size": 1048576,
"url": "https://archive.org/download/x/3DO.zip/boot.rom",
},
"games/GameCom/boot.rom": {
"hash": "3f2178d717dd309ed7424d49ef92987f",
"size": 262144,
"url": "https://archive.org/download/x/GameCom.zip/boot.rom",
},
},
"archives": {
"neogeo_unibios": {
"archive_file": {"hash": "a" * 32, "size": 101498, "url": "http://x/u.zip"},
"summary_inline": {
"files": {
"games/NEOGEO/uni-bios.rom": {
"hash": "4f0aeda8d2d145f596826b62d563c4ef",
"size": 131072,
}
}
},
}
},
}
def _mister_scraper(self):
import json
from scraper.misterfpga_scraper import Scraper
scraper = Scraper()
scraper._raw_data = json.dumps(self.MISTER_DB)
return scraper
def test_221_mister_parse_db_inlines_archive_entries(self):
"""Archive-provided files carry the extracted member hash."""
import json
from scraper.misterfpga_scraper import parse_db
entries = parse_db(json.dumps(self.MISTER_DB))
self.assertIn("games/NEOGEO/uni-bios.rom", entries)
self.assertEqual(
entries["games/NEOGEO/uni-bios.rom"]["hash"],
"4f0aeda8d2d145f596826b62d563c4ef",
)
self.assertEqual(len(entries), 3)
def test_222_mister_validate_format_rejects_foreign_db(self):
"""Only the bios_db database is accepted."""
import json
scraper = self._mister_scraper()
self.assertTrue(scraper.validate_format(json.dumps(self.MISTER_DB)))
self.assertFalse(scraper.validate_format("not json"))
self.assertFalse(
scraper.validate_format(json.dumps({"db_id": "distribution_mister", "files": {}}))
)
broken = json.loads(json.dumps(self.MISTER_DB))
broken["files"]["games/3DO/boot.rom"].pop("size")
self.assertFalse(scraper.validate_format(json.dumps(broken)))
def test_223_mister_requirements_strip_games_prefix(self):
"""Destinations are relative to base_destination games."""
reqs = {r.destination: r for r in self._mister_scraper().fetch_requirements()}
self.assertIn("3DO/boot.rom", reqs)
self.assertEqual(reqs["3DO/boot.rom"].name, "boot.rom")
self.assertEqual(reqs["3DO/boot.rom"].system, "3do")
self.assertEqual(reqs["3DO/boot.rom"].native_id, "3DO")
self.assertEqual(reqs["3DO/boot.rom"].size, 1048576)
def test_224_mister_platform_yaml_shape(self):
"""The generated config declares md5 verification and no cores."""
config = self._mister_scraper().generate_platform_yaml()
self.assertEqual(config["verification_mode"], "md5")
self.assertEqual(config["base_destination"], "games")
self.assertEqual(config["cores"], [])
self.assertEqual(config["version"], "2026-07-27")
systems = config["systems"]
self.assertEqual(
systems["3do"]["docs"], "https://github.com/MiSTer-devel/3DO_MiSTer"
)
# No MiSTer-devel repository exists for the Game.com core
self.assertNotIn("docs", systems["tiger-game-com"])
entry = systems["3do"]["files"][0]
self.assertTrue(entry["required"])
self.assertEqual(entry["md5"], "f47264dd47fe30f73ab3c010015c155b")
def test_225_mister_docs_targets_are_unique_per_system(self):
"""Every mapped system resolves to a distinct retrobios system ID."""
from scraper.misterfpga_scraper import SYSTEM_MAP
folders = list(SYSTEM_MAP)
systems = [SYSTEM_MAP[f][0] for f in folders]
self.assertEqual(len(systems), len(set(systems)))
def test_226_cross_reference_platform_filter(self):
"""--platform restricts declarations to that platform alone."""
from cross_reference import load_platform_files
with tempfile.TemporaryDirectory() as tmpdir:
plat_dir = os.path.join(tmpdir, "platforms")
os.makedirs(plat_dir)
registry = {
"platforms": {
"alpha": {"status": "active"},
"beta": {"status": "active"},
}
}
with open(os.path.join(plat_dir, "_registry.yml"), "w") as f:
yaml.dump(registry, f)
for name, fname in (("alpha", "a.bin"), ("beta", "b.bin")):
cfg = {
"platform": name,
"verification_mode": "md5",
"systems": {"shared-system": {"files": [{"name": fname}]}},
}
with open(os.path.join(plat_dir, f"{name}.yml"), "w") as f:
yaml.dump(cfg, f)
everything, _ = load_platform_files(plat_dir)
self.assertEqual(everything["shared-system"], {"a.bin", "b.bin"})
only_alpha, _ = load_platform_files(plat_dir, ["alpha"])
self.assertEqual(only_alpha["shared-system"], {"a.bin"})
def test_227_manifest_injected_compressed(self):
"""The manifest is deflated whichever injection path runs."""
import json as _json
from generate_pack import inject_manifest
manifest = {"files": [{"path": f"f{i}.bin"} for i in range(200)]}
raw = _json.dumps(manifest, indent=2, ensure_ascii=False)
sizes = []
with tempfile.TemporaryDirectory() as tmpdir:
for preexisting in (False, True):
path = os.path.join(tmpdir, f"pack{int(preexisting)}.zip")
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("bios.bin", b"x" * 4096)
if preexisting:
zf.writestr("manifest.json", "{}")
inject_manifest(path, manifest)
with zipfile.ZipFile(path) as zf:
info = zf.getinfo("manifest.json")
self.assertEqual(info.compress_type, zipfile.ZIP_DEFLATED)
self.assertEqual(_json.loads(zf.read("manifest.json")), manifest)
sizes.append(info.compress_size)
self.assertLess(sizes[0], len(raw))
self.assertEqual(sizes[0], sizes[1])
# ---------------------------------------------------------------
# Region filter
# ---------------------------------------------------------------
def _region_profiles(self) -> dict:
return {
"rgncore": {
"emulator": "RegionCore",
"type": "libretro",
"cores": ["rgncore"],
"systems": ["region-console"],
"files": [
{"name": "rgn_us.bin", "region": ["north-america"]},
{"name": "rgn_jp.bin", "region": ["japan"]},
{"name": "rgn_eu.bin", "region": ["europe"]},
{"name": "rgn_world.bin", "region": ["world"]},
{"name": "rgn_plain.bin"},
],
}
}
def _create_region_fixtures(self) -> None:
names = (
"rgn_us.bin",
"rgn_jp.bin",
"rgn_eu.bin",
"rgn_world.bin",
"rgn_plain.bin",
)
for name in names:
self._make_file(name, name.encode())
self.db = self._build_db()
config = {
"platform": "RegionPlat",
"verification_mode": "existence",
"systems": {
"region-console": {
"files": [
{
"name": n,
"destination": n,
"required": n
in ("rgn_us.bin", "rgn_jp.bin", "rgn_eu.bin"),
}
for n in names
]
}
},
}
with open(
os.path.join(self.platforms_dir, "regionplat.yml"), "w", encoding="utf-8"
) as fh:
yaml.safe_dump(config, fh)
def _region_pack(self, **kwargs) -> str:
from generate_pack import generate_pack
self._create_region_fixtures()
out = generate_pack(
"regionplat",
self.platforms_dir,
self.db,
self.bios_dir,
self.root,
emu_profiles=self._region_profiles(),
**kwargs,
)
self.assertIsNotNone(out)
return out
@staticmethod
def _names_in(zip_path: str) -> set[str]:
with zipfile.ZipFile(zip_path) as zf:
return {os.path.basename(n) for n in zf.namelist()}
def test_region_filter_drops_other_regions(self):
names = self._names_in(self._region_pack(regions=["north-america"]))
self.assertIn("rgn_us.bin", names)
self.assertNotIn("rgn_jp.bin", names)
self.assertNotIn("rgn_eu.bin", names)
def test_region_priority_selects_second_choice(self):
names = self._names_in(self._region_pack(regions=["oceania", "europe"]))
self.assertIn("rgn_eu.bin", names)
self.assertNotIn("rgn_us.bin", names)
self.assertNotIn("rgn_jp.bin", names)
def test_region_filter_uses_world_when_no_regional_match(self):
names = self._names_in(self._region_pack(regions=["oceania"]))
self.assertIn("rgn_world.bin", names)
for expected in ("rgn_us.bin", "rgn_jp.bin", "rgn_eu.bin"):
self.assertNotIn(expected, names)
def test_region_filter_keeps_untagged_and_world(self):
names = self._names_in(self._region_pack(regions=["north-america"]))
self.assertIn("rgn_plain.bin", names)
self.assertIn("rgn_world.bin", names)
def test_region_tag_in_pack_name(self):
out = self._region_pack(regions=["north-america", "japan"])
self.assertIn("NorthAmerica_Japan", os.path.basename(out))
def test_no_region_argument_changes_nothing(self):
names = self._names_in(self._region_pack())
for expected in (
"rgn_us.bin",
"rgn_jp.bin",
"rgn_eu.bin",
"rgn_plain.bin",
"rgn_world.bin",
):
self.assertIn(expected, names)
def test_region_composes_with_required_only(self):
out = self._region_pack(regions=["north-america"], required_only=True)
names = self._names_in(out)
self.assertIn("rgn_us.bin", names)
self.assertNotIn("rgn_jp.bin", names)
self.assertNotIn("rgn_eu.bin", names)
self.assertIn("NorthAmerica", os.path.basename(out))
self.assertIn("Required", os.path.basename(out))
def test_region_composes_with_source_platform(self):
names = self._names_in(
self._region_pack(regions=["north-america"], source="platform")
)
self.assertIn("rgn_us.bin", names)
self.assertNotIn("rgn_jp.bin", names)
self.assertNotIn("rgn_eu.bin", names)
self.assertIn("rgn_world.bin", names)
self.assertIn("rgn_plain.bin", names)
def test_region_composes_with_split(self):
from generate_pack import generate_split_packs
self._create_region_fixtures()
paths = generate_split_packs(
"regionplat",
self.platforms_dir,
self.db,
self.bios_dir,
self.root,
emu_profiles=self._region_profiles(),
regions=["north-america"],
offline=True,
)
self.assertTrue(paths)
for p in paths:
self.assertIn("NorthAmerica", os.path.basename(p))
names = self._names_in(p)
self.assertNotIn("rgn_jp.bin", names)
self.assertNotIn("rgn_eu.bin", names)
def test_region_composes_with_split_and_target(self):
from generate_pack import generate_split_packs
self._create_region_fixtures()
paths = generate_split_packs(
"regionplat",
self.platforms_dir,
self.db,
self.bios_dir,
self.root,
emu_profiles=self._region_profiles(),
target_cores={"rgncore"},
regions=["north-america"],
)
self.assertTrue(paths)
for p in paths:
self.assertIn("NorthAmerica", os.path.basename(p))
names = self._names_in(p)
self.assertNotIn("rgn_jp.bin", names)
self.assertNotIn("rgn_eu.bin", names)
self.assertTrue(
any("rgn_us.bin" in self._names_in(p) for p in paths),
"target filtering removed the whole system",
)
def test_region_composes_with_manifest(self):
from generate_pack import generate_manifest
self._create_region_fixtures()
registry_path = os.path.join(self.platforms_dir, "_registry.yml")
if not os.path.exists(registry_path):
with open(registry_path, "w", encoding="utf-8") as fh:
yaml.safe_dump({"platforms": {}}, fh)
manifest = generate_manifest(
"regionplat",
self.platforms_dir,
self.db,
self.bios_dir,
registry_path,
emu_profiles=self._region_profiles(),
regions=["north-america"],
)
packed = {os.path.basename(f["dest"]) for f in manifest["files"]}
self.assertEqual(manifest["regions"], ["north-america"])
self.assertIn("rgn_us.bin", packed)
self.assertNotIn("rgn_jp.bin", packed)
self.assertNotIn("rgn_eu.bin", packed)
def test_manifest_without_region_keeps_every_variant(self):
from generate_pack import generate_manifest
self._create_region_fixtures()
registry_path = os.path.join(self.platforms_dir, "_registry.yml")
if not os.path.exists(registry_path):
with open(registry_path, "w", encoding="utf-8") as fh:
yaml.safe_dump({"platforms": {}}, fh)
manifest = generate_manifest(
"regionplat",
self.platforms_dir,
self.db,
self.bios_dir,
registry_path,
emu_profiles=self._region_profiles(),
offline=True,
)
packed = {os.path.basename(f["dest"]) for f in manifest["files"]}
for expected in ("rgn_us.bin", "rgn_jp.bin", "rgn_eu.bin"):
self.assertIn(expected, packed)
def test_region_composes_with_system_pack(self):
from generate_pack import generate_system_pack
self._create_region_fixtures()
os.makedirs(self.emulators_dir, exist_ok=True)
with open(
os.path.join(self.emulators_dir, "rgncore.yml"), "w", encoding="utf-8"
) as fh:
yaml.safe_dump(self._region_profiles()["rgncore"], fh)
out = generate_system_pack(
["region-console"],
self.emulators_dir,
self.db,
self.bios_dir,
self.root,
regions=["north-america"],
)
self.assertIsNotNone(out)
self.assertIn("NorthAmerica", os.path.basename(out))
names = self._names_in(out)
self.assertIn("rgn_us.bin", names)
self.assertNotIn("rgn_jp.bin", names)
def test_verify_platform_region_matches_the_pack(self):
from common import load_platform_config
from verify import verify_platform
self._create_region_fixtures()
profiles = self._region_profiles()
config = load_platform_config("regionplat", self.platforms_dir)
full = verify_platform(config, self.db, self.emulators_dir, profiles)
filtered = verify_platform(
config,
self.db,
self.emulators_dir,
profiles,
regions=["north-america"],
)
checked = {d["name"] for d in filtered["details"]}
self.assertIn("rgn_us.bin", checked)
self.assertNotIn("rgn_jp.bin", checked)
self.assertNotIn("rgn_eu.bin", checked)
self.assertLess(len(filtered["details"]), len(full["details"]))
packed = self._names_in(self._region_pack(regions=["north-america"]))
self.assertTrue(checked.issubset(packed), checked - packed)
def test_verify_emulator_region_matches_the_pack(self):
from generate_pack import generate_emulator_pack
from verify import verify_emulator
self._create_region_fixtures()
with open(
os.path.join(self.emulators_dir, "rgncore.yml"), "w", encoding="utf-8"
) as fh:
yaml.safe_dump(self._region_profiles()["rgncore"], fh)
full = verify_emulator(["rgncore"], self.emulators_dir, self.db)
filtered = verify_emulator(
["rgncore"], self.emulators_dir, self.db, regions=["north-america"]
)
checked = {d["name"] for d in filtered["details"]}
self.assertIn("rgn_us.bin", checked)
self.assertNotIn("rgn_jp.bin", checked)
self.assertLess(len(filtered["details"]), len(full["details"]))
out = generate_emulator_pack(
["rgncore"],
self.emulators_dir,
self.db,
self.bios_dir,
self.root,
regions=["north-america"],
)
self.assertTrue(checked.issubset(self._names_in(out)))
def test_every_narrowing_names_itself_and_says_so(self):
"""A dimension that removes files must show in the pack name and in the
README. Both are built from _narrowings, so this holds for any new one."""
from generate_pack import _build_readme, _narrowings
dimensions = [
{"source": "platform"},
{"source": "truth"},
{"regions": ["north-america"]},
{"target_name": "switch"},
{"one_per_slot": True},
{"required_only": True},
]
for kwargs in dimensions:
applied = _narrowings(
kwargs.get("source", "full"),
kwargs.get("regions"),
kwargs.get("target_name"),
kwargs.get("one_per_slot", False),
kwargs.get("required_only", False),
)
self.assertEqual(len(applied), 1, kwargs)
tag, label = applied[0]
self.assertTrue(tag.startswith("_") and len(tag) > 1, kwargs)
self.assertTrue(label.strip(), kwargs)
readme = _build_readme("p", "P", "", 1, 1, narrowings=applied)
self.assertIn("PACK TYPE: Narrowed", readme)
self.assertIn(label, readme)
def test_no_mode_swallows_a_narrowing_flag(self):
"""Every mode either honours a narrowing flag or refuses it.
Silently ignoring one is how --manifest-targets accepted --region and
wrote seven unfiltered manifests, and how --manifest accepted
--one-per-slot. A mode that cannot apply a flag must say so.
"""
import subprocess
repo = os.path.join(os.path.dirname(__file__), "..")
matrix = [
(["--manifest-targets"], ["--region", "us"], "refuse"),
(["--manifest-targets"], ["--one-per-slot"], "refuse"),
(["--platform", "retroarch", "--manifest"], ["--one-per-slot"], "refuse"),
(["--emulator", "duckstation"], ["--one-per-slot"], "refuse"),
(["--system", "sony-playstation"], ["--one-per-slot"], "refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"], ["--region", "us"],
"refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"], ["--one-per-slot"],
"refuse"),
]
for mode, flag, expected in matrix:
with self.subTest(mode=mode, flag=flag):
result = subprocess.run(
[sys.executable, "scripts/generate_pack.py", *mode, *flag,
"--output-dir", os.path.join(self.root, "modecheck")],
capture_output=True, text=True, cwd=repo, timeout=120,
)
combined = result.stdout + result.stderr
self.assertNotEqual(
result.returncode, 0,
f"{' '.join(mode + flag)} was accepted and ignored:\n{combined}",
)
self.assertIn("error:", combined)
def test_standalone_mode_names_itself(self):
"""--emulator --standalone ships a different file set; without a tag it
overwrote the pack built without it."""
from generate_pack import _narrowings
applied = _narrowings("full", None, None, False, False, standalone=True)
self.assertEqual([tag for tag, _l in applied], ["_Standalone"])
def test_every_name_builder_uses_the_same_list(self):
"""Four places name a pack or a manifest. None may assemble tags by
hand: that is how --one-per-slot reached --split unnamed."""
import inspect
import generate_pack
source = inspect.getsource(generate_pack)
for hand_rolled in (
'req_tag = "_Required"',
'source_tag = {"platform": "_Platform"',
'req_suffix = "_required"',
'slot_tag = "_OnePerSlot"',
):
self.assertNotIn(hand_rolled, source, hand_rolled)
self.assertGreaterEqual(source.count("_narrowings("), 5)
def test_a_full_pack_is_not_announced_as_narrowed(self):
from generate_pack import _build_readme, _narrowings
self.assertEqual(_narrowings("full", None, None, False, False), [])
self.assertNotIn(
"PACK TYPE: Narrowed", _build_readme("p", "P", "", 1, 1)
)
def test_system_filter_is_announced_too(self):
from generate_pack import _build_readme
readme = _build_readme(
"p", "P", "", 1, 1, system_filter=["sony-playstation"]
)
self.assertIn("PACK TYPE: Narrowed", readme)
self.assertIn("sony-playstation", readme)
def test_extras_group_under_their_system_not_a_shared_bucket(self):
"""find_undeclared_files reports the display name, the profiles are
keyed by slug; a key-only lookup put nearly every core extra in one
bucket and lost the per-system grouping both narrowing passes need."""
from generate_pack import _emulator_systems_index
index = _emulator_systems_index(
{
"beetle_psx": {
"emulator": "Beetle PSX (Mednafen PSX)",
"systems": ["sony-playstation"],
},
"beebem": {"emulator": "beebem", "systems": ["bbc-micro-b"]},
}
)
self.assertEqual(index["beetle_psx"], ["sony-playstation"])
self.assertEqual(
index["Beetle PSX (Mednafen PSX)"], ["sony-playstation"]
)
self.assertEqual(index["beebem"], ["bbc-micro-b"])
self.assertNotIn("_extras", index)
def test_slot_narrowed_pack_gets_its_own_name(self):
names = {
os.path.basename(self._region_pack()),
os.path.basename(self._region_pack(one_per_slot=True)),
}
self.assertEqual(len(names), 2, names)
self.assertTrue(any("OnePerSlot" in n for n in names))
def test_verify_pack_return_matches_its_annotation(self):
import inspect
import typing
from generate_pack import generate_pack, verify_pack_against_platform
self._create_region_fixtures()
profiles = self._region_profiles()
out = generate_pack(
"regionplat",
self.platforms_dir,
self.db,
self.bios_dir,
self.root,
emu_profiles=profiles,
)
result = verify_pack_against_platform(
out, "regionplat", self.platforms_dir, db=self.db, emu_profiles=profiles
)
annotated = typing.get_args(
inspect.signature(
verify_pack_against_platform, eval_str=True
).return_annotation
)
self.assertEqual(len(result), len(annotated))
def test_narrowed_variants_skip_conformance(self):
from generate_pack import _narrows_contents
for narrowed in (
"EmuDeck_2.3.8_Required_BIOS_Pack.zip",
"EmuDeck_2.3.8_Platform_BIOS_Pack.zip",
"EmuDeck_2.3.8_Truth_Required_BIOS_Pack.zip",
"EmuDeck_2.3.8_NorthAmerica_Required_BIOS_Pack.zip",
):
self.assertTrue(_narrows_contents(narrowed), narrowed)
for full in (
"EmuDeck_2.3.8_BIOS_Pack.zip",
"EmuDeck_2.3.8_NorthAmerica_BIOS_Pack.zip",
"RetroArch_v1.22.2_Switch_BIOS_Pack.zip",
):
self.assertFalse(_narrows_contents(full), full)
def test_target_tag_formats_hardware_names(self):
from generate_pack import _target_tag
self.assertEqual(_target_tag("switch"), "Switch")
self.assertEqual(_target_tag("rpi4"), "Rpi4")
self.assertEqual(_target_tag("x86_64"), "X86 64".replace(" ", ""))
def test_targeted_pack_does_not_overwrite_the_full_one(self):
names = {
os.path.basename(self._region_pack()),
os.path.basename(self._region_pack(target_name="switch")),
}
self.assertEqual(len(names), 2, names)
self.assertTrue(any("Switch" in n for n in names))
def test_target_and_region_tags_compose_in_the_name(self):
name = os.path.basename(
self._region_pack(regions=["north-america"], target_name="switch")
)
self.assertIn("NorthAmerica", name)
self.assertIn("Switch", name)
def test_readme_region_section_lists_fallback_systems(self):
from generate_pack import _build_readme
text = _build_readme(
"regionplat",
"RegionPlat",
"",
10,
2,
regions=["north-america"],
fallback_systems=["nintendo-fds"],
)
self.assertIn("PACK TYPE: Region Filtered", text)
self.assertIn("Region priority: North America", text)
self.assertIn("nintendo-fds", text)
self.assertIn("This pack was filtered", text)
self.assertNotIn("All are included", text)
def test_readme_without_region_keeps_the_regional_note(self):
from generate_pack import _build_readme
text = _build_readme("regionplat", "RegionPlat", "", 10, 2)
self.assertNotIn("PACK TYPE: Region Filtered", text)
self.assertIn("All are included", text)
def test_verify_agrees_with_region_filtered_pack(self):
from generate_pack import verify_pack_against_platform
out = self._region_pack(regions=["north-america"])
result = verify_pack_against_platform(
out,
"regionplat",
self.platforms_dir,
db=self.db,
emu_profiles=self._region_profiles(),
regions=["north-america"],
)
ok, _checked, _present, errors = result[:4]
self.assertTrue(ok, errors)
def test_verify_reports_the_gap_without_the_region_filter(self):
from generate_pack import verify_pack_against_platform
out = self._region_pack(regions=["north-america"])
result = verify_pack_against_platform(
out,
"regionplat",
self.platforms_dir,
db=self.db,
emu_profiles=self._region_profiles(),
)
ok, _checked, _present, errors = result[:4]
self.assertFalse(ok)
self.assertTrue(any("rgn_jp.bin" in e for e in errors), errors)
def test_region_composes_with_source_truth(self):
names = self._names_in(
self._region_pack(regions=["north-america"], source="truth")
)
self.assertIn("rgn_us.bin", names)
self.assertNotIn("rgn_jp.bin", names)
# ---- gap analysis resolves files the pack can already ship ----
@staticmethod
def _gap_db(repo_name: str, sha1: str, crc32: str) -> dict:
return {
"files": {sha1: {"path": f"bios/{repo_name}", "size": 8192}},
"indexes": {
"by_name": {repo_name: [sha1]},
"by_md5": {},
"by_crc32": {crc32: sha1},
"by_sha256": {},
"by_path_suffix": {},
},
}
def _gap_source(self, entry: dict, db: dict) -> str:
from cross_reference import cross_reference
report = cross_reference(
{"gapemu": {"emulator": "GapEmu", "systems": ["s"], "files": [entry]}},
{"s": set()},
db,
)
return report["gapemu"]["gap_details"][0]["source"]
def test_gap_analysis_resolves_by_alias(self):
"""A file stored under an alias is in the repo, not a sourcing target."""
db = self._gap_db("REPO_chip.rom", "a" * 40, "465c4e1c")
entry = {"name": "chip.rom", "aliases": ["REPO_chip.rom"], "required": True}
self.assertEqual(self._gap_source(entry, db), "bios")
def test_gap_analysis_resolves_by_crc32(self):
"""crc32 is the only hash some emulators declare."""
db = self._gap_db("REPO_chip.rom", "a" * 40, "465c4e1c")
entry = {"name": "chip.rom", "crc32": "465c4e1c", "required": True}
self.assertEqual(self._gap_source(entry, db), "bios")
def test_gap_analysis_reports_unknown_file_missing(self):
db = self._gap_db("REPO_chip.rom", "a" * 40, "465c4e1c")
entry = {"name": "chip.rom", "aliases": ["OTHER.rom"], "crc32": "deadbeef"}
self.assertEqual(self._gap_source(entry, db), "missing")
def test_gap_analysis_resolves_renamed_mame_set(self):
"""A renamed set is held under the name dedup kept, as verify reads it."""
import cross_reference
db = self._gap_db("v4bios.zip", "a" * 40, "465c4e1c")
original = cross_reference.get_mame_clone_map
cross_reference.get_mame_clone_map = lambda: {"bctvidbs.zip": "v4bios.zip"}
try:
entry = {"name": "bctvidbs.zip", "required": True}
self.assertEqual(self._gap_source(entry, db), "bios")
finally:
cross_reference.get_mame_clone_map = original
if __name__ == "__main__":
unittest.main()