diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index f82966b5..b9aaa16e 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -45,7 +45,13 @@ from common import ( select_emulator_profiles, ) from hashing import parse_hash_list -from validation import case_folded_slots, declaration_settles, outside_gap_scope +from validation import ( + case_folded_slots, + declaration_settles, + installed_with_emulator, + outside_gap_scope, + unread_on, +) yaml = require_yaml() @@ -606,9 +612,15 @@ def _cross_reference_profile( report: dict, index: dict, standalone_cores: set[str] | None = None, + platform: str = "", ) -> None: """Compare one emulator profile against what the platforms declare. + *platform* is the platform key under --platform, empty for the whole + tree. It decides, through the functions verify reads, which entries the + platform's launcher never opens and which ones its emulator package + already installs. + Split out of cross_reference, where it was a 200-line loop body that carried the whole function's branching: every file is resolved by name, by path, by hash and by archive membership before it can be called a @@ -667,7 +679,7 @@ def _cross_reference_profile( if len(layouts) > 1: # One file read at one path by both builds counts once. seen_key = (effective_path.casefold(), seen_key) - if not fname or seen_key in seen_files: + if not fname or seen_key in seen_files or unread_on(f, platform): continue # Collect unsourceable files separately (documented, not a gap) @@ -760,7 +772,7 @@ def _cross_reference_profile( "name": fname, "path": effective_path, "required": f.get("required", False), - "bundled": bool(f.get("bundled")), + "bundled": installed_with_emulator(f, platform), "note": f.get("note", ""), "source_ref": f.get("source_ref", ""), "in_platform": False, @@ -800,6 +812,7 @@ def cross_reference( all_declared: dict[str, list[tuple[int | None, str]]] | None = None, standalone_cores: set[str] | None = None, folded_slots: frozenset[str] = frozenset(), + platform: str = "", ) -> dict: """Compare emulator profiles against platform declarations. @@ -841,7 +854,8 @@ def cross_reference( } for emu_name, profile in profiles.items(): _cross_reference_profile( - emu_name, profile, declared, report, index, standalone_cores + emu_name, profile, declared, report, index, standalone_cores, + platform, ) return report @@ -872,14 +886,14 @@ def print_report( if gaps == 0 and not show_clean: continue - # Counted once, for the heading and the total alike: a file bundled - # with its emulator is not one to source. - emulator_bundled = sum( - 1 for g in data["gap_details"] - if g.get("bundled") and g.get("source") == "missing" - ) + # Counted once, for the heading and the total alike: a file installed + # with its emulator is neither one to source nor one the pack brings, + # held or not (validation.deliverable leaves it out of every pack). + bundled_gaps = [g for g in data["gap_details"] if g.get("bundled")] + emulator_bundled = len(bundled_gaps) counts = {key: data.get(f"gap_{key}", 0) for key in totals} - counts["missing"] -= emulator_bundled + for g in bundled_gaps: + counts[g.get("source", "missing")] -= 1 counts["bundled"] = emulator_bundled parts = [f"{count} {key}" for key, count in counts.items() if count] status = ", ".join(parts) if parts else "OK" @@ -896,7 +910,7 @@ def print_report( for g in data["gap_details"]: req = "*" if g["required"] else " " src = g.get("source", "missing").upper() - if g.get("bundled") and g.get("source") == "missing": + if g.get("bundled"): src = "BUNDLED" # One line per file: a note spanning lines fell to column 0 # between entries and read as an emulator heading. @@ -920,7 +934,7 @@ def print_report( f"large_file: {totals['large_file']})") print(f" {totals['missing']} missing (need to be sourced)") if bundled: - print(f" {bundled} bundled with their emulator, not collected") + print(f" {bundled} bundled with their emulator, not packed") def main() -> None: @@ -1023,6 +1037,7 @@ def main() -> None: all_declared=declared_names(args.platforms_dir, selected, db), standalone_cores=standalone_cores, folded_slots=folded_declared_slots(args.platforms_dir, selected, db), + platform=config.get("_key", args.platform) if args.platform else "", ) if args.json: diff --git a/tests/test_cross_reference_sources.py b/tests/test_cross_reference_sources.py index 40a6fa91..67de9cbf 100644 --- a/tests/test_cross_reference_sources.py +++ b/tests/test_cross_reference_sources.py @@ -110,5 +110,60 @@ class GapReportSources(unittest.TestCase): ) +class PlatformScopedEntries(unittest.TestCase): + """Under --platform the report reads the platform's scoping fields + through the functions verify and the builder call: blastem's cdbios.bin + is never opened by RetroBat (unread_on), touchHLE's fonts are not in + ROCKNIX's package (not_bundled_on), fMSX's fonts come with Recalbox's + (installed_on).""" + + PROFILES = {"emu": {"emulator": "Emu", "systems": ["sys"], "files": [ + {"name": "skipped.bin", "required": True, "unread_on": ["plat"]}, + {"name": "font.ttf", "required": True, "bundled": True, + "not_bundled_on": ["plat"]}, + {"name": "held.fnt", "installed_on": ["plat"]}, + ]}} + DB = {"files": {"a" * 40: {"path": "bios/held.fnt", "name": "held.fnt"}}, + "indexes": {"by_name": {"held.fnt": ["a" * 40]}, "by_md5": {}, + "by_crc32": {}, "by_sha256": {}, "by_path_suffix": {}}} + + def _gaps(self, platform: str) -> dict[str, dict]: + report = cross_reference(self.PROFILES, {}, self.DB, platform=platform) + return {g["name"]: g for g in report["emu"]["gap_details"]} + + def test_an_entry_the_platform_never_opens_is_not_a_gap(self): + self.assertNotIn("skipped.bin", self._gaps("plat")) + self.assertIn("skipped.bin", self._gaps("other")) + self.assertIn("skipped.bin", self._gaps("")) + + def test_bundled_follows_the_platform_package(self): + gaps = self._gaps("plat") + self.assertFalse(gaps["font.ttf"]["bundled"]) + self.assertTrue(gaps["held.fnt"]["bundled"]) + gaps = self._gaps("other") + self.assertTrue(gaps["font.ttf"]["bundled"]) + self.assertFalse(gaps["held.fnt"]["bundled"]) + + def test_a_held_bundled_file_is_not_announced_as_available(self): + import contextlib + import io + + report = cross_reference(self.PROFILES, {}, self.DB, platform="plat") + with contextlib.redirect_stdout(io.StringIO()) as out: + print_report(report) + text = out.getvalue() + self.assertIn("held.fnt [BUNDLED]", text) + self.assertIn("0 available", text) + self.assertIn("1 missing (need to be sourced)", text) + + def test_the_report_reads_the_shared_rules(self): + """One rule, one function: a local `bool(f.get("bundled"))` answered + differently from verify for every installed_on/not_bundled_on entry.""" + source = (REPO_ROOT / "scripts" / "cross_reference.py").read_text() + self.assertNotIn('bool(f.get("bundled"))', source) + self.assertIn("installed_with_emulator(f, platform)", source) + self.assertIn("unread_on(f, platform)", source) + + if __name__ == "__main__": unittest.main()