#!/usr/bin/env python3 """Rename detection in the truth-vs-platform diff. A platform may call a file whatever it likes: Batocera ships the same Sega CD ROM as ROM1. A name that matches nothing in the profile is therefore not yet a gap, and reporting it as both a missing file and an extra one would invent a discrepancy that does not exist. Content decides, as everywhere else here. """ from __future__ import annotations import pathlib import subprocess import sys import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT / "scripts")) from truth import _diff_system, _match_renames, _merge_file_into_system # noqa: E402 def _entry(name: str, **hashes) -> dict: return {"name": name, **hashes} class ATargetedModelIsItsOwnArtifact(unittest.TestCase): """A narrowed truth model must not take the full model's place. generate_truth wrote dist/truth/.yml whatever --target said, so a targeted run overwrote the full model and diff_truth then compared a narrowed model against the whole scrape. """ def test_a_target_writes_beside_the_full_model(self): repo = pathlib.Path(__file__).resolve().parent.parent with tempfile.TemporaryDirectory(dir=str(repo / "tmp")) as directory: for extra in ([], ["--target", "browser"]): proc = subprocess.run( [sys.executable, "scripts/generate_truth.py", "--platform", "romm", *extra, "--output-dir", directory], capture_output=True, check=False, text=True, cwd=str(repo), timeout=400, ) self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) produced = sorted( str(p.relative_to(directory)) for p in pathlib.Path(directory).rglob("*.yml") ) self.assertEqual( produced, ["browser/romm.yml", "romm.yml"], "the targeted model did not land beside the full one", ) class ADiffThatComparedNothingFails(unittest.TestCase): """diff_truth --all skipped every platform without a model and exited 0.""" def test_an_empty_truth_directory_is_an_error(self): repo = pathlib.Path(__file__).resolve().parent.parent with tempfile.TemporaryDirectory(dir=str(repo / "tmp")) as directory: for selection in (["--all"], ["--platform", "retroarch"]): proc = subprocess.run( [sys.executable, "scripts/diff_truth.py", *selection, "--truth-dir", directory, "--json"], capture_output=True, check=False, text=True, cwd=str(repo), timeout=120, ) self.assertEqual(proc.returncode, 1, selection) self.assertIn("skip", proc.stderr) class OneNameSeveralFiles(unittest.TestCase): """Dolphin's three IPL.bin differ only by path; the model kept one.""" REGIONS = {"USA": "a" * 32, "EUR": "b" * 32, "JAP": "c" * 32} def _system(self) -> dict: system: dict = {} for core in ("dolphin", "ishiiruka"): for region, md5 in self.REGIONS.items(): _merge_file_into_system( system, {"name": "IPL.bin", "path": f"GC/{region}/IPL.bin", "md5": md5}, core, None, ) _merge_file_into_system(system, {"name": "IPL.bin"}, "other", None) return system def test_a_core_declaring_a_name_twice_declares_two_files(self): files = self._system()["files"] self.assertEqual( sorted(f["path"] for f in files), ["GC/EUR/IPL.bin", "GC/JAP/IPL.bin", "GC/USA/IPL.bin"], ) for entry in files: self.assertEqual(entry["_cores"] - {"other"}, {"dolphin", "ishiiruka"}) def test_revisions_under_one_name_and_no_path_stay_one_file(self): system: dict = {} for md5 in ("a" * 32, "b" * 32): _merge_file_into_system( system, {"name": "MT32_CONTROL.ROM", "md5": md5}, "dosbox", None ) self.assertEqual(len(system["files"]), 1) def test_the_diff_pairs_each_by_destination(self): scraped = {"files": [ {"name": "IPL.bin", "destination": f"dolphin-emu/Sys/GC/{region}/IPL.bin", "md5": md5} for region, md5 in self.REGIONS.items() ]} self.assertEqual(_diff_system(self._system(), scraped), {}) class DifferentContentsStayApart(unittest.TestCase): def test_two_cores_same_name_different_contents(self): """beebem's BBC BIOS.rom and np2kai's PC-98 bios.rom merged into one.""" system: dict = {} _merge_file_into_system(system, {"name": "bios.rom", "path": "np2kai/bios.rom", "md5": "a" * 32}, "np2kai", None) _merge_file_into_system(system, {"name": "bios.rom", "path": "BeebFile/BIOS.rom", "md5": "b" * 32, "size": 16384}, "beebem", None) self.assertEqual(len(system["files"]), 2) class RenameMatching(unittest.TestCase): def test_a_shared_sha1_pairs_the_two_names(self): truth = [_entry("bios_CD_U.bin", sha1="a" * 40)] scraped = {"rom1.bin": _entry("ROM1.bin", sha1="a" * 40)} matched_truth, matched_scraped = _match_renames(truth, scraped) self.assertEqual(matched_truth, {0}) self.assertEqual(matched_scraped, {"rom1.bin"}) def test_md5_and_crc32_pair_them_too(self): for algorithm in ("md5", "crc32"): with self.subTest(algorithm=algorithm): value = "b" * 32 truth = [_entry("a.bin", **{algorithm: value})] scraped = {"b.bin": _entry("b.bin", **{algorithm: value})} matched_truth, _ = _match_renames(truth, scraped) self.assertEqual(matched_truth, {0}) def test_hash_comparison_ignores_case(self): truth = [_entry("a.bin", sha1="AbCdEf" + "0" * 34)] scraped = {"b.bin": _entry("b.bin", sha1="abcdef" + "0" * 34)} self.assertEqual(_match_renames(truth, scraped)[0], {0}) def test_different_content_is_not_a_rename(self): truth = [_entry("a.bin", sha1="a" * 40)] scraped = {"b.bin": _entry("b.bin", sha1="b" * 40)} self.assertEqual(_match_renames(truth, scraped), (set(), set())) def test_files_with_no_hashes_never_pair(self): """Two unknowns are not evidence of the same file.""" truth = [_entry("a.bin")] scraped = {"b.bin": _entry("b.bin")} self.assertEqual(_match_renames(truth, scraped), (set(), set())) def test_a_non_string_hash_is_ignored_rather_than_crashing(self): truth = [_entry("a.bin", crc32=12345678)] scraped = {"b.bin": _entry("b.bin", crc32=12345678)} self.assertEqual(_match_renames(truth, scraped), (set(), set())) def test_empty_input_on_either_side_pairs_nothing(self): self.assertEqual(_match_renames([], {}), (set(), set())) self.assertEqual(_match_renames([_entry("a", sha1="a" * 40)], {}), (set(), set())) self.assertEqual( _match_renames([], {"b": _entry("b", sha1="a" * 40)}), (set(), set()) ) def test_each_scraped_file_pairs_once(self): """A file matching on several digests must not be counted twice.""" truth = [_entry("a.bin", sha1="a" * 40, md5="b" * 32)] scraped = {"b.bin": _entry("b.bin", sha1="a" * 40, md5="b" * 32)} matched_truth, matched_scraped = _match_renames(truth, scraped) self.assertEqual(len(matched_truth), 1) self.assertEqual(len(matched_scraped), 1) def test_several_renames_are_all_reported(self): truth = [_entry("a.bin", sha1="a" * 40), _entry("c.bin", sha1="c" * 40)] scraped = { "b.bin": _entry("b.bin", sha1="a" * 40), "d.bin": _entry("d.bin", sha1="c" * 40), } matched_truth, matched_scraped = _match_renames(truth, scraped) self.assertEqual(matched_truth, {0, 1}) self.assertEqual(matched_scraped, {"b.bin", "d.bin"}) def test_an_unrelated_file_beside_a_rename_stays_unmatched(self): truth = [_entry("a.bin", sha1="a" * 40), _entry("lonely.bin", sha1="f" * 40)] scraped = {"b.bin": _entry("b.bin", sha1="a" * 40)} matched_truth, _ = _match_renames(truth, scraped) self.assertEqual(matched_truth, {0}) self.assertNotIn("lonely.bin", matched_truth) class OnlySystemDirectoryEntries(unittest.TestCase): """The truth kept files read from the save, content or working directory: diff_truth listed Cemu's mlc01 certificates, panda3ds's seeddb.bin and neo-emu's neogeo.zip as missing from platform lists that the pack and verify deliberately leave them out of.""" def test_a_file_read_elsewhere_is_not_asked_of_the_platform(self): from truth import generate_platform_truth profiles = { "core": { "emulator": "Core", "type": "libretro", "systems": ["sys"], "files": [ {"name": "bios.bin", "sha1": "a" * 40}, {"name": "explicit.bin", "sha1": "b" * 40, "load_from": "system_dir"}, {"name": "cert.der", "sha1": "c" * 40, "load_from": "save_dir"}, {"name": "game.zip", "sha1": "d" * 40, "load_from": "content_dir"}, ], } } result = generate_platform_truth("p", {"cores": ["core"]}, {}, profiles) names = sorted(f["name"] for f in result["systems"]["sys"]["files"]) self.assertEqual(names, ["bios.bin", "explicit.bin"]) if __name__ == "__main__": unittest.main()