Files
libretro/tests/test_pack_destinations.py
T
Abdessamad Derraz 4ba935374e fix: keep pack order outside contested paths
Ranking contested core claims sorted the whole gap report, which
reordered every manifest. Only the claims on one contested path move now,
grouped where the first of them stood, and two spellings of a path are
one group where the filesystem folds case (RetroBat's 64DD_IPL_DEV.bin).

An entry without a destination (EmuDeck's system:md5 whitelist) no
longer counts as a pack path in verify, and step 5 prints one status for
counts and paths together. A full offline run compared all 12 archives
and 930 emulator packs path by path, every one equal.
2026-10-21 06:47:44 +02:00

196 lines
7.7 KiB
Python

"""verify and the pack name the same paths, not only the same number of them.
The pipeline compared counts. RetroArch declares SGB1.sfc and, below it,
SGB1.sfc/program.rom: the pack writes the file and skips the path it blocks,
verify credited both, and both sides still read 526/526. A user_provided
declaration keeps its slot in the pack, while verify let a core file take
it and counted one more file from the cores.
"""
from __future__ import annotations
import contextlib
import hashlib
import io
import json
import os
import shutil
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
class _Fixture(unittest.TestCase):
def setUp(self) -> None:
from common import _emulator_profiles_cache
_emulator_profiles_cache.clear()
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
self.bios = self.root / "bios"
self.platforms = self.root / "platforms"
self.emulators = self.root / "emulators"
for directory in (self.bios, self.platforms, self.emulators):
directory.mkdir()
files, by_name, by_md5 = {}, {}, {}
for name, data in (("sgb.sfc", b"SGB" * 8), ("program.rom", b"PRG" * 8),
("core.bin", b"CORE" * 8)):
path = self.bios / name
path.write_bytes(data)
sha1, md5 = hashlib.sha1(data).hexdigest(), hashlib.md5(data).hexdigest()
files[sha1] = {"path": str(path), "name": name, "md5": md5, "size": len(data)}
by_name[name] = [sha1]
by_md5[md5] = sha1
self.db = {"files": files, "indexes": {"by_name": by_name, "by_md5": by_md5,
"by_crc32": {}, "by_path_suffix": {}}}
def _platform(self, files: list[dict], cores: list[str] | None = None) -> dict:
config = {"platform": "Paths", "verification_mode": "existence",
"base_destination": "system", "systems": {"sys": {"files": files}}}
if cores is not None:
config["cores"] = cores
(self.platforms / "paths.yml").write_text(yaml.safe_dump(config))
return config
def _both(self) -> tuple[dict, set[str]]:
from common import load_emulator_profiles, load_platform_config
from generate_pack import generate_pack
from verify import verify_platform
profiles = load_emulator_profiles(str(self.emulators))
report = verify_platform(
load_platform_config("paths", str(self.platforms)), self.db,
str(self.emulators), profiles,
)
with contextlib.redirect_stdout(io.StringIO()):
zip_path = generate_pack(
"paths", str(self.platforms), self.db, str(self.bios),
str(self.root / "out"), emulators_dir=str(self.emulators),
emu_profiles=profiles, offline=True,
)
with zipfile.ZipFile(zip_path) as zf:
members = {n for n in zf.namelist() if n != "README.txt"}
return report, members
class APathBlockedByAFile(_Fixture):
def test_verify_credits_only_the_file_the_pack_writes(self):
self._platform([
{"name": "sgb.sfc", "destination": "SGB1.sfc"},
{"name": "program.rom", "destination": "SGB1.sfc/program.rom"},
])
report, members = self._both()
self.assertEqual(members, {"SGB1.sfc"})
self.assertEqual(set(report["pack_destinations"]), members)
class AUserProvidedSlot(_Fixture):
def test_no_core_file_takes_it_on_either_side(self):
(self.emulators / "coreemu.yml").write_text(yaml.safe_dump({
"emulator": "CoreEmu", "type": "libretro", "systems": ["sys"],
"files": [{"name": "core.bin", "required": True}],
}))
self._platform(
[{"name": "manual.bin", "destination": "core.bin", "storage": "user_provided"}],
cores=["coreemu"],
)
report, members = self._both()
self.assertEqual(members, {"INSTRUCTIONS_manual.bin.txt"})
self.assertEqual(report["packed_extras"], {"bios": 0, "game_data": 0})
self.assertEqual(report["pack_destinations"], [])
class AnEntryWithoutAFileTakesNoPath(unittest.TestCase):
"""EmuDeck's system:md5 entries name no file. verify credited the bare
base destination as a pack path, and the comparison failed on ''."""
def test_it_is_left_out(self):
from verify import _builder_placement
shipped, occupied = _builder_placement(
[("s", "bios/", None), ("s", "", "x"), ("s", "bios/a.bin", "bios/a.bin")], False
)
self.assertEqual(shipped, {"bios/a.bin": "bios/a.bin"})
self.assertEqual(set(occupied), {"bios/a.bin"})
class ThePipelineComparesPaths(unittest.TestCase):
"""Equal counts, other paths: the comparison fails and names both sides."""
def test_a_swap_at_equal_counts_fails(self):
import pipeline
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, root, True)
zip_path = root / "Plat_BIOS_Pack.zip"
with zipfile.ZipFile(zip_path, "w") as zf:
for name in ("a.bin", "b.bin", "README.txt", "INSTRUCTIONS_x.bin.txt"):
zf.writestr(name, name)
output = f"Generating pack for plat [source=full]...\n {zip_path}: 2 files packed\n"
for credited, expected in ((["a.bin", "b.bin"], True), (["a.bin", "c.bin"], False)):
with self.subTest(credited=credited):
destinations = root / "d.json"
destinations.write_text(json.dumps(
{"Plat": {"platform": "plat", "pack_destinations": credited}}
))
previous = os.getcwd()
os.chdir(REPO_ROOT)
try:
with mock_tree_members(), contextlib.redirect_stdout(io.StringIO()) as out:
ok = pipeline.check_pack_destinations(str(destinations), output)
finally:
os.chdir(previous)
self.assertIs(ok, expected, out.getvalue())
if not expected:
self.assertIn("verify only 1: c.bin", out.getvalue())
self.assertIn("pack only 1: b.bin", out.getvalue())
def test_the_run_compares_every_emulator_pack(self):
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
self.assertIn("ok = check_pack_destinations(destinations_file, pack_output) and ok", source)
self.assertIn("ok = check_emulator_packs() and ok", source)
self.assertIn('"--destinations", destinations_file', source)
@contextlib.contextmanager
def mock_tree_members():
import pipeline
original = pipeline._tree_members
pipeline._tree_members = lambda *_args: {}
try:
yield
finally:
pipeline._tree_members = original
class EveryEmulatorPackMatchesItsReport(unittest.TestCase):
"""Reads the repository: every profile, both builds, built in memory."""
def test_the_paths_agree(self):
if not (REPO_ROOT / "database.json").is_file():
self.skipTest("database.json is not built")
import pipeline
previous = os.getcwd()
os.chdir(REPO_ROOT)
try:
with contextlib.redirect_stdout(io.StringIO()) as out:
ok = pipeline.check_emulator_packs()
finally:
os.chdir(previous)
self.assertTrue(ok, out.getvalue())
if __name__ == "__main__":
unittest.main()