diff --git a/README.md b/README.md index f88e300e..c09f716e 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Full list with per-file details: **[https://abdess.github.io/retrobios/](https:/ | Recalbox | 351/351 | 2,788/2,806 | MD5 hash | | RetroArch | 526/526 | 5,277/5,309 | file presence | | RetroBat | 348/348 | 7,255/7,417 | MD5 hash | -| RetroDECK | 2,008/2,008 | 5,323/5,377 | MD5 hash | +| RetroDECK | 2,007/2,008 | 5,323/5,377 | MD5 hash | | RetroPie * | 532/532 | 9,195/9,339 | file presence | | RomM | 383/383 | 61/788 | MD5 hash | @@ -180,4 +180,4 @@ The scripts and tooling are released under the [MIT License](LICENSE). The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed. The reasoning, and where it is weakest, is in the [FAQ](https://abdess.github.io/retrobios/wiki/faq/#is-this-legal). -*Auto-generated on 2026-10-19T05:42:39Z* +*Auto-generated on 2026-10-19T20:59:35Z* diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index c419b8dd..cabf8aed 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -60,7 +60,9 @@ def compute_coverage( untested = sc.get("untested", 0) missing = sc.get("missing", 0) total = result["total_files"] - present = ok + untested + # An UNTESTED file the builder refuses (its declared hash contradicted on + # a hash platform) is held, not packed. + present = ok + untested - len(result.get("excluded_files", [])) pct = (present / total * 100) if total > 0 else 0 undeclared = result.get("undeclared_files", []) # Held is not packed: a file the platform has no folder for, or one below diff --git a/scripts/verify.py b/scripts/verify.py index 9c8c6c9a..7c32a9e9 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1045,6 +1045,7 @@ def verify_platform( ) shipped: dict[str, str] = {} + shipped_keys: set[str] = set() results_at: dict[str, list[dict]] = {} for sys_id, system in verify_systems.items(): for file_entry in system.get("files", []): @@ -1111,6 +1112,7 @@ def verify_platform( f"{base_dest}/{destination}" if base_dest else destination, local_path, ) + shipped_keys.add(_dest_key(file_entry, sys_id)) if _twin_unmet(result, held_by): continue @@ -1191,6 +1193,14 @@ def verify_platform( for s in file_severity.values(): counts[s] = counts.get(s, 0) + 1 + # Held but refused: on a hash platform the builder leaves out a file whose + # declared hash the local copy contradicts. Counted apart, so a coverage + # figure never calls present what the pack does not carry. + excluded = sorted( + dest for dest, status in file_status.items() + if status == Status.UNTESTED and dest not in shipped_keys + ) + # Count by file status (ok/untested/missing) status_counts: dict[str, int] = {} for s in file_status.values(): @@ -1229,6 +1239,7 @@ def verify_platform( "total_files": total, "severity_counts": counts, "status_counts": status_counts, + "excluded_files": excluded, "undeclared_files": undeclared, "exclusion_notes": exclusions, "region_fallbacks": sorted(region_fallbacks), diff --git a/tests/test_pack_counts.py b/tests/test_pack_counts.py index 78b1615c..7158cd01 100644 --- a/tests/test_pack_counts.py +++ b/tests/test_pack_counts.py @@ -425,5 +425,54 @@ class TheCollectionTotalNamesTheCollection(unittest.TestCase): self.assertEqual(generate_site.COLLECTION_FILES_LABEL, "Files collected") +class RefusedFilesAreNotPresent(unittest.TestCase): + """RetroDECK's dsifirmware.bin is held, but its declared hash contradicts + the local copy, so the md5 builder leaves it out. The README still + printed 2,008/2,008 for a pack that carries 2,007.""" + + def test_the_coverage_subtracts_a_refused_file(self): + import os + + import common + import generate_db + import generate_readme + + with tempfile.TemporaryDirectory() as tmp: + previous = os.getcwd() + os.chdir(tmp) + self.addCleanup(os.chdir, previous) + for d in ("bios/X", "platforms", "emulators"): + Path(d).mkdir(parents=True) + files = {} + for name in ("good.bin", "wrong.bin"): + payload = name.encode() + Path(f"bios/X/{name}").write_bytes(payload) + sha1 = hashlib.sha1(payload).hexdigest() + files[sha1] = { + "path": f"bios/X/{name}", "name": name, "size": len(payload), + "sha1": sha1, "md5": hashlib.md5(payload).hexdigest(), + "sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000001", + } + db = {"files": files, "indexes": generate_db.build_indexes(files, {})} + config = { + "platform": "Hashy", "verification_mode": "md5", "base_destination": "bios", + "systems": {"sys": {"files": [ + {"name": "good.bin", "destination": "good.bin", + "md5": hashlib.md5(b"good.bin").hexdigest()}, + {"name": "wrong.bin", "destination": "wrong.bin", "md5": "f" * 32}, + ]}}, + } + Path("platforms/hashy.yml").write_text(yaml.dump(config)) + Path("platforms/_registry.yml").write_text( + yaml.dump({"platforms": {"hashy": {"status": "active"}}}) + ) + common._platform_config_cache.clear() + self.addCleanup(common._platform_config_cache.clear) + coverage = generate_readme.compute_coverage( + "hashy", "platforms", db, emulators_dir="emulators" + ) + self.assertEqual((coverage["present"], coverage["total"]), (1, 2)) + + if __name__ == "__main__": unittest.main()