diff --git a/scripts/exporter/emudeck_exporter.py b/scripts/exporter/emudeck_exporter.py index aef7d45e..ab5ba1ba 100644 --- a/scripts/exporter/emudeck_exporter.py +++ b/scripts/exporter/emudeck_exporter.py @@ -90,8 +90,27 @@ class Exporter(BaseExporter): def states(self, fe: NativeFile, field_name: str) -> bool: if field_name in fe.filled: return False + if field_name == "md5" and fe.native_system in self._refused_systems(): + # The array was left as EmuDeck wrote it: nothing was corrected. + return False return super().states(fe, field_name) + def _refused_systems(self) -> set[str]: + withdrawn = getattr(self, "_withdrawn", {}) + return {FUNCTION_HASH_MAP[name] for name in withdrawn if name in FUNCTION_HASH_MAP} + + @staticmethod + def _corrected_away(systems: dict[str, NativeSystem], system_id: str) -> set[str]: + """The platform md5 values a correction of ours replaces.""" + return { + value + for system in systems.values() + if system.native_id == system_id + for fe in system.files + if "md5" in fe.corrections + for value in _hash_values(fe.platform or {}, "md5") + } + @classmethod def _md5s(cls, systems: dict[str, NativeSystem], system_id: str) -> list[str]: """Every MD5 the system accepts, in a stable order, deduplicated.""" @@ -151,7 +170,9 @@ class Exporter(BaseExporter): # and our model does not was deleted: a user whose dump matched # it would stop passing the check. A rewrite that withdraws one # is refused and reported instead. - withdrawn = theirs - set(md5s) + # A correction replaces the value it corrects, which is not a + # withdrawal; any other value missing from ours would be. + withdrawn = theirs - set(md5s) - self._corrected_away(systems, system_id) if withdrawn: self._withdrawn.setdefault(name, set()).update(withdrawn) elif set(md5s) != theirs: @@ -180,7 +201,8 @@ class Exporter(BaseExporter): issues.append(f"check absent from the output: {name}") continue md5s = self._md5s(systems, system_id) - if not md5s: + if not md5s or name in getattr(self, "_withdrawn", {}): + # A refused rewrite keeps EmuDeck's own array, by design. continue body = next( content[start:end] diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index aabdca1f..fce1c7e0 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -123,6 +123,49 @@ class HashesTheFormatCannotHold(unittest.TestCase): self.assertFalse(EmuDeck().states(unhashed, "md5")) +class EmuDeckWritesItsCorrections(unittest.TestCase): + """A correction replaced nothing: every withdrawal was refused, the run + counted the correction anyway and validate failed on it.""" + + SCRIPT = "checkPS1BIOS(){\n local hashes=(%s)\n}\n" + + def _systems(self, corrected: NativeFile, other: NativeFile) -> dict: + from scraper.emudeck_scraper import FUNCTION_HASH_MAP + + system_id = FUNCTION_HASH_MAP["checkPS1BIOS"] + corrected.native_system = other.native_system = system_id + return {system_id: NativeSystem(system_id, files=[corrected, other])} + + def test_a_correction_replaces_its_value(self): + corrected = NativeFile("scph5501.bin", "scph5501.bin", "psx", + platform={"md5": A}, truth={"md5": B}, corrections=["md5"]) + other = NativeFile("scph1001.bin", "scph1001.bin", "psx", platform={"md5": C}) + systems = self._systems(corrected, other) + exporter = EmuDeck() + produced = exporter.render(systems, None, {"checkBIOS.sh": self.SCRIPT % f"{A} {C}"}) + text = produced["checkBIOS.sh"] + self.assertIn(B, text) + self.assertNotIn(A, text) + self.assertTrue(exporter.states(corrected, "md5")) + self.assertEqual( + [i for i in exporter.validate(systems, produced) if "checkPS1BIOS" in i], [] + ) + + def test_a_withdrawal_is_refused_and_not_counted(self): + corrected = NativeFile("scph5501.bin", "scph5501.bin", "psx", + platform={"md5": A}, truth={"md5": B}, corrections=["md5"]) + other = NativeFile("scph1001.bin", "scph1001.bin", "psx", platform={"md5": C}) + systems = self._systems(corrected, other) + exporter = EmuDeck() + d = "d" * 32 + produced = exporter.render(systems, None, {"checkBIOS.sh": self.SCRIPT % f"{A} {C} {d}"}) + self.assertIn(d, produced["checkBIOS.sh"]) + self.assertFalse(exporter.states(corrected, "md5")) + self.assertEqual( + [i for i in exporter.validate(systems, produced) if "checkPS1BIOS" in i], [] + ) + + class ModelKeepsOneFileOneEntry(unittest.TestCase): def test_size_describes_the_hash_written(self): fe = NativeFile("boot.bin", "dc/boot.bin", "dc",