"""Tests for dump-catalog provenance: parsers, importers, join, report.""" from __future__ import annotations import json import tempfile import unittest import zipfile from pathlib import Path from scripts.common import ( annotate_provenance, build_provenance_index, load_provenance_snapshots, write_provenance_snapshot, ) from scripts.generate_site import generate_provenance_page from scripts.provenance_report import build_report from scripts.scraper.dat_pack_importer import _bios_entries, import_pack from scripts.scraper.logiqx_parser import parse_logiqx, validate_logiqx_format from scripts.scraper.redump_dat_scraper import discover_bios_datfiles, parse_redump_dat LOGIQX_GAME_FIXTURE = """
Nintendo - Game Boy Advance 20260801-123456
[BIOS] Game Boy Advance (World) Some Game (USA)
""" LOGIQX_MACHINE_FIXTURE = """
Sony - PlayStation - BIOS Images 2026-06-16
SCPH-1000/DTL-H1000 (Version 1.0 J) Two chips
""" TOSEC_FIRMWARE_FIXTURE = """
Sega Dreamcast - Firmware TOSEC-v2025-03-13
Dreamcast BIOS v1.01d (1998)(Sega)(JP)
""" class TestLogiqxParser(unittest.TestCase): def test_parse_header_and_games(self): dat = parse_logiqx(LOGIQX_GAME_FIXTURE) self.assertEqual(dat.name, "Nintendo - Game Boy Advance") self.assertEqual(dat.version, "20260801-123456") self.assertEqual(len(dat.roms), 2) bios = dat.roms[0] self.assertEqual(bios.game, "[BIOS] Game Boy Advance (World)") self.assertEqual(bios.name, "[BIOS] Game Boy Advance (World).gba") self.assertEqual(bios.size, 16384) def test_hashes_lowercased(self): dat = parse_logiqx(LOGIQX_GAME_FIXTURE) bios = dat.roms[0] self.assertEqual(bios.md5, "a860e8c0b6d573d191e4ec7db1b1e4f6") self.assertEqual(bios.sha1, "300c20df6731a33952ded8c436f7f186d25d3492") def test_parse_machine_entries(self): dat = parse_logiqx(LOGIQX_MACHINE_FIXTURE) self.assertEqual(len(dat.roms), 3) self.assertEqual(dat.roms[0].description, "SCPH-1000/DTL-H1000 (Version 1.0 J)") def test_multiple_roms_per_machine(self): dat = parse_logiqx(LOGIQX_MACHINE_FIXTURE) names = [r.name for r in dat.roms if r.game == "multi-rom"] self.assertEqual(names, ["chip1.bin", "chip2.bin"]) def test_invalid_size_defaults_to_zero(self): content = LOGIQX_GAME_FIXTURE.replace('size="16384"', 'size="bogus"') dat = parse_logiqx(content) self.assertEqual(dat.roms[0].size, 0) def test_entity_declarations_rejected(self): content = ( ']>' "" ) with self.assertRaises(ValueError): parse_logiqx(content) def test_validate_format(self): self.assertTrue(validate_logiqx_format(LOGIQX_GAME_FIXTURE)) self.assertFalse(validate_logiqx_format("clrmamepro ( name x )")) self.assertFalse( validate_logiqx_format("
") ) class TestRedumpScraper(unittest.TestCase): def test_discover_bios_datfiles(self): html = ( 'x' 'y' 'z' 'dup' ) self.assertEqual( discover_bios_datfiles(html), ["/static/bios/Nintendo%20GC.dat", "/static/bios/Sony%20PSX.dat"], ) def test_parse_redump_dat(self): name, version, entries = parse_redump_dat(LOGIQX_MACHINE_FIXTURE) self.assertEqual(name, "Sony - PlayStation - BIOS Images") self.assertEqual(version, "2026-06-16") self.assertEqual(len(entries), 3) self.assertEqual(entries[0]["dat"], "Sony - PlayStation - BIOS Images") self.assertEqual(entries[0]["name"], "ps-10j.bin") self.assertEqual(entries[0]["description"], "SCPH-1000/DTL-H1000 (Version 1.0 J)") class TestPackImporter(unittest.TestCase): def test_no_intro_filter_keeps_bios_only(self): dat = parse_logiqx(LOGIQX_GAME_FIXTURE) entries = _bios_entries("no-intro", dat) self.assertEqual(len(entries), 1) self.assertEqual(entries[0]["name"], "[BIOS] Game Boy Advance (World).gba") def test_tosec_filter_by_dat_name(self): firmware = parse_logiqx(TOSEC_FIRMWARE_FIXTURE) self.assertEqual(len(_bios_entries("tosec", firmware)), 1) games = parse_logiqx(LOGIQX_GAME_FIXTURE) self.assertEqual(_bios_entries("tosec", games), []) def test_import_pack_zip(self): with tempfile.TemporaryDirectory() as tmp: pack = Path(tmp) / "pack.zip" with zipfile.ZipFile(pack, "w") as zf: zf.writestr("Nintendo - Game Boy Advance.dat", LOGIQX_GAME_FIXTURE) zf.writestr("garbage.dat", "not xml at all") zf.writestr("readme.txt", "ignored") dats, entries, skipped = import_pack("no-intro", pack) self.assertEqual(list(dats), ["Nintendo - Game Boy Advance"]) self.assertEqual(len(entries), 1) self.assertEqual(skipped, 1) def test_import_pack_directory(self): with tempfile.TemporaryDirectory() as tmp: (Path(tmp) / "Sega Dreamcast - Firmware.dat").write_text( TOSEC_FIRMWARE_FIXTURE ) dats, entries, skipped = import_pack("tosec", Path(tmp)) self.assertEqual(list(dats), ["Sega Dreamcast - Firmware"]) self.assertEqual(len(entries), 1) self.assertEqual(skipped, 0) def _snapshot_entries(): return [ { "dat": "Sony - PlayStation - BIOS Images", "name": "ps-30j.bin", "description": "SCPH-5500 (Version 3.0 J)", "size": 524288, "crc32": "ff3eeb8c", "md5": "8dd7d5296a650fac7319bce665a6a53c", "sha1": "b05def971d8ec59f346f2d9ac21fb742e3eb6917", }, { "dat": "Sony - PlayStation - BIOS Images", "name": "ps-41a.bin", "description": "SCPH-7001 (Version 4.1 A)", "size": 524288, "crc32": "502224b6", "md5": "1e68c231d0896b7eadcad1d7d8e76129", "sha1": "14df4f6c1e367ce097c11deae21566b4fe5647a9", }, { "dat": "Sony - PlayStation - BIOS Images", "name": "no-sha1.bin", "description": "Entry without sha1", "size": 1024, "crc32": "deadbeef", "md5": "d41d8cd98f00b204e9800998ecf8427e", "sha1": "", }, ] class TestProvenanceJoin(unittest.TestCase): def _snapshots(self): return {"redump": {"source": "redump", "entries": _snapshot_entries()}} def test_index_by_sha1_and_md5_size(self): index = build_provenance_index(self._snapshots()) redump = index["redump"] self.assertIn("b05def971d8ec59f346f2d9ac21fb742e3eb6917", redump["by_sha1"]) self.assertNotIn("", redump["by_sha1"]) self.assertIn( ("d41d8cd98f00b204e9800998ecf8427e", 1024), redump["by_md5_size"] ) def test_annotate_matches_by_sha1(self): files = { "b05def971d8ec59f346f2d9ac21fb742e3eb6917": { "name": "scph5500.bin", "size": 524288, "md5": "8dd7d5296a650fac7319bce665a6a53c", }, "0000000000000000000000000000000000000000": { "name": "unrelated.bin", "size": 42, "md5": "ffffffffffffffffffffffffffffffff", }, } counts = annotate_provenance(files, self._snapshots()) self.assertEqual(counts, {"redump": 1}) match = files["b05def971d8ec59f346f2d9ac21fb742e3eb6917"]["provenance"] self.assertEqual(match["redump"]["name"], "ps-30j.bin") self.assertEqual(match["redump"]["description"], "SCPH-5500 (Version 3.0 J)") self.assertNotIn( "provenance", files["0000000000000000000000000000000000000000"] ) def test_annotate_falls_back_to_md5_size(self): files = { "1111111111111111111111111111111111111111": { "name": "other-name.bin", "size": 1024, "md5": "d41d8cd98f00b204e9800998ecf8427e", } } counts = annotate_provenance(files, self._snapshots()) self.assertEqual(counts, {"redump": 1}) self.assertEqual( files["1111111111111111111111111111111111111111"]["provenance"]["redump"][ "name" ], "no-sha1.bin", ) def test_annotate_pops_stale_provenance(self): files = { "2222222222222222222222222222222222222222": { "name": "was-matched.bin", "size": 5, "md5": "00000000000000000000000000000000", "provenance": {"redump": {"dat": "old", "name": "old.bin"}}, } } annotate_provenance(files, self._snapshots()) self.assertNotIn( "provenance", files["2222222222222222222222222222222222222222"] ) def test_annotate_without_snapshots(self): files = {"aa": {"name": "x.bin", "size": 1, "md5": "bb"}} self.assertEqual(annotate_provenance(files, {}), {}) self.assertNotIn("provenance", files["aa"]) class TestSnapshotIO(unittest.TestCase): def test_write_and_load_roundtrip(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "redump.json" written = write_provenance_snapshot( str(path), "redump", "2026-08-07", {"Sony - PlayStation - BIOS Images": "2026-06-16"}, _snapshot_entries(), ) self.assertTrue(written) snapshots = load_provenance_snapshots(tmp) self.assertEqual(list(snapshots), ["redump"]) self.assertEqual(len(snapshots["redump"]["entries"]), 3) names = [e["name"] for e in snapshots["redump"]["entries"]] self.assertEqual(names, sorted(names)) def test_rewrite_with_new_date_is_unchanged(self): with tempfile.TemporaryDirectory() as tmp: path = str(Path(tmp) / "redump.json") write_provenance_snapshot( path, "redump", "2026-08-07", {}, _snapshot_entries() ) rewritten = write_provenance_snapshot( path, "redump", "2027-01-01", {}, _snapshot_entries() ) self.assertFalse(rewritten) def test_load_missing_dir(self): self.assertEqual(load_provenance_snapshots("does/not/exist"), {}) def test_load_ignores_empty_snapshot(self): with tempfile.TemporaryDirectory() as tmp: (Path(tmp) / "empty.json").write_text( json.dumps({"source": "tosec", "entries": []}) ) self.assertEqual(load_provenance_snapshots(tmp), {}) class TestProvenanceReport(unittest.TestCase): def test_build_report_matched_and_missing(self): db = { "files": { "b05def971d8ec59f346f2d9ac21fb742e3eb6917": { "name": "scph5500.bin", "size": 524288, "md5": "8dd7d5296a650fac7319bce665a6a53c", } } } snapshots = { "redump": { "source": "redump", "imported_at": "2026-08-07", "dats": {"Sony - PlayStation - BIOS Images": "2026-06-16"}, "entries": _snapshot_entries(), } } report = build_report(db, snapshots) self.assertEqual(report["redump"]["total"], 3) self.assertEqual(report["redump"]["matched"], 1) missing = [e["name"] for e in report["redump"]["missing"]] self.assertEqual(missing, ["ps-41a.bin", "no-sha1.bin"]) self.assertEqual(report["redump"]["out_of_scope"], 0) self.assertEqual( report["redump"]["covered_dats"], ["Sony - PlayStation - BIOS Images"] ) def test_uncovered_dat_entries_are_out_of_scope(self): """Entries from a DAT the collection never matches are not targets. No-Intro tags digital title distribution "[BIOS]" too, so an uncovered DAT must not swamp the acquisition list. """ entries = _snapshot_entries() + [ { "dat": "Nintendo - Wii U (Digital) (CDN)", "name": "00000001.app", "description": "[BIOS] Account Settings (Japan)", "size": 999, "crc32": "aaaaaaaa", "md5": "99999999999999999999999999999999", "sha1": "9999999999999999999999999999999999999999", } ] db = { "files": { "b05def971d8ec59f346f2d9ac21fb742e3eb6917": { "name": "scph5500.bin", "size": 524288, "md5": "8dd7d5296a650fac7319bce665a6a53c", } } } report = build_report(db, {"no-intro": {"entries": entries}}) data = report["no-intro"] self.assertEqual(data["out_of_scope"], 1) self.assertNotIn( "Nintendo - Wii U (Digital) (CDN)", [e["dat"] for e in data["missing"]], ) class TestProvenancePage(unittest.TestCase): def _page(self, extra_entries=None, provenance=True): db = { "total_files": 2, "files": { "b05def971d8ec59f346f2d9ac21fb742e3eb6917": { "name": "scph5500.bin", "size": 524288, "md5": "8dd7d5296a650fac7319bce665a6a53c", **( { "provenance": { "redump": { "dat": "Sony - PlayStation - BIOS Images", "name": "ps-30j.bin", "description": "SCPH-5500", } } } if provenance else {} ), }, "cccccccccccccccccccccccccccccccccccccccc": { "name": "unrelated.bin", "size": 1, "md5": "cc", }, }, } snapshots = { "redump": { "source": "redump", "imported_at": "2026-08-07", "dats": {}, "entries": _snapshot_entries() + (extra_entries or []), } } return generate_provenance_page(db, build_report(db, snapshots)) def test_page_states_match_count_and_boundary(self): page = self._page() self.assertIn("**1** of 2 system files", page) self.assertIn("## What the badge means", page) self.assertIn("## What it does not mean", page) self.assertIn("A file without a badge is not inferior", page) def test_page_lists_missing_entries_with_hashes(self): page = self._page() self.assertIn("ps-41a.bin", page) self.assertIn("14df4f6c1e367ce097c11deae21566b4fe5647a9", page) self.assertIn("### Redump", page) def test_page_has_anchor_targets_for_badges(self): """System-page badges link to ../provenance.md#.""" page = self._page() self.assertIn("### Redump", page) def test_pipe_in_catalog_text_does_not_break_table(self): extra = [ { "dat": "Sony - PlayStation - BIOS Images", "name": "we|rd.bin", "description": "has | pipe", "size": 7, "crc32": "0", "md5": "77777777777777777777777777777777", "sha1": "7777777777777777777777777777777777777777", } ] page = self._page(extra) row = next(line for line in page.split("\n") if "we-rd.bin" in line) self.assertEqual(row.count("|"), 4) self.assertIn("has - pipe", row) def test_page_without_any_match(self): page = self._page(provenance=False) self.assertIn("**0** of 2 system files", page) if __name__ == "__main__": unittest.main()