chore: merge audit batch, verify and pack

This commit is contained in:
Abdessamad Derraz committed 2026-11-05 13:16:03 +01:00
commit 3f50163b0a
15 files changed
+748 -127

No files matched your search

+1 -1
View File
@@ -4830,7 +4830,7 @@ class VerifyAllSummaryNamesWhatIsWrong(unittest.TestCase):
summary = verify._overall_summary(
[(clean, ["RetroArch"]), (warned, ["RetroDECK"]), (core_miss, ["ROCKNIX"])]
)
self.assertIn("1 warning (RetroDECK)", summary)
self.assertIn("1 warning (RetroDECK 1)", summary)
self.assertIn("ROCKNIX: n64.psh", summary)
self.assertIn("0 critical,", summary)
+53
View File
@@ -87,6 +87,59 @@ class CoreFilesAsTheBuilderPlacesThem(unittest.TestCase):
)
class ARefusedPinLeavesItsPathToTheCore(unittest.TestCase):
"""EmuELEC pins naomi.zip by an md5 the collection does not hold: the
builder refuses the held copy and ships the core's file at that path.
The check marked the path taken by the refused platform file, so the
core file the pack carries there was never compared."""
def setUp(self):
tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self.addCleanup(tmp.cleanup)
previous = os.getcwd()
os.chdir(tmp.name)
self.addCleanup(os.chdir, previous)
files: dict = {}
by_name: dict = {}
_held(files, by_name, "bios/dc/naomi.zip", b"the core's naomi")
self.db = {"files": files, "indexes": {
"by_name": by_name, "by_md5": {e["md5"]: s for s, e in files.items()},
"by_path_suffix": {}, "by_crc32": {}, "by_sha256": {}}}
Path("platforms").mkdir()
config = {"platform": "P", "verification_mode": "md5", "cores": ["flycast"],
"systems": {"dc": {"files": [{
"name": "naomi.zip", "destination": "dc/naomi.zip",
"md5": hashlib.md5(b"a bundle nobody holds").hexdigest(),
}]}}}
Path("platforms/p.yml").write_text(yaml.safe_dump(config), encoding="utf-8")
Path("emulators").mkdir()
self.profiles = {"flycast": {
"emulator": "flycast", "type": "libretro", "systems": ["dc"], "files": [
{"name": "naomi.zip", "path": "dc/naomi.zip"}]}}
def _check(self, member: bytes) -> tuple:
with zipfile.ZipFile("pack.zip", "w") as zf:
zf.writestr("dc/naomi.zip", member)
return packverify.verify_pack_against_platform(
"pack.zip", "p", "platforms", self.db, emulators_dir="emulators",
emu_profiles=self.profiles,
)
def test_the_core_file_at_the_refused_path_is_compared(self):
_ok, _c, _p, errors, *_mid, core_checked, _cp, _ex, _ab = self._check(b"other")
self.assertEqual(core_checked, 1)
self.assertTrue(
any(e.startswith("core file differs: dc/naomi.zip") for e in errors), errors
)
def test_the_right_core_file_passes(self):
_ok, _c, _p, errors, *_mid, core_checked, core_present, _ex, _ab = (
self._check(b"the core's naomi")
)
self.assertEqual((core_checked, core_present), (1, 1))
self.assertFalse([e for e in errors if e.startswith("core")], errors)
class NarrowedPacksAgainstTheirOwnList(unittest.TestCase):
"""--verify-packs after --all-variants said "hashes checked during the
build" for the five narrowed packs and compared none of them: a narrowed
+18
View File
@@ -420,6 +420,24 @@ class TestArbitration(unittest.TestCase):
served = slots.format_decision(slots.arbitrate(self._conflict(), "existence"))
self.assertIn("both are satisfied", served)
def test_each_emulator_is_given_the_file_it_reads(self):
"""Recalbox's neocdz.zip line said geolith reads the archive fbneo
reads, while geolith pins another one."""
conflict = self._conflict()
conflict.profile_claims = [
slots.Claim("profile", "neocdz.zip", "neocdz.zip", emulator="fbneo",
local_path="a/neocdz.zip.ba20a7be", status="members_held"),
slots.Claim("profile", "neocdz.zip", "neocdz.zip", emulator="geolith",
local_path="b/neocdz.zip.712816a3", status="md5_exact"),
]
kept = slots.format_decision(slots.arbitrate(conflict, "md5"))
self.assertIn("a/neocdz.zip.ba20a7be, the file fbneo reads", kept)
self.assertIn("b/neocdz.zip.712816a3, the file geolith reads", kept)
self.assertEqual(conflict.files_read(), [
("a/neocdz.zip.ba20a7be", ["fbneo"], "members_held"),
("b/neocdz.zip.712816a3", ["geolith"], "md5_exact"),
])
class TheStrongestProofTakesAnUndeclaredPath(unittest.TestCase):
"""No platform declares RetroDECK's hiscore.dat. fbalpha2012 asks for it
+169 -2
View File
@@ -4,11 +4,13 @@
under the target's name (Recalbox, Lakka, BizHawk, MiSTer, ROCKNIX). The
conformance check read the same target without the --all tolerance, raised
FileNotFoundError on the first of those packs and stopped the run in a
traceback after every pack had been written.
traceback after every pack had been written. Such a platform is now left
out of a targeted --all run, and the check refuses it as the build does.
"""
from __future__ import annotations
import os
import sys
import contextlib
import io
@@ -31,7 +33,10 @@ class CheckReadsTheTargetLikeTheBuild(unittest.TestCase):
contextlib.redirect_stdout(io.StringIO()):
with self.assertRaises(ValueError):
build_target_cores_cache(["recalbox"], "x86_64", platforms, is_all=True)
self.assertIsNone(_target_cores_for("recalbox", "x86_64", platforms))
# The build never packs such a platform under the target, so the
# check refuses it too instead of reading the full pack.
with self.assertRaises(FileNotFoundError):
_target_cores_for("recalbox", "x86_64", platforms)
def test_a_platform_with_a_target_file_is_narrowed(self):
cores = _target_cores_for("retroarch", "switch", str(REPO_ROOT / "platforms"))
@@ -54,5 +59,167 @@ class ANameClaimsOnlyANarrowingThatHappened(unittest.TestCase):
)
class AMistypedTargetIsRefusedFirst(unittest.TestCase):
"""`--platform retroarch --target swich` took the dist lock and waited on
the data directory refresh, 22 s here and hours behind a pipeline, before
saying the target does not exist."""
def test_no_lock_and_no_refresh_before_the_refusal(self):
from unittest import mock
import generate_pack
def touched(*_a, **_kw):
raise AssertionError("lock or refresh taken before the target check")
argv = ["generate_pack.py", "--platform", "retroarch", "--target", "swich",
"--output-dir", str(REPO_ROOT / "tmp" / "never-written")]
err = io.StringIO()
with mock.patch.object(sys, "argv", argv), \
mock.patch.object(generate_pack, "_pack_output_lock", touched), \
mock.patch.object(generate_pack, "hold_data_dirs", touched), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(err), \
self.assertRaises(SystemExit) as ctx:
generate_pack.main()
self.assertEqual(ctx.exception.code, 1)
self.assertIn("Unknown target 'swich'", err.getvalue())
class ASystemTheTargetDropsIsNamedAsSuch(unittest.TestCase):
"""`--platform retroarch --system sony-playstation-2 --target switch`:
verify printed 0/0 with exit 0 under --strict, and generate_pack said
RetroArch has no such system and suggested sony-playstation."""
PLATFORMS = str(REPO_ROOT / "platforms")
def test_both_tools_read_one_scope(self):
from common import (
load_emulator_profiles, load_platform_config, platform_scope_systems,
)
config = load_platform_config("retroarch", self.PLATFORMS)
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
switch = _target_cores_for("retroarch", "switch", self.PLATFORMS)
wanted = ["sony-playstation-2"]
self.assertEqual(platform_scope_systems(config, profiles, switch, wanted), {})
self.assertEqual(list(platform_scope_systems(config, profiles, None, wanted)), wanted)
def test_the_message_names_the_target(self):
from common import system_scope_error
config = {"systems": {"sony-playstation-2": {}, "sony-playstation": {}}}
self.assertEqual(
system_scope_error(config, "retroarch", ["sony-playstation-2"], "switch"),
"no core retroarch runs on target switch emulates sony-playstation-2",
)
undeclared = system_scope_error(config, "retroarch", ["nec-pc-98"], "switch")
self.assertTrue(undeclared.startswith("retroarch has no system matching nec-pc-98."))
def test_verify_refuses_it(self):
import subprocess
proc = subprocess.run(
[sys.executable, "scripts/verify.py", "--platform", "retroarch",
"--system", "sony-playstation-2", "--target", "switch", "--strict"],
capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=600, check=False,
)
self.assertEqual(proc.returncode, 1, proc.stdout[-300:])
self.assertIn("no core retroarch runs on target switch emulates sony-playstation-2",
proc.stderr)
class ASplitBuildsOnlyTheTargetsSystems(unittest.TestCase):
"""`--split --target switch` asked for a part per declared system, and
each one the target drops ended on 'retroarch has no system matching'."""
def test_no_part_is_asked_for_a_dropped_system(self):
from unittest import mock
import generate_pack
from common import load_emulator_profiles
platforms = str(REPO_ROOT / "platforms")
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
asked: list[list[str]] = []
def record(*_a, **kw):
if kw.get("system_filter"):
asked.append(kw["system_filter"])
return None
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as out, \
mock.patch.object(generate_pack, "generate_pack", record), \
mock.patch.object(generate_pack, "_collect_emulator_extras",
return_value=[]):
generate_pack.generate_split_packs(
"retroarch", platforms, {"files": {}}, "bios", out,
emu_profiles=profiles,
target_cores=_target_cores_for("retroarch", "switch", platforms),
target_name="nintendo-switch",
)
systems = {sid for ids in asked for sid in ids}
self.assertTrue(systems)
self.assertNotIn("sony-playstation-2", systems)
self.assertIn("sony-playstation", systems)
class APlatformWithoutTheSystemIsOutOfScope(unittest.TestCase):
"""`--all --system sony-playstation-2` built the five PS2 packs and
exited 1 on 'no pack for misterfpga', which has no PS2."""
def _scope(self, **args):
import argparse
import generate_pack
from common import load_emulator_profiles
values = {"platforms_dir": str(REPO_ROOT / "platforms"), "target": None,
"all": True}
values.update(args)
out = io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(out):
kept = generate_pack._platforms_with_systems(
argparse.Namespace(**values), ["retroarch", "misterfpga"],
["sony-playstation-2"],
load_emulator_profiles(str(REPO_ROOT / "emulators")), {},
)
return kept, out.getvalue()
def test_all_skips_it(self):
kept, out = self._scope()
self.assertEqual(kept, ["retroarch"])
self.assertIn("INFO: Skipping misterfpga: no system sony-playstation-2", out)
def test_a_named_platform_is_refused(self):
with self.assertRaises(SystemExit) as ctx:
self._scope(all=False)
self.assertEqual(ctx.exception.code, 1)
class TheSplitDirectoryReadsThePackStem(unittest.TestCase):
"""The split prefix was assembled by hand: a version 'release/1.22' made
'RetroArch_release/1.22_Split', a nested directory the check never
found, beside a full pack named 'RetroArch_release-1.22_BIOS_Pack.zip'."""
def test_a_reserved_character_is_made_safe_as_in_the_full_pack(self):
from unittest import mock
import generate_pack
config = {"platform": "Foo: Bar", "version": "release/1.22", "systems": {}}
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as out, \
mock.patch.object(generate_pack, "load_platform_config",
return_value=config), \
mock.patch.object(generate_pack, "_collect_emulator_extras",
return_value=[]):
generate_pack.generate_split_packs(
"foo", "platforms", {"files": {}}, "bios", out, emu_profiles={},
)
stem = generate_pack._platform_pack_stem(["foo"], "foo", "platforms")
self.assertEqual(os.listdir(out), [f"{stem}_Split"])
self.assertEqual(stem, "Foo-_Bar_release-1.22")
if __name__ == "__main__":
unittest.main()
+116 -1
View File
@@ -132,7 +132,7 @@ class TheVerifySummaryCountsDiscrepancies(unittest.TestCase):
({"severity_counts": {}, "details": details}, ["RetroArch"]),
({"severity_counts": {}, "details": []}, ["Batocera"]),
])
self.assertIn("1 discrepancy (RetroArch)", summary)
self.assertIn("1 discrepancy (RetroArch 1)", summary)
def test_a_shared_destination_names_the_path_and_both_sides(self):
result = {"name": "disk.rom", "status": verify.Status.UNTESTED}
@@ -145,6 +145,121 @@ class TheVerifySummaryCountsDiscrepancies(unittest.TestCase):
self.assertIn(part, message)
class EveryPlatformInTheSummaryCarriesItsCount(unittest.TestCase):
"""'17 discrepancies (Lakka 5, RetroArch 5, RetroBat 2, RetroDECK 4,
ROCKNIX)': the one platform with a single entry had no number, and the
named counts did not add up to the total."""
def test_a_count_of_one_is_written(self):
one = {"severity_counts": {verify.Severity.WARNING: 1}, "details": []}
two = {"severity_counts": {verify.Severity.WARNING: 2}, "details": []}
summary = verify._overall_summary([(two, ["Lakka"]), (one, ["ROCKNIX"])])
self.assertIn("3 warning (Lakka 2, ROCKNIX 1)", summary)
class HeldButNotPackedNamesEachReason(unittest.TestCase):
"""'Core files held but not packed: 16 (a packed file occupies its
directory)' on Lakka, where 14 of the 16 are installed with their
emulator: the first entry's reason stood for the whole count."""
@staticmethod
def _entry(name: str, reason: str, category: str = "bios") -> dict:
return {"name": name, "emulator": "x", "required": True, "in_repo": True,
"in_pack": False, "omitted": reason, "category": category}
def _printed(self, entries: list[dict]) -> str:
out = io.StringIO()
with contextlib.redirect_stdout(out):
verify._print_undeclared_section({"undeclared_files": entries}, False)
return out.getvalue()
def test_each_reason_carries_its_count(self):
entries = [self._entry("a.rom", "a packed file occupies its directory")]
entries += [self._entry(f"{i}.rom", "installed with its emulator") for i in range(3)]
entries += [self._entry(f"g{i}", "installed with its emulator", "game_data")
for i in range(2)]
entries.append(self._entry("g9", "no platform slug for its system", "game_data"))
text = self._printed(entries)
self.assertIn(
"Core files held but not packed: 4 (3 installed with its emulator, "
"1 a packed file occupies its directory)", text)
self.assertIn(
"Game data held but not packed: 3 (2 installed with its emulator, "
"1 no platform slug for its system)", text)
def test_a_missing_file_follows_the_line_that_counts_it(self):
"""Indented under 'held but not packed: 102 (installed with its
emulator)', Batocera's one missing sfiii2a.zip read as one of them."""
missing = {"name": "sfiii2a.zip", "emulator": "FinalBurn Neo", "required": True,
"in_repo": False, "category": "bios"}
lines = self._printed(
[missing, self._entry("a.rom", "installed with its emulator")]
).splitlines()
self.assertTrue(lines[0].startswith(" Core files: "), lines)
self.assertIn("MISSING (required): FinalBurn Neo needs sfiii2a.zip", lines[1])
self.assertTrue(lines[2].startswith(" Core files held but not packed: "), lines)
def test_one_name_read_at_two_paths_prints_both_paths(self):
"""MyMinUI printed 'FinalBurn Neo needs sfiii2a.zip' twice, word for
word, for Bios/FBN/ and Bios/NG/."""
base = {"name": "sfiii2a.zip", "emulator": "FinalBurn Neo", "required": True,
"in_repo": False, "category": "bios"}
lone = dict(base, name="other.zip", path="Bios/FBN/other.zip")
text = self._printed([dict(base, path="Bios/FBN/sfiii2a.zip"),
dict(base, path="Bios/NG/sfiii2a.zip"), lone])
self.assertIn("FinalBurn Neo needs Bios/FBN/sfiii2a.zip", text)
self.assertIn("FinalBurn Neo needs Bios/NG/sfiii2a.zip", text)
self.assertIn("FinalBurn Neo needs other.zip\n", text)
def test_a_single_reason_reads_as_before(self):
text = self._printed([self._entry("a.rom", "installed with its emulator")] * 2)
self.assertIn("Core files held but not packed: 2 (installed with its emulator)", text)
class APlatformWithoutTargetsSaysSo(unittest.TestCase):
"""`--platform misterfpga --target switch` printed 'No target config for
platform 'misterfpga': platforms/targets/misterfpga.yml', which reads as
a file missing from the repository."""
def test_the_message_names_the_reason_and_the_platforms_with_targets(self):
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / "targets").mkdir()
for name in ("retroarch", "batocera", "_overrides"):
(Path(tmp) / "targets" / f"{name}.yml").write_text("targets: {}\n")
with self.assertRaises(FileNotFoundError) as caught:
common.load_target_config("misterfpga", "switch", tmp)
self.assertEqual(
str(caught.exception),
"misterfpga has no hardware targets: drop --target "
"(platforms with targets: batocera, retroarch)",
)
class OneNameListsThePlatforms(unittest.TestCase):
"""install.py --list-platforms, generate_pack.py --list, and no listing
at all in verify.py: three scripts, three answers to one question."""
def _run(self, script: str, flag: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, f"scripts/{script}", flag], capture_output=True, text=True,
cwd=str(REPO_ROOT), timeout=120, check=False,
)
def test_both_tools_take_it_and_agree(self):
verify_out = self._run("verify.py", "--list-platforms")
pack_out = self._run("generate_pack.py", "--list-platforms")
self.assertEqual(verify_out.returncode, 0, verify_out.stderr)
self.assertEqual(pack_out.returncode, 0, pack_out.stderr)
self.assertEqual(verify_out.stdout, pack_out.stdout)
self.assertIn("retroarch", verify_out.stdout.split())
self.assertIn("--list-platforms", (REPO_ROOT / "install.py").read_text())
def test_the_installer_name_is_the_one_documented(self):
tools = (REPO_ROOT / "wiki" / "tools.md").read_text()
self.assertIn("generate_pack.py --list-platforms", tools)
self.assertIn("verify.py --list-platforms", tools)
class OneSpellingPerOption(unittest.TestCase):
"""verify.py -p batocera worked and slots.py -p batocera was refused."""
+18
View File
@@ -116,6 +116,24 @@ class VerifyPackChoice(unittest.TestCase):
dict(zip((name for _p, name in checked), self.variants)), names
)
def test_a_target_the_platform_lacks_is_refused(self):
"""`--platform bizhawk --target rpi4typo --verify-packs` verified the
full pack and said `all OK`, where the build refused the target."""
import contextlib
import io
bizhawk = self.gp._platform_pack_stem(["bizhawk"], "bizhawk", "platforms")
self._touch(f"{bizhawk}_BIOS_Pack.zip")
self._touch(f"{self.stem}_BIOS_Pack.zip")
for platform, target in (("bizhawk", "rpi4typo"), ("retroarch", "swich")):
with self.subTest(platform=platform), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()), \
self.assertRaises(SystemExit) as ctx:
self._run(self._args(platform=platform, target=target))
self.assertEqual(ctx.exception.code, 1)
self.assertEqual(self.variants, [])
def test_the_run_ends_on_a_summary(self):
"""A failing pack was named only above its own error lines, and the
log ended on the last platform's OK line."""
+59
View File
@@ -89,6 +89,12 @@ class ReleaseAssetNotCached(unittest.TestCase):
self.assertEqual(entry["status"], Status.OK)
self.assertEqual(entry["release_asset"], REGISTERED[0])
def test_its_path_is_a_pack_destination(self):
"""The pack holds the fetched file at its path: absent from
pack_destinations, step 5 reported it 'pack only'."""
report = verify_platform(self.config, self.db, "emulators", {})
self.assertIn("big.bin", report["pack_destinations"])
def test_offline_counts_it_missing(self):
packresolve.set_offline(True)
self.assertEqual(self._entry()["status"], Status.MISSING)
@@ -117,6 +123,59 @@ class ReleaseAssetNotCachedMd5(ReleaseAssetNotCached):
mode = "md5"
class CoreExtraReleaseAssetNotCached(ReleaseAssetNotCached):
"""The same asset read by a core and declared by no platform entry: the
builder fetched and packed it online, verify's replay of the extras
resolved it offline and left its path out of the pack."""
def setUp(self):
super().setUp()
self.config["systems"]["sys"]["files"] = []
self.config["cores"] = ["bigcore"]
Path("platforms/big.yml").write_text(yaml.dump(self.config))
self.profiles = {"bigcore": {
"emulator": "Big Core", "type": "libretro", "systems": ["sys"],
"files": [{"name": "big.bin", "required": True, "sha1": SHA1}],
}}
Path("emulators/bigcore.yml").write_text(yaml.dump(self.profiles["bigcore"]))
common._platform_config_cache.clear()
common._emulator_profiles_cache.clear()
def _report(self) -> dict:
return verify_platform(self.config, self.db, "emulators", self.profiles)
def test_counted_as_fetched_when_the_pack_is_built(self):
report = self._report()
self.assertIn("big.bin", report["pack_destinations"])
self.assertEqual(report["packed_extras"]["bios"], 1)
def test_its_path_is_a_pack_destination(self):
cache = Path("cache/big.bin")
cache.parent.mkdir()
cache.write_bytes(PAYLOAD)
with mock.patch.object(
packresolve, "fetch_large_file", return_value=str(cache)
):
zip_path = builder.generate_pack(
"big", "platforms", self.db, "bios", "out",
emulators_dir="emulators", emu_profiles=self.profiles, offline=False,
)
with zipfile.ZipFile(zip_path) as zf:
packed = {n for n in zf.namelist() if not n.endswith("/")}
self.assertIn("big.bin", packed)
self.assertIn("big.bin", self._report()["pack_destinations"])
def test_offline_counts_it_missing(self):
packresolve.set_offline(True)
self.assertNotIn("big.bin", self._report()["pack_destinations"])
def test_an_asset_with_other_content_is_not_counted(self):
self.profiles["bigcore"]["files"][0]["sha1"] = "0" * 40
self.assertNotIn("big.bin", self._report()["pack_destinations"])
test_the_online_build_packs_it = None
class OfflinePipelineVerifiesOffline(unittest.TestCase):
"""The offline pipeline builds packs without release assets: its verify
step has to count them the same way, or step 5 compares two answers."""