diff --git a/scripts/pipeline.py b/scripts/pipeline.py index a7614af82..02e195a52 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -523,15 +523,43 @@ def check_pack_destinations( return all_ok +# The narrowings an emulator or system pack takes, compared by +# check_emulator_packs. A test holds this to the builder's parameters. +EMULATOR_NARROWINGS_COMPARED = frozenset({"standalone", "regions", "required_only"}) + + +def emulator_variants(profiles: list[dict], full: bool) -> list[tuple[str, dict]]: + """(label, builder arguments) of each narrowed emulator pack compared. + + --required-only always, and each region the profiles' files declare, + sampled as check_narrowed_packs samples them unless *full*. A profile + without a regional file has nothing --region could withdraw. + """ + import region as region_mod # noqa: PLC0415 + + regions = sorted({ + r for profile in profiles for fe in profile.get("files") or [] + for r in region_mod.normalize_declared(fe.get("region")) + }) + variants: list[tuple[str, dict]] = [("", {}), (" --required-only", {"required_only": True})] + variants += [ + (f" --region {regions[i]}", {"regions": [regions[i]]}) + for i in _matrix_coverage(full, len(regions)) + ] + return variants + + def check_emulator_packs( emulators_dir: str = "emulators", platforms_dir: str = "platforms", - db_path: str = "database.json", + db_path: str = "database.json", full: bool = False, ) -> bool: """Build every emulator and system pack in memory and compare it with verify. - Each profile in each build it has (libretro, standalone): the paths the - pack fills with the profile's files against the ones verify credits. No - archive is written and no file read, so all of them take seconds. + Each profile in each build it has (libretro, standalone), full and under + each narrowing of emulator_variants: the paths the pack fills with the + profile's files against the ones verify credits. The region and + required-only rules are written twice, once per side, and only this + comparison holds them together. No archive is written and no file read. """ import generate_pack # noqa: PLC0415 import verify # noqa: PLC0415 @@ -549,6 +577,26 @@ def check_emulator_packs( db = load_database(db_path) zip_contents = build_zip_contents_index(db) profiles = load_emulator_profiles(emulators_dir, skip_aliases=False) + + def compare(names: list[str], standalone: bool, label: str, **narrowing) -> bool: + record = generate_pack.PackRecord() + with contextlib.redirect_stdout(io.StringIO()), \ + contextlib.redirect_stderr(io.StringIO()): + generate_pack.generate_emulator_pack( + names, emulators_dir, db, "bios", "", standalone, zip_contents, + offline=True, platforms_dir=platforms_dir, record=record, **narrowing, + ) + report = verify.verify_emulator( + names, emulators_dir, db, standalone, platforms_dir=platforms_dir, + **narrowing, + ) + lines = _set_difference_lines( + label, set(report["pack_destinations"]), set(record.files) + ) + if lines: + print("\n".join(lines)) + return not lines + all_ok = True compared = 0 for name in sorted(profiles): @@ -559,23 +607,10 @@ def check_emulator_packs( select_emulator_profiles([name], profiles, standalone) except ProfileSelectionError: continue - record = generate_pack.PackRecord() - with contextlib.redirect_stdout(io.StringIO()): - generate_pack.generate_emulator_pack( - [name], emulators_dir, db, "bios", "", standalone, zip_contents, - offline=True, platforms_dir=platforms_dir, record=record, - ) - report = verify.verify_emulator( - [name], emulators_dir, db, standalone, platforms_dir=platforms_dir, - ) - compared += 1 - label = f"{name}{' --standalone' if standalone else ''}" - lines = _set_difference_lines( - label, set(report["pack_destinations"]), set(record.files) - ) - if lines: - print("\n".join(lines)) - all_ok = False + for tag, narrowing in emulator_variants([profiles[name]], full): + compared += 1 + label = f"{name}{' --standalone' if standalone else ''}{tag}" + all_ok = compare([name], standalone, label, **narrowing) and all_ok print(f" {compared} emulator packs compared, {'all equal' if all_ok else 'differences above'}") # A system pack builds every profile of the system together: a placement # rule that only goes wrong when two profiles share a path (the bluemsx @@ -593,34 +628,24 @@ def check_emulator_packs( matching = profiles_for_systems(profiles, [sid], standalone) if len(matching) < 2: continue - record = generate_pack.PackRecord() - with contextlib.redirect_stdout(io.StringIO()), \ - contextlib.redirect_stderr(io.StringIO()): - generate_pack.generate_emulator_pack( - matching, emulators_dir, db, "bios", "", standalone, zip_contents, - offline=True, platforms_dir=platforms_dir, record=record, - system_ids=[sid], - ) - report = verify.verify_emulator( - matching, emulators_dir, db, standalone, platforms_dir=platforms_dir, - system_ids=[sid], - ) - compared += 1 - label = f"--system {sid}{' --standalone' if standalone else ''}" - lines = _set_difference_lines( - label, set(report["pack_destinations"]), set(record.files) - ) - if lines: - print("\n".join(lines)) - systems_ok = False + for tag, narrowing in emulator_variants( + [profiles[n] for n in matching if n in profiles], full + ): + compared += 1 + label = f"--system {sid}{' --standalone' if standalone else ''}{tag}" + systems_ok = compare( + matching, standalone, label, system_ids=[sid], **narrowing + ) and systems_ok print(f" {compared} system packs compared, " f"{'all equal' if systems_ok else 'differences above'}") return all_ok and systems_ok -# How each _narrowings() dimension is compared by check_narrowed_packs, or why -# it is not. A test reads the builder's signature against these two tables, so -# a new narrowing fails it until it is compared or a reason is written here. +# How each _narrowings() dimension of a platform pack is compared by +# check_narrowed_packs, or why it is not. A test reads the builder's +# signature against these two tables, so a new narrowing fails it until it +# is compared or a reason is written here. Emulator packs have their own +# table, EMULATOR_NARROWINGS_COMPARED. NARROWINGS_COMPARED = frozenset({"regions", "target_name", "required_only", "no_game_data"}) NARROWINGS_NOT_COMPARED = { "standalone": "an emulator pack mode, compared by check_emulator_packs", @@ -964,7 +989,8 @@ def main() -> None: parser.add_argument( "--full-narrowings", action="store_true", help="Step 5 compares every region, target and system of each " - "platform, not a sample of three (about 25 minutes)", + "platform, and every region of each emulator and system pack, not a " + "sample of three (about 25 minutes)", ) parser.add_argument( "--check-buildbot", @@ -1335,7 +1361,7 @@ def main() -> None: ) # Counts can agree while the paths differ, both ways at once. ok = check_pack_destinations(destinations_file, pack_output) and ok - ok = check_emulator_packs() and ok + ok = check_emulator_packs(full=args.full_narrowings) and ok ok = check_narrowed_packs( include_archived=args.include_archived, full=args.full_narrowings, ) and ok diff --git a/tests/test_pack_destinations.py b/tests/test_pack_destinations.py index 6d15a718f..65bd1277d 100644 --- a/tests/test_pack_destinations.py +++ b/tests/test_pack_destinations.py @@ -182,6 +182,28 @@ class EveryNarrowingIsCompared(unittest.TestCase): "compare the new narrowing in check_narrowed_packs, " "or write in NARROWINGS_NOT_COMPARED why it is not") + def test_each_emulator_pack_narrowing_is_compared(self): + """check_emulator_packs built every pack without --region or + --required-only, while the table above counted both as compared: + it describes the platform packs only.""" + import inspect + + import pipeline + from generate_pack import _narrowings, generate_emulator_pack + + taken = (set(inspect.signature(_narrowings).parameters) + & set(inspect.signature(generate_emulator_pack).parameters)) + self.assertEqual(taken, set(pipeline.EMULATOR_NARROWINGS_COMPARED)) + profile = {"files": [{"name": "a.bin", "region": "japan"}, + {"name": "b.bin", "region": ["north-america", "europe"]}]} + used = {key for _label, narrowing in pipeline.emulator_variants([profile], False) + for key in narrowing} + # standalone is the loop around the variants, both builds of each profile. + self.assertEqual(used, pipeline.EMULATOR_NARROWINGS_COMPARED - {"standalone"}) + regions = [n["regions"] for _l, n in pipeline.emulator_variants([profile], True) + if "regions" in n] + self.assertEqual(len(regions), 3) + def test_the_variants_reach_every_compared_dimension(self): import pipeline from common import load_emulator_profiles, load_platform_config @@ -246,7 +268,7 @@ class ThePipelineComparesPaths(unittest.TestCase): 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("ok = check_emulator_packs(full=args.full_narrowings) and ok", source) self.assertIn('"--destinations", destinations_file', source) diff --git a/wiki/tools.md b/wiki/tools.md index aa9e695a4..5c2dccbc0 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -95,6 +95,10 @@ alone, run `python scripts/check_buildbot_system.py`. ones verify credits (`--destinations`), every emulator pack, each profile in each build, built in memory against `verify --emulator`, and every system pack that gathers several profiles against `verify --system`. + Each of them is also built with `--required-only` and with the regions + its files declare (first, middle and last, all of them with + `--full-narrowings`). 2,112 emulator and 1,493 system packs took about + two minutes on 2026-11-06. Equal counts once hid four RetroArch paths credited and never written. - **Each narrowed platform pack is compared too.** Step 5 builds in memory the `--required-only` and `--no-game-data` pack of every platform, and its