Files
libretro/tests/test_e2e.py
T

6373 lines
253 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_20b_agnostic_core_fills_existence_slots(self):
"""An agnostic core on the platform serves a missing slot, as the pack does."""
config = load_platform_config("test_existence", self.platforms_dir)
result = verify_platform(config, self.db, self.emulators_dir)
c = result["severity_counts"]
self.assertEqual(c[Severity.OK], result["total_files"])
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)
# Without the filename-agnostic core, which would serve the two
# missing slots under existence (test_20b covers that case).
profiles = {
k: v for k, v in load_emulator_profiles(self.emulators_dir).items()
if v.get("bios_mode") != "agnostic"
}
result = verify_platform(config, self.db, self.emulators_dir, emu_profiles=profiles)
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_52_platform_grouping_splits_on_cores(self):
"""Same baseline, different emulators: the core files differ."""
declared = {
"cores_all": "all_libretro",
"cores_all_too": "all_libretro",
"cores_listed": ["retroarch", "openmsx"],
}
for name, cores in declared.items():
config = {
"platform": name,
"verification_mode": "existence",
"cores": cores,
"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(list(declared), self.platforms_dir)
self.assertEqual(
sorted(sorted(group) for group, _rep in groups),
[["cores_all", "cores_all_too"], ["cores_listed"]],
)
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_97b_a_data_directory_follows_its_build_mode(self):
"""A tree only the standalone build reads is no concern of the core."""
profile = {
"emulator": "ModeDirs",
"type": "standalone + libretro",
"cores": ["mode_dirs"],
"systems": ["console-a"],
"data_directories": [
{"ref": "standalone-only-dir", "mode": "standalone"},
{"ref": "shared-dir"},
],
"files": [{"name": "present_req.bin", "required": True}],
}
with open(os.path.join(self.emulators_dir, "mode_dirs.yml"), "w") as fh:
yaml.dump(profile, fh)
as_core = verify_emulator(["mode_dirs"], self.emulators_dir, self.db)
self.assertEqual(as_core.get("data_dir_notices", []), ["shared-dir"])
alone = verify_emulator(
["mode_dirs"], self.emulators_dir, self.db, standalone=True
)
self.assertEqual(
sorted(alone.get("data_dir_notices", [])),
["shared-dir", "standalone-only-dir"],
)
def test_97c_one_held_release_fills_a_slot_several_are_accepted_for(self):
"""Entries of one profile on one destination are alternatives.
A profile lists each release the code accepts under a name (the
shareware and the retail archive, told apart by size). Holding one
is the slot filled: the releases not held are not gaps and must not
drag the destination down to the worst status among them.
"""
present = os.path.join(self.bios_dir, "present_req.bin")
size = os.path.getsize(present)
profile = {
"emulator": "Releases",
"type": "standalone",
"cores": ["releases"],
"systems": ["console-a"],
"files": [
{"name": "present_req.bin", "path": "game/present_req.bin",
"required": False, "size": size + 7, "validation": ["size"],
"description": "retail"},
{"name": "present_req.bin", "path": "game/present_req.bin",
"required": False, "size": size, "validation": ["size"],
"description": "shareware"},
{"name": "absent_everywhere.bin", "path": "game/absent.bin",
"required": False},
],
}
with open(os.path.join(self.emulators_dir, "releases.yml"), "w") as fh:
yaml.dump(profile, fh)
result = verify_emulator(
["releases"], self.emulators_dir, self.db, standalone=True
)
statuses = {d["name"]: d["status"] for d in result["details"]}
self.assertEqual(statuses["present_req.bin"], "ok")
self.assertEqual(statuses["absent_everywhere.bin"], "missing")
self.assertEqual(
sum(1 for d in result["details"] if d["name"] == "present_req.bin"), 1
)
self.assertEqual(result["total_files"], 2)
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_sa"],
"standalone_cores": ["test_sa"],
"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_sa"],
"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_default_override_reaches_every_target(self):
"""`_default` drops a name on every target, then the target adds its own."""
self._write_target_fixtures()
from common import load_target_config
targets_dir = os.path.join(self.platforms_dir, "targets")
overrides = {
"testplatform": {
"targets": {
"_default": {"remove_cores": ["core_a"], "add_cores": ["core_z"]},
"target-full": {"aliases": ["full"], "add_cores": ["core_a"]},
},
},
}
with open(os.path.join(targets_dir, "_overrides.yml"), "w") as f:
yaml.dump(overrides, f)
self.assertEqual(
load_target_config("testplatform", "target-minimal", self.platforms_dir),
{"core_z"},
)
self.assertEqual(
load_target_config("testplatform", "full", self.platforms_dir),
{"core_a", "core_b", "core_c", "core_z"},
)
with self.assertRaises(ValueError):
load_target_config("testplatform", "_default", self.platforms_dir)
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_manifest_required_only_drops_optional_files(self):
"""A manifest named _required must hold only the required files.
The filename carried the narrowing while the content did not, so
install.py fetched every optional file for a user who asked for the
required set, and --all-variants wrote three manifests that duplicated
their non-required twins.
"""
from generate_pack import generate_manifest
registry_path = os.path.join(self.platforms_dir, "_test_registry.yml")
with open(registry_path, "w") as fh:
yaml.dump({"platforms": {"test_existence": {"install": {}}}}, fh)
def names(required_only):
manifest = generate_manifest(
"test_existence",
self.platforms_dir,
self.db,
self.bios_dir,
registry_path,
emulators_dir=self.emulators_dir,
offline=True,
required_only=required_only,
)
return {entry["dest"] for entry in manifest["files"]}
every = names(False)
required = names(True)
self.assertIn("present_req.bin", every)
self.assertIn("present_opt.bin", every)
self.assertIn("present_req.bin", required)
self.assertNotIn(
"present_opt.bin",
required,
"an optional file reached a manifest built with required_only",
)
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_", "RENAMED_"))
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"},
],
},
},
}
from exporter.baseline import build_native_model
systems, report = build_native_model(truth, scraped)
exporter = SystemDatExporter()
produced = exporter.render(systems, report, {}, scraped)
content = produced["System.dat"]
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(systems, produced)
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,
},
],
},
},
}
from exporter.baseline import build_native_model
systems, report = build_native_model(truth, {"systems": {}})
produced = SystemDatExporter().render(systems, report, {}, {"systems": {}})
self.assertIn("crc 70295038", produced["System.dat"])
# ---------------------------------------------------------------
# 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: a rename, not missing plus 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": []},
}
}
from exporter.baseline import build_native_model
# The systems dict is one block inside the checker Batocera runs, so
# the exporter is handed the script it patches.
skeleton = (
"#!/usr/bin/env python\n"
"systems = {\n"
"}\n"
"def checkBios(systems, prefix, filterROMs):\n"
" return {}\n"
)
systems, report = build_native_model(truth, scraped)
exp = Exporter()
produced = exp.render(
systems, report, {"batocera-systems": skeleton}, scraped
)
content = produced["batocera-systems"]
self.assertIn('"psx"', content)
self.assertIn("scph5501.bin", content)
self.assertIn("b" * 32, content)
self.assertIn("def checkBios(", content)
self.assertEqual(exp.validate(systems, produced), [])
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": []},
}
}
from exporter.baseline import build_native_model
systems, report = build_native_model(truth, scraped)
exp = Exporter()
produced = exp.render(systems, report, {}, scraped)
content = produced["es_bios.xml"]
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(systems, produced), [])
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": []},
}
}
from exporter.baseline import build_native_model
systems, report = build_native_model(truth, scraped)
exp = Exporter()
produced = exp.render(systems, report, {}, scraped)
data = _json.loads(produced["batocera-systems.json"])
self.assertIn("psx", data)
self.assertTrue(
any("scph5501" in bf["file"] for bf in data["psx"]["biosFiles"])
)
self.assertEqual(exp.validate(systems, produced), [])
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"),
# --verify-packs returns before the argument checks run, so it
# used to accept these and answer about the pack sitting in the
# output directory. --target is honoured instead of refused: the
# check narrows by it.
(["--platform", "retroarch", "--verify-packs"], ["--one-per-slot"],
"refuse"),
(["--platform", "retroarch", "--verify-packs"], ["--required-only"],
"refuse"),
(["--platform", "retroarch", "--verify-packs"], ["--source", "truth"],
"refuse"),
# Modes that read none of these: the flag reached the filename or
# nothing at all, never the selection.
(["--emulator", "duckstation"], ["--source", "truth"], "refuse"),
(["--system", "sony-playstation"], ["--source", "truth"], "refuse"),
(["--manifest-targets"], ["--target", "switch"], "refuse"),
(["--manifest-targets"], ["--required-only"], "refuse"),
(["--manifest-targets"], ["--source", "truth"], "refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"],
["--target", "switch"], "refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"],
["--required-only"], "refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"],
["--source", "truth"], "refuse"),
(["--platform", "retroarch", "--manifest"],
["--system", "sony-playstation"], "refuse"),
(["--platform", "retroarch", "--split"],
["--system", "sony-playstation"], "refuse"),
# --all took the same filter and wrote twelve complete manifests.
(["--all", "--manifest"], ["--system", "sony-playstation"], "refuse"),
(["--all", "--split"], ["--system", "sony-playstation"], "refuse"),
# A pack built with --system is named after its systems; the check
# looked for the full pack instead.
(["--platform", "retroarch", "--verify-packs"],
["--system", "sony-playstation"], "refuse"),
# --all-variants builds its own six combinations: it overrode these
# and the single-pack modes never read it at all.
(["--platform", "retroarch", "--all-variants"], ["--source", "truth"],
"refuse"),
(["--platform", "retroarch", "--all-variants"], ["--required-only"],
"refuse"),
(["--emulator", "duckstation"], ["--all-variants"], "refuse"),
(["--system", "sony-playstation"], ["--all-variants"], "refuse"),
(["--emulator", "duckstation", "--verify-packs"], ["--all-variants"],
"refuse"),
(["--all", "--manifest", "--all-variants"], ["--verify-packs"],
"refuse"),
(["--all", "--all-variants", "--verify-packs"], ["--one-per-slot"],
"refuse"),
(["--all", "--all-variants", "--verify-packs"], ["--split"],
"refuse"),
# Every flag a quick mode does not read, from one table.
(["--platform", "retroarch", "--verify-packs"], ["--split"], "refuse"),
(["--platform", "retroarch", "--verify-packs"], ["--manifest"], "refuse"),
(["--platform", "retroarch", "--verify-packs"], ["--emulator", "handy"],
"refuse"),
(["--manifest-targets"], ["--platform", "batocera"], "refuse"),
(["--manifest-targets"], ["--system", "sony-playstation"], "refuse"),
(["--manifest-targets"], ["--all"], "refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"], ["--manifest"],
"refuse"),
(["--list-systems"], ["--region", "us"], "refuse"),
(["--list-emulators"], ["--target", "switch"], "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_conformance_narrows_by_target_like_it_does_by_region(self):
"""A targeted pack is checked against the targeted expectation.
--target narrowed the build and nothing else: the conformance stage
held every targeted pack to the platform's whole system list and
core set, so a correct build exited non-zero on hundreds of files it
was never asked to carry. Region was already passed to the check;
target has to travel the same way, which is why the pack is not in
the skip list.
"""
import inspect
import packverify
from generate_pack import _narrows_contents, _narrowings
source = inspect.getsource(packverify.verify_pack_against_platform)
self.assertIn(
"target_cores",
inspect.signature(packverify.verify_pack_against_platform).parameters,
"the pack verifier takes no target, so it cannot narrow by one",
)
self.assertIn(
"filter_systems_by_target(",
source,
"the baseline expectation is not narrowed by the target",
)
self.assertIn(
"target_cores=target_cores",
source,
"the core-extras expectation is not narrowed by the target",
)
# And the pack must not be skipped instead: skipping checks nothing.
for target in ("nintendo-switch", "switch", "rpi4"):
tag = _narrowings("full", None, target, False, False)[0][0]
self.assertFalse(
_narrows_contents(f"Platform_1.0{tag}_BIOS_Pack.zip"),
f"a pack built for {target} would skip conformance entirely",
)
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"',
# The --system rename kept the region tag alone, the custom pack
# took no tag at all, and the skip list retyped four of them.
'_Custom_BIOS_Pack.zip"',
'{rgn_tag}_BIOS_Pack.zip"',
'("_Platform_", "_Truth_"',
):
self.assertNotIn(hand_rolled, source, hand_rolled)
self.assertGreaterEqual(source.count("_narrowings("), 8)
def test_a_narrowed_system_pack_does_not_take_the_full_name(self):
"""Every dimension has to show in the filename or two builds collide.
--system X --required-only produced the same name as --system X and
overwrote it, because the rename kept only the region tag.
"""
import subprocess
repo = os.path.join(os.path.dirname(__file__), "..")
names = {}
for label, extra in (("full", []), ("required", ["--required-only"])):
out = os.path.join(self.root, f"sysname_{label}")
proc = subprocess.run(
[sys.executable, "scripts/generate_pack.py", "--system",
"atari-lynx", "--offline", *extra, "--output-dir", out],
capture_output=True, text=True, cwd=repo, timeout=300,
)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
names[label] = sorted(
f for f in os.listdir(out) if f.endswith("_BIOS_Pack.zip")
)
self.assertTrue(names["full"] and names["required"])
self.assertNotEqual(
names["full"], names["required"],
"a required-only system pack took the name of the full one",
)
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
class StandaloneModeRequiresAStandaloneBuild(unittest.TestCase):
"""A platform names its standalone emulators by its own names, which can
equal the key of a libretro-only profile (Recalbox and Batocera both
call their standalone ScummVM ``scummvm``). Laid out standalone, such a
profile loses every ``path:`` and drops its files at the root."""
def test_libretro_only_profile_keeps_its_layout(self):
from common import runs_standalone
libretro_only = {"type": "libretro", "cores": ["scummvm"]}
dual = {"type": "standalone + libretro", "cores": ["dolphin"]}
standalone = {"type": "standalone", "cores": ["xemu"]}
named = {"scummvm", "dolphin", "xemu", "hatari"}
self.assertFalse(runs_standalone("scummvm", libretro_only, named))
self.assertTrue(runs_standalone("dolphin", dual, named))
self.assertTrue(runs_standalone("xemu", standalone, named))
self.assertTrue(runs_standalone("other-key", {"type": "standalone", "cores": ["hatari"]}, named))
self.assertFalse(runs_standalone("dolphin", dual, set()))
self.assertFalse(runs_standalone("dolphin", dual, {"pcsx2"}))
if __name__ == "__main__":
unittest.main()