Files
libretro/tests/test_profile_checks.py
T

209 lines
8.8 KiB
Python

"""A profile's checks are the ones the emulator's code runs.
Each case reads a profile from emulators/ and offers its entry files of
chosen sizes or contents, then compares the verdict with what the code at
the profile's pin does with the same file. The cases come from reading that
code: a check the code does not run rejects a file the emulator loads, and a
check left out lets through a file the emulator refuses.
"""
from __future__ import annotations
import sys
import tempfile
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from validation import _build_validation_index, check_file_validation # noqa: E402
def _profile(name: str) -> dict:
with open(REPO_ROOT / "emulators" / f"{name}.yml", encoding="utf-8") as fh:
return yaml.safe_load(fh)
class _Probe(unittest.TestCase):
"""Offer a profile entry a file and read the verdict."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
def accepts(self, emulator: str, name: str, data: bytes, strict: bool = False) -> bool:
"""strict counts a check the code only warns about."""
index = _build_validation_index({emulator: _profile(emulator)})
path = Path(self._tmp.name) / name
path.write_bytes(data)
return check_file_validation(
str(path), name, index, emulators={emulator}, strict=strict
) is None
class BeebEmTeletextFont(_Probe):
"""BuildMode7Font reads 96 characters of 18 two-byte rows, 3456 bytes,
and returns false at the first EOF, which stops BeebEm at startup;
bytes past them are never read (Src/Video.cpp:167-215 at a982ba6)."""
def test_short_is_refused_and_longer_is_read(self):
self.assertFalse(self.accepts("beebem", "Teletext.fnt", b"\0" * 100))
self.assertTrue(self.accepts("beebem", "Teletext.fnt", b"\0" * 3456))
self.assertTrue(self.accepts("beebem", "Teletext.fnt", b"\0" * 4000))
class BeebEmSystemRoms(_Probe):
"""The OS, Tube, Z80 and Master 512 ROMs are read with one fread of the
slot's size. A short read reports an error and BeebEm runs with what it
read; bytes past the slot are never read (Src/BeebMem.cpp:1291-1308,
Src/Tube.cpp:1540-1553, Src/Z80Support.cpp:247-261,
Src/Master512CoPro.cpp:3777-3790 at a982ba6)."""
CASES = (("OS12.rom", 16384), ("B+MOS.rom", 16384), ("MOS.rom", 16384),
("MOS400.rom", 16384), ("6502Tube.rom", 2048), ("Z80.rom", 4096),
("BIOS.rom", 16384))
def test_longer_is_read_and_shorter_only_warns(self):
for name, size in self.CASES:
with self.subTest(name=name):
self.assertTrue(self.accepts("beebem", name, b"\0" * (size * 2)))
self.assertTrue(self.accepts("beebem", name, b"\0" * (size // 2)))
self.assertFalse(
self.accepts("beebem", name, b"\0" * (size // 2), strict=True)
)
class BeebEmSidewaysRoms(_Probe):
"""ReadRom loads any file up to MAX_PALROM_SIZE (131072) into a bank and
refuses a larger one, leaving the bank empty (Src/BeebMem.cpp:1207-1262,
Src/PALRom.h:52 at a982ba6)."""
def test_up_to_128k_is_read_and_larger_refused(self):
for size in (8192, 16384, 32768, 131072):
with self.subTest(size=size):
self.assertTrue(self.accepts("beebem", "BASIC2.rom", b"\0" * size))
self.assertFalse(self.accepts("beebem", "BASIC2.rom", b"\0" * 200000))
class BeetleLynxBootRom(_Probe):
"""CRom drops a boot ROM under 512 bytes and keeps the 0x88 fill, so no
game boots; a longer one is read for its first 512 bytes
(mednafen/lynx/rom.cpp:61-75 at fcdefcf)."""
def test_short_is_refused_and_longer_is_read(self):
self.assertFalse(self.accepts("beetle_lynx", "lynxboot.img", b"\0" * 256))
self.assertTrue(self.accepts("beetle_lynx", "lynxboot.img", b"\0" * 512))
self.assertTrue(self.accepts("beetle_lynx", "lynxboot.img", b"\0" * 1024))
class BeetleGbaBios(_Probe):
"""CPUInit copies the BIOS only when it is exactly 0x4000 bytes and
otherwise falls back to HLE (mednafen/gba/GBA.cpp:2639-2673 at
b158166)."""
def test_only_16k_is_used(self):
self.assertTrue(self.accepts("beetle_gba", "gba_bios.bin", b"\0" * 16384))
for size in (1000, 32768):
with self.subTest(size=size):
self.assertFalse(self.accepts("beetle_gba", "gba_bios.bin", b"\0" * size))
class Atari800SystemRoms(_Probe):
"""SYSROM_FindInDir takes a file of an allowed length whose CRC32 is in
SYSROM_roms, else, by its lowercased name, any file of that length as a
custom ROM (atari800/src/sysrom.c:88-112, 281-315, 387-416 at 4e7fbc7).
BB01R4_OS.ROM and XEGAME.ROM are on no name list: only their CRC32 makes
them that ROM."""
def test_named_roms_need_only_the_length(self):
for name, size in (("ATARIOSA.ROM", 10240), ("ATARIOSB.ROM", 10240),
("ATARIXL.ROM", 16384), ("5200.rom", 2048),
("ATARIBAS.ROM", 8192)):
with self.subTest(name=name):
self.assertTrue(self.accepts("atari800", name, b"\1" * size))
self.assertFalse(self.accepts("atari800", name, b"\1" * (size + 1)))
def test_unlisted_names_need_their_crc(self):
for name, size in (("BB01R4_OS.ROM", 16384), ("XEGAME.ROM", 8192)):
with self.subTest(name=name):
self.assertFalse(self.accepts("atari800", name, b"\1" * size))
class AresAtari5200Bios(unittest.TestCase):
"""ares registers an Atari 5200 firmware by sha256 and refuses to load a
game without it (desktop-ui/emulator/atari-5200.cpp:12-16, 82-88 at
4cb8d92); the profile left the system out, so no pack carried the BIOS."""
def test_the_profile_declares_it(self):
profile = _profile("ares")
self.assertIn("atari-5200", profile["systems"])
entry = next(f for f in profile["files"] if f.get("system") == "atari-5200")
self.assertTrue(entry["required"])
self.assertEqual(
entry["sha256"],
"06b250f18983d058c0f156ce7ee88ae48b6eaf11e6f10f21dccf6ac7ffb6a6af",
)
class A5200Bios(_Probe):
"""The core reads 0x800 bytes and falls back to the Altirra OS on a short
read; bytes past them are never read (libretro/libretro.c:456-468 at
40c6f2f)."""
def test_short_falls_back_and_longer_is_read(self):
self.assertFalse(self.accepts("a5200", "5200.rom", b"\0" * 1024))
self.assertTrue(self.accepts("a5200", "5200.rom", b"\0" * 2048))
self.assertTrue(self.accepts("a5200", "5200.rom", b"\0" * 4096))
class BeetlePceSystemCards(_Probe):
"""PCE_LoadCD hands the file to HuC_Load, which skips a 512-byte copier
header and checks no hash (mednafen/pce/pce.cpp:441-457,
mednafen/pce/huc.cpp:185-202 at b96c11e). The profile carried md5 and
sha1 values no code holds, and a headered card the core boots was
reported as a hash discrepancy."""
def test_a_headered_card_is_not_judged(self):
self.assertTrue(self.accepts("beetle_pce", "syscard3.pce", b"\0" * 262656))
def test_no_hash_the_code_does_not_hold(self):
for entry in _profile("beetle_pce")["files"]:
with self.subTest(name=entry["name"]):
self.assertFalse({"md5", "sha1", "crc32", "sha256"} & set(entry))
class EightyOneRoms(_Probe):
"""memory_load reads the ROM and CRC32Block's value only picks the tape
patches in rompatch.c; a ROM the table does not know loads without them
(81/zx81/zx81.c:102-103, 81/Ace/ace.c:77-78, 81/zx81/rompatch.c at
fa70949). The profile declared a crc32 check with no value."""
def test_an_unknown_rom_is_not_refused(self):
for name, size in (("zx81.edition3.rom", 8192), ("zx80.rom", 4096),
("jupiterace.rom", 8192)):
with self.subTest(name=name):
self.assertTrue(self.accepts("81", name, b"\1" * size))
entry = next(f for f in _profile("81")["files"] if f["name"] == name)
self.assertNotIn("crc32", entry.get("validation") or [])
class AdvanceMameArchives(_Probe):
"""verify_length_and_hash checks each member and only adds a warning; the
archive itself has no size or hash anyone checks (src/romload.c:327-363
at 21f8f6f). The 26 BIOS archives declared archive-level checks with no
archive values."""
def test_no_archive_level_check(self):
for entry in _profile("advancemame")["files"]:
if entry.get("category") == "bios_zip":
with self.subTest(name=entry["name"]):
self.assertFalse(entry.get("validation"))
self.assertTrue(entry.get("contents"))
if __name__ == "__main__":
unittest.main()