Files
libretro/tests/test_truth_diff.py

233 lines
9.7 KiB
Python

#!/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 ( # noqa: E402
_diff_system,
_match_renames,
_merge_file_into_system,
generate_platform_truth,
)
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/<platform>.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):
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()