mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-11 05:53:23 -05:00
The gap analysis answered from the name index, so a core extra whose local copy contradicts its declared hash counted as held. Under a digest mode the builder drops exactly that file, so the coverage report described a pack that would not contain it: seven files across Batocera, Recalbox and RetroBat. An entry that states what its content should be is now resolved by content; the name still answers for entries that declare nothing to check against, and existence mode is unchanged because there the frontend never opens the file and the pack does carry it.
216 lines
7.9 KiB
Python
216 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""The native-mode policy verify.py and generate_pack.py must share.
|
|
|
|
verify.py and generate_pack.py must reach the same verdict on the same file:
|
|
a pack and a coverage report that disagree describe different collections.
|
|
The rule used to rest on both files spelling out `mode == "existence"` in
|
|
their own words. Routing both through scripts/nativemode.py single-sources
|
|
it, and these tests make the agreement checkable instead of reviewable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
import nativemode # noqa: E402
|
|
|
|
|
|
class ModeVocabulary(unittest.TestCase):
|
|
def test_modes_match_the_platform_schema(self):
|
|
"""A mode added to one side and not the other is the bug this catches."""
|
|
schema = json.loads(
|
|
(REPO_ROOT / "schemas" / "platform.schema.json").read_text()
|
|
)
|
|
enum = schema["properties"]["verification_mode"]["enum"]
|
|
self.assertEqual(sorted(nativemode.MODES), sorted(enum))
|
|
|
|
def test_schema_default_matches_the_module_default(self):
|
|
schema = json.loads(
|
|
(REPO_ROOT / "schemas" / "platform.schema.json").read_text()
|
|
)
|
|
self.assertEqual(
|
|
schema["properties"]["verification_mode"]["default"],
|
|
nativemode.DEFAULT_MODE,
|
|
)
|
|
|
|
def test_unknown_or_missing_mode_falls_back_to_the_default(self):
|
|
for value in (None, "", "sha256", "MD5"):
|
|
self.assertEqual(nativemode.normalize(value), nativemode.DEFAULT_MODE)
|
|
|
|
def test_every_declared_platform_mode_is_known(self):
|
|
import yaml
|
|
|
|
for path in sorted((REPO_ROOT / "platforms").glob("*.yml")):
|
|
if path.name.startswith("_"):
|
|
continue
|
|
data = yaml.safe_load(path.read_text()) or {}
|
|
mode = data.get("verification_mode")
|
|
if mode is None:
|
|
continue
|
|
self.assertIn(mode, nativemode.MODES, f"{path.name} declares {mode!r}")
|
|
|
|
|
|
class ContentReadingPolicy(unittest.TestCase):
|
|
def test_existence_reads_nothing(self):
|
|
self.assertFalse(nativemode.reads_file_contents("existence"))
|
|
self.assertIsNone(nativemode.digest_algorithm("existence"))
|
|
|
|
def test_digest_modes_read_their_digest(self):
|
|
self.assertEqual(nativemode.digest_algorithm("md5"), "md5")
|
|
self.assertEqual(nativemode.digest_algorithm("sha1"), "sha1")
|
|
self.assertTrue(nativemode.reads_file_contents("md5"))
|
|
self.assertTrue(nativemode.reads_file_contents("sha1"))
|
|
|
|
def test_exclusion_follows_content_reading_for_every_mode(self):
|
|
"""The builder omits exactly what the frontend would reject."""
|
|
for mode in nativemode.MODES:
|
|
self.assertEqual(
|
|
nativemode.hash_mismatch_excludes_file(mode),
|
|
nativemode.reads_file_contents(mode),
|
|
f"mode {mode} disagrees with itself",
|
|
)
|
|
|
|
|
|
class BothConsumersAgree(unittest.TestCase):
|
|
"""verify.py and generate_pack.py must answer the same for every mode."""
|
|
|
|
def test_builder_and_verifier_share_one_predicate(self):
|
|
from generate_pack import _intentional_hash_exclusion
|
|
from verify import compute_severity # noqa: F401 (import proves wiring)
|
|
|
|
for mode in nativemode.MODES:
|
|
# No entries is never an exclusion, whatever the mode.
|
|
self.assertFalse(_intentional_hash_exclusion([], {}, verification_mode=mode))
|
|
|
|
# The builder must refuse to exclude under existence even when asked.
|
|
self.assertFalse(
|
|
_intentional_hash_exclusion(
|
|
[{"name": "x.bin"}], {}, verification_mode="existence"
|
|
)
|
|
)
|
|
|
|
def test_severity_uses_the_shared_predicate(self):
|
|
from verify import Severity, Status, compute_severity
|
|
|
|
# Existence mode: a missing required file is a warning, not critical.
|
|
self.assertEqual(
|
|
compute_severity(Status.MISSING, True, "existence"),
|
|
Severity.WARNING,
|
|
)
|
|
# Digest mode: the same absence is critical.
|
|
self.assertEqual(
|
|
compute_severity(Status.MISSING, True, "md5"),
|
|
Severity.CRITICAL,
|
|
)
|
|
# An unknown mode must fall back to existence, not to the strictest
|
|
# reading: a typo in a platform YAML should not invent CRITICALs.
|
|
self.assertEqual(
|
|
compute_severity(Status.MISSING, True, "sha256"),
|
|
Severity.WARNING,
|
|
)
|
|
|
|
|
|
class GapAnalysisAgreesWithTheBuilder(unittest.TestCase):
|
|
""""Available" must mean the pack will carry it.
|
|
|
|
find_undeclared_files answered from the name index, so a core extra whose
|
|
local copy contradicts its declared hash counted as held. Under a digest
|
|
mode the builder drops exactly that file, so the coverage report described
|
|
a pack that would not contain it: seven such files across three platforms.
|
|
"""
|
|
|
|
def _fixture(self):
|
|
import hashlib
|
|
import tempfile
|
|
|
|
tmp = tempfile.TemporaryDirectory()
|
|
root = Path(tmp.name)
|
|
(root / "emulators").mkdir()
|
|
rom = root / "collide.rom"
|
|
rom.write_bytes(b"THE BYTES THE COLLECTION HOLDS")
|
|
sha1 = hashlib.sha1(rom.read_bytes()).hexdigest()
|
|
db = {
|
|
"files": {
|
|
sha1: {
|
|
"path": str(rom),
|
|
"name": "collide.rom",
|
|
"size": rom.stat().st_size,
|
|
"sha1": sha1,
|
|
"md5": hashlib.md5(rom.read_bytes()).hexdigest(),
|
|
"sha256": hashlib.sha256(rom.read_bytes()).hexdigest(),
|
|
"crc32": "00000000",
|
|
}
|
|
},
|
|
"indexes": {
|
|
"by_name": {"collide.rom": [sha1]},
|
|
"by_md5": {hashlib.md5(rom.read_bytes()).hexdigest(): sha1},
|
|
"by_sha256": {},
|
|
"by_crc32": {},
|
|
"by_path_suffix": {},
|
|
},
|
|
}
|
|
(root / "emulators" / "demo.yml").write_text(
|
|
"emulator: demo\n"
|
|
"type: libretro\n"
|
|
"display_name: Demo\n"
|
|
"systems: [demo-system]\n"
|
|
"cores: [demo]\n"
|
|
"files:\n"
|
|
" - name: collide.rom\n"
|
|
" system: demo-system\n"
|
|
" required: true\n"
|
|
" md5: \"" + "f" * 32 + "\"\n"
|
|
)
|
|
return tmp, root, db
|
|
|
|
def _in_repo(self, mode: str) -> bool:
|
|
import common
|
|
from verify import find_undeclared_files
|
|
|
|
tmp, root, db = self._fixture()
|
|
try:
|
|
common._emulator_profiles_cache.clear()
|
|
profiles = common.load_emulator_profiles(str(root / "emulators"))
|
|
config = {
|
|
"platform": "Demo",
|
|
"verification_mode": mode,
|
|
"cores": ["demo"],
|
|
"systems": {},
|
|
}
|
|
found = find_undeclared_files(
|
|
config, str(root / "emulators"), db, emu_profiles=profiles
|
|
)
|
|
entry = next(e for e in found if e["name"] == "collide.rom")
|
|
return bool(entry["in_repo"])
|
|
finally:
|
|
common._emulator_profiles_cache.clear()
|
|
tmp.cleanup()
|
|
|
|
def test_a_digest_mode_does_not_call_a_contradicted_copy_available(self):
|
|
self.assertFalse(
|
|
self._in_repo("md5"),
|
|
"the builder drops this file, so the report must not count it",
|
|
)
|
|
|
|
def test_existence_mode_still_counts_it(self):
|
|
"""The frontend never opens the file, so the pack carries it."""
|
|
self.assertTrue(self._in_repo("existence"))
|
|
|
|
def test_the_two_modes_answer_the_way_the_shared_predicate_says(self):
|
|
for mode in nativemode.MODES:
|
|
with self.subTest(mode=mode):
|
|
self.assertEqual(
|
|
self._in_repo(mode),
|
|
not nativemode.hash_mismatch_excludes_file(mode),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|