mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
A card's BIOS folder zipped to upload it was hashed as one file and reported not recognised, its contributor asked where the dump came from and told to place it under bios/. A ZIP no list or profile declares as a whole is now read member by member: each is named held, declared and not held, or not recognised, and the contributor is asked for the files unzipped. A declared archive (a MAME or FBNeo set) is never opened.
618 lines
28 KiB
Python
618 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""The gate that inspects contributor files before a PR is reviewed.
|
|
|
|
validate.yml runs this on every added or modified file under bios/, and its
|
|
output is posted to the PR. It reads paths chosen by whoever opened the PR, so
|
|
it is an untrusted-input boundary with nothing behind it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
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"))
|
|
|
|
import validate_pr # noqa: E402
|
|
|
|
EMPTY_HASHES: dict = {"sha1": set(), "md5": set(), "names": set()}
|
|
|
|
|
|
class _Fixture(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.tmp = Path(self._tmp.name)
|
|
self._cwd = os.getcwd()
|
|
os.chdir(self.tmp)
|
|
|
|
def tearDown(self):
|
|
os.chdir(self._cwd)
|
|
self._tmp.cleanup()
|
|
|
|
def _bios_file(self, relative: str, data: bytes = b"BIOS") -> str:
|
|
path = self.tmp / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(data)
|
|
return relative
|
|
|
|
|
|
class FileShapeChecks(_Fixture):
|
|
def test_missing_file_fails_without_hashing(self):
|
|
result = validate_pr.validate_file("bios/nope.bin", None, EMPTY_HASHES)
|
|
self.assertFalse(result.passed)
|
|
self.assertEqual(result.sha1, "")
|
|
|
|
def test_a_path_never_read_shows_no_digest(self):
|
|
"""Empty Size, SHA1, MD5 and CRC32 lines read as an empty file contributed."""
|
|
result = validate_pr.validate_file("bios/nope.bin", None, EMPTY_HASHES)
|
|
markdown = result.to_markdown()
|
|
for label in ("**Size**", "**SHA1**", "**MD5**", "**CRC32**"):
|
|
self.assertNotIn(label, markdown)
|
|
self.assertIn("--changed", markdown)
|
|
|
|
def test_a_folder_is_read_file_by_file(self):
|
|
self._bios_file("card/bios/a.bin")
|
|
self._bios_file("card/bios/sub/b.bin")
|
|
(self.tmp / "empty").mkdir()
|
|
self.assertEqual(
|
|
validate_pr.expand_folders(["card", "empty", "loose.bin"]),
|
|
[str(Path("card/bios/a.bin")), str(Path("card/bios/sub/b.bin")), "empty", "loose.bin"],
|
|
)
|
|
result = validate_pr.validate_file("empty", None, EMPTY_HASHES)
|
|
self.assertIn("Folder with no file in it", result.to_markdown())
|
|
|
|
def test_empty_file_fails(self):
|
|
rel = self._bios_file("bios/Sony/PS/empty.bin", b"")
|
|
result = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
self.assertFalse(result.passed)
|
|
self.assertIn("empty", result.to_markdown().lower())
|
|
|
|
def test_executable_extension_is_refused(self):
|
|
rel = self._bios_file("bios/Sony/PS/payload.exe")
|
|
result = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
self.assertFalse(result.passed)
|
|
self.assertIn("Blocked file extension", result.to_markdown())
|
|
|
|
def test_oversized_file_is_refused(self):
|
|
rel = self._bios_file("bios/Sony/PS/big.bin")
|
|
original = validate_pr.MAX_FILE_SIZE
|
|
validate_pr.MAX_FILE_SIZE = 2
|
|
try:
|
|
result = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
finally:
|
|
validate_pr.MAX_FILE_SIZE = original
|
|
self.assertFalse(result.passed)
|
|
self.assertIn("too large", result.to_markdown())
|
|
|
|
def test_ordinary_file_passes(self):
|
|
rel = self._bios_file("bios/Sony/PlayStation/scph.bin")
|
|
result = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
self.assertTrue(result.passed, result.to_markdown())
|
|
self.assertEqual(len(result.sha1), 40)
|
|
|
|
|
|
class AFileOnlyACoreReadsIsKnown(_Fixture):
|
|
"""A file only an emulator profile declares (a standalone firmware, a
|
|
game engine's data) was reported "not referenced in any platform
|
|
config - needs manual review"."""
|
|
|
|
def test_the_profiles_that_declare_the_hash_are_named(self):
|
|
import hashlib # noqa: PLC0415
|
|
|
|
emulators = self.tmp / "emulators"
|
|
emulators.mkdir()
|
|
data = b"engine data"
|
|
(emulators / "engine.yml").write_text(
|
|
"emulator: Engine\ntype: standalone\nsystems: [x]\nfiles:\n"
|
|
f" - name: \"data.bin\"\n sha1: \"{hashlib.sha1(data).hexdigest()}\"\n"
|
|
)
|
|
known = validate_pr.load_profile_hashes(str(emulators))
|
|
result = validate_pr.validate_file(
|
|
self._bios_file("bios/Maker/Engine/data.bin", data), None, EMPTY_HASHES, known,
|
|
)
|
|
self.assertIn(("PASS", "Hash matches a file the emulator profile declares: engine"),
|
|
result.checks)
|
|
self.assertFalse(any("not referenced" in message for _s, message in result.checks))
|
|
|
|
def test_a_missing_file_is_told_the_folder_of_its_system(self):
|
|
"""The folder came from platform lists only: a file only a profile
|
|
declares, absent from the collection, got the generic advice."""
|
|
import hashlib # noqa: PLC0415
|
|
|
|
held, missing = b"held rom", b"missing rom"
|
|
emulators = self.tmp / "emulators"
|
|
emulators.mkdir()
|
|
(emulators / "engine.yml").write_text(
|
|
"emulator: Engine\ntype: standalone\nsystems: [x, y]\nfiles:\n"
|
|
f" - name: \"a.bin\"\n system: x\n sha1: \"{hashlib.sha1(held).hexdigest()}\"\n"
|
|
f" - name: \"b.bin\"\n system: x\n md5: \"{hashlib.md5(missing).hexdigest()}\"\n"
|
|
)
|
|
db = {"files": {hashlib.sha1(held).hexdigest(): {"path": "bios/Maker/Console/a.bin"}},
|
|
"indexes": {"by_md5": {}, "by_sha256": {}}}
|
|
known = validate_pr.load_profile_hashes(str(emulators), db)
|
|
result = validate_pr.validate_file(
|
|
self._bios_file("Downloads/b.bin", missing), db, EMPTY_HASHES, known,
|
|
)
|
|
self.assertIn(
|
|
("WARN", "File is not under bios/ yet. Its system's files are in "
|
|
"`bios/Maker/Console/`. --place copies it to `bios/Maker/Console/b.bin`"),
|
|
result.checks,
|
|
)
|
|
self.assertEqual(result.destination, "bios/Maker/Console/b.bin")
|
|
# A file of that name already there: the copy is a variant.
|
|
self._bios_file("bios/Maker/Console/b.bin", b"another")
|
|
self.assertEqual(
|
|
validate_pr.validate_file("Downloads/b.bin", db, EMPTY_HASHES, known).destination,
|
|
f"bios/Maker/Console/.variants/b.bin.{result.sha1[:8]}",
|
|
)
|
|
# Copied under the declared name, never over a file, and only once.
|
|
card = self._bios_file("card/B.BIN", missing)
|
|
named = validate_pr.validate_file(card, db, EMPTY_HASHES, known)
|
|
self.assertEqual(validate_pr.place([named]), [named.destination])
|
|
self.assertEqual((self.tmp / named.destination).read_bytes(), missing)
|
|
self.assertTrue((self.tmp / card).is_file())
|
|
self.assertEqual(validate_pr.place([named]), [])
|
|
unknown = validate_pr.validate_file(self._bios_file("card/x.bin"), db, EMPTY_HASHES, known)
|
|
self.assertEqual((unknown.outcome, unknown.destination), ("unknown", ""))
|
|
placed = validate_pr.validate_file(
|
|
self._bios_file("bios/Other/Place/b.bin", missing), db, EMPTY_HASHES, known,
|
|
)
|
|
self.assertTrue(any("Placed under bios/Other/Place/" in m for _s, m in placed.checks))
|
|
|
|
|
|
class ASizeEveryDeclarationRulesOutIsNotAVariant(_Fixture):
|
|
"""1024 random bytes named scph5501.bin, a name every declaration sizes
|
|
at 524288, were told "File size OK" and offered a .variants slot at the
|
|
repository root."""
|
|
|
|
DB = {
|
|
"files": {"a" * 40: {"path": "bios/Sony/PlayStation/scph5501.bin"}},
|
|
"indexes": {"by_name": {"scph5501.bin": ["a" * 40]}},
|
|
}
|
|
PLATFORM = {"sha1": set(), "md5": set(), "names": {"scph5501.bin"},
|
|
"folders": {}, "sizes": {"scph5501.bin": [(524288, 524288)]}}
|
|
|
|
def test_a_truncated_file_is_warned_and_not_offered_a_slot(self):
|
|
rel = self._bios_file("scph5501.bin", b"x" * 1024)
|
|
text = validate_pr.validate_file(rel, self.DB, self.PLATFORM).to_markdown()
|
|
self.assertIn("expects 524,288 bytes", text)
|
|
self.assertNotIn(".variants/", text)
|
|
self.assertNotIn("File size OK", text)
|
|
|
|
def test_a_right_sized_variant_goes_beside_its_name(self):
|
|
rel = self._bios_file("scph5501.bin", b"y" * 524288)
|
|
text = validate_pr.validate_file(rel, self.DB, self.PLATFORM).to_markdown()
|
|
self.assertIn("bios/Sony/PlayStation/.variants/scph5501.bin.", text)
|
|
self.assertNotIn("expects", text)
|
|
|
|
|
|
class SymlinksAreRefusedBeforeReading(_Fixture):
|
|
"""A symlink must be rejected without its target being read.
|
|
|
|
The check used to run after compute_hashes, so a link to an endless
|
|
device would be read until the CI job timed out, and a link outside the
|
|
checkout would be hashed and reported as though it were contributed.
|
|
"""
|
|
|
|
def test_symlink_is_refused(self):
|
|
self._bios_file("bios/Sony/PS/real.bin")
|
|
link = self.tmp / "bios" / "Sony" / "PS" / "link.bin"
|
|
link.symlink_to(self.tmp / "bios" / "Sony" / "PS" / "real.bin")
|
|
result = validate_pr.validate_file(
|
|
"bios/Sony/PS/link.bin", None, EMPTY_HASHES
|
|
)
|
|
self.assertFalse(result.passed)
|
|
self.assertIn("Symlink", result.to_markdown())
|
|
|
|
def test_symlink_target_is_never_hashed(self):
|
|
self._bios_file("bios/Sony/PS/real.bin")
|
|
link = self.tmp / "bios" / "Sony" / "PS" / "link.bin"
|
|
link.symlink_to(self.tmp / "bios" / "Sony" / "PS" / "real.bin")
|
|
calls = []
|
|
real = validate_pr.compute_hashes
|
|
validate_pr.compute_hashes = lambda p, *a, **k: (
|
|
calls.append(p) or real(p, *a, **k)
|
|
)
|
|
try:
|
|
validate_pr.validate_file("bios/Sony/PS/link.bin", None, EMPTY_HASHES)
|
|
finally:
|
|
validate_pr.compute_hashes = real
|
|
self.assertEqual(calls, [], "a symlink target must not be read")
|
|
|
|
@unittest.skipUnless(os.path.exists("/dev/zero"), "needs /dev/zero")
|
|
def test_a_link_to_an_endless_device_does_not_hang(self):
|
|
link = self.tmp / "bios" / "Sony" / "PS" / "zero.bin"
|
|
link.parent.mkdir(parents=True, exist_ok=True)
|
|
link.symlink_to("/dev/zero")
|
|
result = validate_pr.validate_file(
|
|
"bios/Sony/PS/zero.bin", None, EMPTY_HASHES
|
|
)
|
|
self.assertFalse(result.passed)
|
|
|
|
|
|
class HashRecognition(_Fixture):
|
|
def test_a_declared_sha1_is_recognised(self):
|
|
rel = self._bios_file("bios/Sony/PS/known.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
known = {"sha1": {probe.sha1}, "md5": set(), "names": set()}
|
|
result = validate_pr.validate_file(rel, None, known)
|
|
self.assertIn("SHA1 matches known platform requirement", result.to_markdown())
|
|
|
|
def test_a_declared_md5_is_recognised(self):
|
|
rel = self._bios_file("bios/Sony/PS/known.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
known = {"sha1": set(), "md5": {probe.md5}, "names": set()}
|
|
self.assertIn(
|
|
"MD5 matches known platform requirement",
|
|
validate_pr.validate_file(rel, None, known).to_markdown(),
|
|
)
|
|
|
|
def test_a_known_name_with_another_hash_is_flagged_as_a_variant(self):
|
|
rel = self._bios_file("bios/Sony/PS/known.bin")
|
|
known = {"sha1": set(), "md5": set(), "names": {"known.bin"}}
|
|
markdown = validate_pr.validate_file(rel, None, known).to_markdown()
|
|
self.assertIn("may be a variant", markdown)
|
|
self.assertIn(".variants/known.bin.", markdown)
|
|
|
|
def test_an_unreferenced_file_asks_for_review(self):
|
|
"""Random bytes read "PASS" and "all checks passed", and the warning
|
|
named the platform configs alone though the profiles were searched."""
|
|
rel = self._bios_file("bios/Sony/PS/mystery.bin")
|
|
result = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
markdown = result.to_markdown()
|
|
self.assertEqual(result.outcome, "unknown")
|
|
self.assertIn("### REVIEW", markdown)
|
|
self.assertIn("no platform list or emulator profile declares this file", markdown)
|
|
self.assertIn("which emulator reads it", markdown)
|
|
|
|
def test_a_zip_of_bios_files_is_read_member_by_member(self):
|
|
"""A card's BIOS folder zipped to upload it was hashed as one file,
|
|
and its contributor was asked where that dump came from."""
|
|
import hashlib # noqa: PLC0415
|
|
import zipfile # noqa: PLC0415
|
|
|
|
held, declared = b"HELD BIOS", b"DECLARED BIOS"
|
|
with zipfile.ZipFile(self.tmp / "mybios.zip", "w") as archive:
|
|
archive.writestr("scph5501.bin", held)
|
|
archive.writestr("sub/declared.bin", declared)
|
|
archive.writestr("other.bin", b"noise")
|
|
db = {"files": {hashlib.sha1(held).hexdigest(): {"path": "bios/Sony/PS/scph5501.bin"}}}
|
|
known = {**EMPTY_HASHES, "md5": {hashlib.md5(declared).hexdigest()}}
|
|
result = validate_pr.validate_file("mybios.zip", db, known)
|
|
markdown = result.to_markdown()
|
|
self.assertEqual(result.outcome, "unknown")
|
|
self.assertIn("unzipped, not the archive", markdown)
|
|
self.assertIn("`scph5501.bin` already held at `bios/Sony/PS/scph5501.bin`", markdown)
|
|
self.assertIn("`sub/declared.bin` declared and not held", markdown)
|
|
self.assertIn("`other.bin` not recognised", markdown)
|
|
self.assertNotIn("where the dump comes from", markdown)
|
|
self.assertNotIn("Place it in", markdown)
|
|
|
|
def test_a_duplicate_of_a_known_file_is_reported(self):
|
|
"""A copy of a held file passed, and was sent to the folder that
|
|
already holds it."""
|
|
rel = self._bios_file("elsewhere/dupe.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
db = {"files": {probe.sha1: {"path": "bios/Sony/PS/original.bin"}}}
|
|
known = {**EMPTY_HASHES, "sha1": {probe.sha1},
|
|
"folders": {probe.sha1: {"bios/Sony/PS/"}}}
|
|
result = validate_pr.validate_file(rel, db, known)
|
|
markdown = result.to_markdown()
|
|
self.assertEqual(result.outcome, "held")
|
|
self.assertIn("### REVIEW", markdown)
|
|
self.assertIn("Already held at `bios/Sony/PS/original.bin`: nothing to add", markdown)
|
|
self.assertNotIn("not under bios/", markdown)
|
|
# A duplicate is worth saying out loud but must not block the PR.
|
|
self.assertTrue(result.passed)
|
|
|
|
def test_a_copy_in_a_tree_read_by_path_is_kept(self):
|
|
rel = self._bios_file("bios/ScummVM/a/dupe.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
db = {"files": {probe.sha1: {"path": "bios/ScummVM/b/dupe.bin"}}}
|
|
self.assertNotEqual(validate_pr.validate_file(rel, db, EMPTY_HASHES).outcome, "held")
|
|
|
|
def test_the_file_the_database_holds_is_not_its_own_duplicate(self):
|
|
"""The documented example on a tracked file warned "Duplicate:
|
|
identical file already exists at" the very path being validated."""
|
|
rel = self._bios_file("bios/Sony/PS/held.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
db = {"files": {probe.sha1: {"path": rel}}}
|
|
markdown = validate_pr.validate_file(rel, db, EMPTY_HASHES).to_markdown()
|
|
self.assertNotIn("Duplicate", markdown)
|
|
self.assertIn("Already in the database at this path", markdown)
|
|
|
|
|
|
class PlacementBySystem(_Fixture):
|
|
"""Depth alone passed a PS1 BIOS filed under bios/Nintendo/Game Boy/:
|
|
a declared hash names its system, and the files of that system already
|
|
held name its folder."""
|
|
|
|
def _known(self, sha1: str) -> dict:
|
|
return {"sha1": {sha1}, "md5": set(), "names": set(),
|
|
"folders": {sha1: {"bios/Sony/PlayStation/"}}}
|
|
|
|
def test_a_file_under_another_maker_is_told_where_its_system_lives(self):
|
|
rel = self._bios_file("bios/Nintendo/Game Boy/scph.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
markdown = validate_pr.validate_file(rel, None, self._known(probe.sha1)).to_markdown()
|
|
self.assertIn("bios/Sony/PlayStation/", markdown)
|
|
self.assertNotIn("Correct placement", markdown)
|
|
|
|
def test_the_folder_of_its_system_passes(self):
|
|
rel = self._bios_file("bios/Sony/PlayStation/scph.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
markdown = validate_pr.validate_file(rel, None, self._known(probe.sha1)).to_markdown()
|
|
self.assertIn("Correct placement: bios/Sony/PlayStation/", markdown)
|
|
|
|
def test_systems_spread_over_makers_name_no_home(self):
|
|
platforms = self.tmp / "platforms"
|
|
platforms.mkdir()
|
|
(platforms / "_registry.yml").write_text(
|
|
"platforms:\n p:\n config: p.yml\n status: active\n", encoding="utf-8")
|
|
(platforms / "p.yml").write_text(
|
|
"platform: P\nsystems:\n arcade:\n files:\n"
|
|
" - {name: a.zip, sha1: aaaa}\n - {name: b.zip, sha1: bbbb}\n",
|
|
encoding="utf-8",
|
|
)
|
|
db = {"files": {"aaaa": {"path": "bios/Arcade/MAME/a.zip"},
|
|
"bbbb": {"path": "bios/Apple/Apple II/b.zip"}},
|
|
"indexes": {"by_md5": {}}}
|
|
known = validate_pr.load_platform_hashes(str(platforms), db)
|
|
self.assertEqual(known["folders"], {})
|
|
|
|
|
|
class Placement(_Fixture):
|
|
def test_manufacturer_and_console_directories_are_accepted(self):
|
|
rel = self._bios_file("bios/Sega/Saturn/sega_101.bin")
|
|
self.assertIn(
|
|
"Correct placement: bios/Sega/Saturn/",
|
|
validate_pr.validate_file(rel, None, EMPTY_HASHES).to_markdown(),
|
|
)
|
|
|
|
def test_a_file_at_the_bios_root_is_flagged(self):
|
|
rel = self._bios_file("bios/loose.bin")
|
|
self.assertIn(
|
|
"bios/Manufacturer/Console/",
|
|
validate_pr.validate_file(rel, None, EMPTY_HASHES).to_markdown(),
|
|
)
|
|
|
|
def test_a_file_outside_bios_is_flagged(self):
|
|
rel = self._bios_file("elsewhere/loose.bin")
|
|
self.assertIn(
|
|
"not under bios/",
|
|
validate_pr.validate_file(rel, None, EMPTY_HASHES).to_markdown(),
|
|
)
|
|
|
|
|
|
class Reporting(_Fixture):
|
|
def test_markdown_carries_every_digest_and_the_verdict(self):
|
|
rel = self._bios_file("bios/Sega/Saturn/sega.bin")
|
|
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
|
|
result = validate_pr.validate_file(rel, None, {**EMPTY_HASHES, "sha1": {probe.sha1}})
|
|
markdown = result.to_markdown()
|
|
for value in (result.sha1, result.md5, result.crc32):
|
|
self.assertIn(value, markdown)
|
|
self.assertIn("### PASS", markdown)
|
|
|
|
def test_a_failure_is_marked_in_the_heading(self):
|
|
rel = self._bios_file("bios/Sega/Saturn/bad.exe")
|
|
self.assertIn("### FAIL", validate_pr.validate_file(rel, None, EMPTY_HASHES).to_markdown())
|
|
|
|
@unittest.skipUnless((REPO_ROOT / "database.json").is_file(), "database.json is not built")
|
|
def test_the_status_line_names_what_is_left_to_review(self):
|
|
import shutil
|
|
import subprocess
|
|
|
|
held = REPO_ROOT / "bios" / "Sony" / "PlayStation Portable" / "ppge_atlas.zim"
|
|
if not held.is_file():
|
|
self.skipTest(f"{held} is not here")
|
|
shutil.copy(held, self.tmp / "ppge_atlas.zim")
|
|
(self.tmp / "mystery.bin").write_bytes(os.urandom(4096))
|
|
proc = subprocess.run(
|
|
[sys.executable, str(REPO_ROOT / "scripts" / "validate_pr.py"), "--markdown",
|
|
str(self.tmp / "mystery.bin"), str(self.tmp / "ppge_atlas.zim")],
|
|
capture_output=True, text=True, timeout=300, check=False,
|
|
)
|
|
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
self.assertIn("**Status**: no check failed, to review: 1 already held, "
|
|
"1 not recognised", proc.stdout)
|
|
self.assertNotIn("all checks passed", proc.stdout)
|
|
|
|
|
|
class PlatformHashLoading(_Fixture):
|
|
def test_missing_directory_yields_empty_sets(self):
|
|
known = validate_pr.load_platform_hashes(str(self.tmp / "absent"))
|
|
self.assertEqual(known, {"sha1": set(), "md5": set(), "sha256": set(), "crc32": set(),
|
|
"names": set(), "folders": {}, "sizes": {}, "entries": {},
|
|
"named": {}})
|
|
|
|
def test_declared_hashes_and_names_are_collected(self):
|
|
platforms = self.tmp / "platforms"
|
|
platforms.mkdir()
|
|
(platforms / "_registry.yml").write_text(
|
|
"platforms:\n demo:\n status: active\n"
|
|
)
|
|
(platforms / "demo.yml").write_text(
|
|
"platform: Demo\n"
|
|
"systems:\n"
|
|
" test-sys:\n"
|
|
" files:\n"
|
|
" - name: boot.bin\n"
|
|
f" sha1: {'a' * 40}\n"
|
|
f" md5: {'b' * 32}\n"
|
|
)
|
|
known = validate_pr.load_platform_hashes(str(platforms))
|
|
self.assertIn("a" * 40, known["sha1"])
|
|
self.assertIn("b" * 32, known["md5"])
|
|
self.assertIn("boot.bin", known["names"])
|
|
|
|
|
|
class ThePlatformsOwnCheckDecides(_Fixture):
|
|
"""Batocera pins a ROM inside adam_fdc.zip, Recalbox hashes a ZIP's
|
|
members, RetroDECK declares a sha256: the container's sha1 and md5 are
|
|
in none of those lists, and a file verify counts OK was reported as an
|
|
unknown variant to be filed under .variants/."""
|
|
|
|
def _platforms(self, registry: str, systems: str) -> dict:
|
|
platforms = self.tmp / "platforms"
|
|
platforms.mkdir()
|
|
(platforms / "_registry.yml").write_text(
|
|
f"platforms:\n p:\n config: p.yml\n status: active\n{registry}",
|
|
encoding="utf-8")
|
|
(platforms / "p.yml").write_text(f"platform: P\nsystems:\n s:\n files:\n{systems}",
|
|
encoding="utf-8")
|
|
return validate_pr.load_platform_hashes(str(platforms))
|
|
|
|
def _zip(self) -> str:
|
|
import zipfile # noqa: PLC0415
|
|
|
|
path = self.tmp / "contrib" / "set.zip"
|
|
path.parent.mkdir()
|
|
with zipfile.ZipFile(path, "w") as archive:
|
|
archive.writestr("rom.u10", b"ROM")
|
|
return "contrib/set.zip"
|
|
|
|
def _verdict(self, path: str, known: dict) -> str:
|
|
return validate_pr.validate_file(path, None, known).to_markdown()
|
|
|
|
def test_a_member_pinned_by_zipped_file(self):
|
|
import hashlib # noqa: PLC0415
|
|
|
|
md5 = hashlib.md5(b"ROM").hexdigest()
|
|
known = self._platforms(
|
|
" verification_mode: md5\n",
|
|
f" - {{name: set.zip, md5: {md5}, zipped_file: rom.u10}}\n")
|
|
markdown = self._verdict(self._zip(), known)
|
|
self.assertIn("Accepted by the platform's own check: p", markdown)
|
|
self.assertNotIn(".variants/", markdown)
|
|
|
|
def test_a_member_digest(self):
|
|
from hashing import md5_composite # noqa: PLC0415
|
|
|
|
path = self._zip()
|
|
known = self._platforms(
|
|
" verification_mode: md5\n zip_digest: composite\n",
|
|
f" - {{name: set.zip, md5: {md5_composite(path)}}}\n")
|
|
self.assertIn("Accepted by the platform's own check: p", self._verdict(path, known))
|
|
|
|
def test_a_sha256(self):
|
|
import hashlib # noqa: PLC0415
|
|
|
|
path = self._zip()
|
|
digest = hashlib.sha256((self.tmp / path).read_bytes()).hexdigest()
|
|
known = self._platforms(" verification_mode: md5\n",
|
|
f" - {{name: set.zip, sha256: {digest}}}\n")
|
|
self.assertIn("Hash matches known platform requirement", self._verdict(path, known))
|
|
|
|
def test_an_identical_copy_names_the_folder(self):
|
|
db = {"files": {"m" * 40: {"path": "bios/Arcade/MAME/set.zip"},
|
|
"c" * 40: {"path": "bios/Coleco/Adam/set.zip"}},
|
|
"indexes": {"by_name": {"set.zip": ["m" * 40, "c" * 40]}}}
|
|
self.assertEqual(validate_pr._variant_home(db, "set.zip", "c" * 40), "bios/Coleco/Adam/")
|
|
self.assertEqual(validate_pr._variant_home(db, "set.zip"), "bios/Arcade/MAME/")
|
|
|
|
def test_another_member_is_still_a_variant(self):
|
|
known = self._platforms(
|
|
" verification_mode: md5\n",
|
|
f" - {{name: set.zip, md5: {'0' * 32}, zipped_file: rom.u10}}\n")
|
|
self.assertIn("may be a variant", self._verdict(self._zip(), known))
|
|
|
|
|
|
class FlagsAreAppliedOrRefused(unittest.TestCase):
|
|
"""Files named beside --changed were dropped for the git diff and the run
|
|
said "No changed BIOS files detected" about files it never read; --json
|
|
beside --markdown printed JSON and dropped the markdown."""
|
|
|
|
def _run(self, *argv: str):
|
|
import subprocess # noqa: PLC0415
|
|
|
|
return subprocess.run(
|
|
[sys.executable, "scripts/validate_pr.py", *argv], cwd=REPO_ROOT,
|
|
capture_output=True, text=True, timeout=300, check=False,
|
|
)
|
|
|
|
def test_files_beside_changed_are_refused(self):
|
|
result = self._run("--changed", "bios/Sony/PlayStation/scph5501.bin", "--json")
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("--changed", result.stderr)
|
|
|
|
def test_two_output_formats_are_refused(self):
|
|
result = self._run("--json", "--markdown", "bios/Sony/PlayStation/scph5501.bin")
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("not allowed with", result.stderr)
|
|
|
|
def test_a_failed_file_is_not_reported_new(self):
|
|
"""A path that does not exist came out as "outcome": "new", and a
|
|
reader listing new contributions announced a BIOS nobody sent."""
|
|
import json # noqa: PLC0415
|
|
|
|
result = self._run("--json", "bios/nope.bin")
|
|
report = json.loads(result.stdout)
|
|
self.assertEqual([(r["passed"], r["outcome"]) for r in report], [(False, "failed")])
|
|
|
|
|
|
|
|
class AFileOutsideBiosIsToldWhereToGo(unittest.TestCase):
|
|
"""validate_pr knew bios/Sony/PlayStation/ for scph5501.bin and only said
|
|
"not under bios/". Given an absolute path, a placed file was reported a
|
|
duplicate of itself."""
|
|
|
|
def test_both(self):
|
|
import os as _os
|
|
import tempfile as _tempfile
|
|
|
|
from validate_pr import validate_file
|
|
|
|
content = b"bios bytes"
|
|
import hashlib as _hashlib
|
|
sha1 = _hashlib.sha1(content).hexdigest()
|
|
md5 = _hashlib.md5(content).hexdigest()
|
|
hashes = {"sha1": {sha1}, "md5": {md5}, "names": set(),
|
|
"folders": {sha1: {"bios/Sony/PlayStation/"}}}
|
|
with _tempfile.TemporaryDirectory() as directory:
|
|
loose = _os.path.join(directory, "scph5501.bin")
|
|
with open(loose, "wb") as handle:
|
|
handle.write(content)
|
|
result = validate_file(loose, None, hashes)
|
|
self.assertTrue(any("bios/Sony/PlayStation/" in m for _s, m in result.checks))
|
|
|
|
previous = _os.getcwd()
|
|
with _tempfile.TemporaryDirectory() as root:
|
|
placed = _os.path.join(root, "bios", "Sony", "PlayStation", "scph5501.bin")
|
|
_os.makedirs(_os.path.dirname(placed))
|
|
with open(placed, "wb") as handle:
|
|
handle.write(content)
|
|
db = {"files": {sha1: {"path": "bios/Sony/PlayStation/scph5501.bin"}}}
|
|
_os.chdir(root)
|
|
try:
|
|
result = validate_file(placed, db, hashes)
|
|
finally:
|
|
_os.chdir(previous)
|
|
self.assertFalse(any("Duplicate" in m for _s, m in result.checks))
|
|
|
|
|
|
class ProfileHashesReadAsTheResolverDoes(unittest.TestCase):
|
|
"""A file a profile names by crc32 alone (clk's Electron os.rom, 626
|
|
entries) or by a comma list of sha256 (mesence's BS-X.bin) was reported
|
|
as referenced nowhere, needing manual review."""
|
|
|
|
def test_crc32_only_and_comma_lists_are_indexed(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
Path(tmp, "core.yml").write_text(
|
|
"emulator: Core\ntype: libretro\nsystems: [x]\nfiles:\n"
|
|
" - name: os.rom\n crc32: BF63FB1F\n size: 16384\n"
|
|
" - name: bsx.bin\n sha256: \"aa,bb\"\n",
|
|
encoding="utf-8",
|
|
)
|
|
known = validate_pr.load_profile_hashes(tmp)
|
|
self.assertEqual(known["crc32"][("bf63fb1f", 16384)], {"core"})
|
|
self.assertIn("bb", known["sha256"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|