diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 1cbf45d9..bdc8f794 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -1270,11 +1270,16 @@ def _render_platform_systems( lines.append(f"{pad}Emulators: {emu_links}") lines.append("") - # File listing + # File listing. The sort is stable, so same-named entries keep the + # order verify reported them in, which is the declaration order. + seen_names: dict[str, int] = {} for f in sorted(files, key=lambda x: x["name"]): status = f["status"] fname = f["name"] - cfg_entry = config_files.get(fname, {}) + declared = config_files.get((sys_id, fname), []) + nth = seen_names.get(fname, 0) + seen_names[fname] = nth + 1 + cfg_entry = declared[nth] if nth < len(declared) else {} sha1 = cfg_entry.get("sha1", f.get("sha1", "")) md5 = cfg_entry.get("md5", f.get("expected_md5", "")) size = cfg_entry.get("size", f.get("size", 0)) @@ -1413,13 +1418,15 @@ def generate_platform_page( ] ) - # Build lookup from config file entries (has hashes/sizes) - config_files: dict[str, dict] = {} + # The declaration behind each detail, by system and name in declaration + # order: keyed by name alone, the last of MiSTer's 23 boot.rom gave its + # MD5 to every system's row. + config_files: dict[tuple[str, str], list[dict]] = {} for sys_id, system in config.get("systems", {}).items(): for fe in system.get("files", []): fname = fe.get("name", "") if fname: - config_files[fname] = fe + config_files.setdefault((sys_id, fname), []).append(fe) # Group details by system by_system: dict[str, list] = {} diff --git a/tests/test_site_platform_hashes.py b/tests/test_site_platform_hashes.py new file mode 100644 index 00000000..672e8fe8 --- /dev/null +++ b/tests/test_site_platform_hashes.py @@ -0,0 +1,48 @@ +"""A platform page shows each row the hash its own declaration carries. + +The page joined verify's details to the platform YAML by file name alone, +the last declaration winning: MiSTer declares boot.rom 23 times with 21 +distinct MD5s, and every boot.rom row showed the same one, so a user +checking a dump against the page was told the wrong hash. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from generate_site import _render_platform_systems # noqa: E402 + +A, B, C = "a" * 32, "b" * 32, "c" * 32 + + +class EachRowItsDeclaration(unittest.TestCase): + def test_same_name_in_two_systems_and_twice_in_one(self): + config_files = { + ("psx", "boot.rom"): [{"name": "boot.rom", "md5": A}], + ("saturn", "boot.rom"): [ + {"name": "boot.rom", "md5": B}, + {"name": "boot.rom", "md5": C}, + ], + } + by_system = { + "psx": [{"name": "boot.rom", "status": "ok", "system": "psx"}], + "saturn": [ + {"name": "boot.rom", "status": "ok", "system": "saturn"}, + {"name": "boot.rom", "status": "ok", "system": "saturn"}, + ], + } + text = "\n".join(_render_platform_systems(by_system, config_files, {})) + psx, saturn = text.split("saturn", 1)[0], text.split("saturn", 1)[1] + self.assertIn(A, psx) + self.assertNotIn(B, psx) + self.assertLess(saturn.index(B), saturn.index(C)) + self.assertNotIn(A, saturn) + + +if __name__ == "__main__": + unittest.main()