Files
libretro/tests/test_path_conflict_status.py
T

343 lines
14 KiB
Python

"""A declaration that conflicts with a packed path is counted by what it resolves to.
RetroArch declares SGB1.sfc as a file and SGB1.sfc/<rom> as files inside it.
The builder ships one shape and counted every conflicting declaration OK,
resolved or not; verify.py resolves each on its own. A required file absent
from the collection read as covered in the pack report and as missing in verify.
"""
from __future__ import annotations
import contextlib
import hashlib
import io
import re
import sys
import tempfile
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import common # noqa: E402
import generate_db # noqa: E402
import generate_pack as builder # noqa: E402
from verify import verify_platform # noqa: E402
class ConflictingDeclarations(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
bios = self.root / "bios" / "Nintendo"
self.platforms = self.root / "platforms"
self.emulators = self.root / "emulators"
for directory in (bios, self.platforms, self.emulators):
directory.mkdir(parents=True)
payload = b"super game boy"
(bios / "SGB1.sfc").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
files = {
sha1: {
"path": str(bios / "SGB1.sfc"), "name": "SGB1.sfc",
"size": len(payload), "sha1": sha1,
"md5": hashlib.md5(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000001",
}
}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
platform = {
"platform": "Conflict",
"verification_mode": "existence",
"base_destination": "system",
"cores": [],
"systems": {
"nintendo-sgb": {"files": [
{"name": "SGB1.sfc", "destination": "SGB1.sfc", "required": True},
{"name": "nothere.rom", "destination": "SGB1.sfc/nothere.rom",
"required": True},
]},
},
}
(self.platforms / "conflict.yml").write_text(yaml.dump(platform))
(self.platforms / "_registry.yml").write_text(
yaml.dump({"platforms": {"conflict": {"status": "active"}}})
)
common._platform_config_cache.clear()
common._emulator_profiles_cache.clear()
def tearDown(self):
common._platform_config_cache.clear()
self._tmp.cleanup()
def test_the_pack_and_verify_count_alike(self):
out = self.root / "dist"
out.mkdir()
report = io.StringIO()
with contextlib.redirect_stdout(report):
builder.generate_pack(
"conflict", str(self.platforms), self.db, str(self.root / "bios"),
str(out), emulators_dir=str(self.emulators), emu_profiles={},
offline=True,
)
packed = re.search(r"(\d+)/(\d+) files OK", report.getvalue())
self.assertIsNotNone(packed, report.getvalue())
config = common.load_platform_config("conflict", str(self.platforms))
verified = verify_platform(config, self.db, str(self.emulators))
counts = verified["status_counts"]
self.assertEqual(counts.get("missing"), 1)
self.assertEqual(
(int(packed.group(1)), int(packed.group(2))),
(counts.get("ok", 0), verified["total_files"]),
)
class AnExtraDoesNotPassAHashCheck(unittest.TestCase):
"""A md5 platform declares scph1001.bin with a hash the collection lacks;
a core declares our dump at the same path. The builder ships the core's
file and called the declaration OK, while the frontend, which hashes the
file, rejects it and verify reports it untested."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
bios = self.root / "bios" / "Sony"
self.platforms = self.root / "platforms"
self.emulators = self.root / "emulators"
for directory in (bios, self.platforms, self.emulators):
directory.mkdir(parents=True)
payload = b"our psx dump"
(bios / "psx-us.bin").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
md5 = hashlib.md5(payload).hexdigest()
files = {
sha1: {
"path": str(bios / "psx-us.bin"), "name": "psx-us.bin",
"size": len(payload), "sha1": sha1, "md5": md5,
"sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000002",
}
}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
platform = {
"platform": "Hashed",
"verification_mode": "md5",
"cores": ["fxcore"],
"systems": {"sony-playstation": {"files": [
{"name": "scph1001.bin", "destination": "scph1001.bin",
"md5": "0123456789abcdef0123456789abcdef", "required": True},
]}},
}
(self.platforms / "hashed.yml").write_text(yaml.dump(platform))
(self.platforms / "_registry.yml").write_text(
yaml.dump({"platforms": {"hashed": {"status": "active"}}})
)
profile = {
"emulator": "FX", "type": "libretro", "cores": ["fxcore"],
"systems": ["sony-playstation"],
"files": [{"name": "psx-us.bin", "path": "scph1001.bin", "md5": md5,
"required": True}],
}
(self.emulators / "fxcore.yml").write_text(yaml.dump(profile))
common._platform_config_cache.clear()
common._emulator_profiles_cache.clear()
def tearDown(self):
common._platform_config_cache.clear()
common._emulator_profiles_cache.clear()
self._tmp.cleanup()
def test_the_declaration_stays_failed(self):
out = self.root / "dist"
out.mkdir()
report = io.StringIO()
with contextlib.redirect_stdout(report):
builder.generate_pack(
"hashed", str(self.platforms), self.db, str(self.root / "bios"),
str(out), emulators_dir=str(self.emulators), offline=True,
)
packed = re.search(r"(\d+)/(\d+) files OK", report.getvalue())
self.assertIsNotNone(packed, report.getvalue())
config = common.load_platform_config("hashed", str(self.platforms))
verified = verify_platform(config, self.db, str(self.emulators))
self.assertEqual(verified["status_counts"].get("ok", 0), 0)
self.assertEqual(int(packed.group(1)), 0, report.getvalue())
class TheEmulatorPackShipsWhatTheCoreAccepts(unittest.TestCase):
"""BasiliskII wants a 512 KB to 1 MB ROM. The only ROM held was 128 KB:
the emulator pack shipped it silently and verify --emulator announced
1/1 OK above the line saying the core would refuse it."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.root = Path(self._tmp.name)
bios = self.root / "bios" / "Apple"
self.emulators = self.root / "emulators"
for directory in (bios, self.emulators):
directory.mkdir(parents=True)
payload = b"r" * 131072
(bios / "ROM").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
files = {sha1: {
"path": str(bios / "ROM"), "name": "ROM", "size": len(payload),
"sha1": sha1, "md5": hashlib.md5(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000003",
}}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
profile = {
"emulator": "Mac", "type": "libretro", "cores": ["mac"],
"systems": ["apple-macintosh"],
"files": [{"name": "ROM", "required": True, "min_size": 524288,
"max_size": 1048576, "validation": ["size"]}],
}
(self.emulators / "mac.yml").write_text(yaml.dump(profile))
common._emulator_profiles_cache.clear()
def tearDown(self):
common._emulator_profiles_cache.clear()
self._tmp.cleanup()
def test_the_refused_rom_is_named_not_shipped(self):
import zipfile
out = self.root / "dist"
out.mkdir()
report = io.StringIO()
with contextlib.redirect_stdout(report):
zip_path = builder.generate_emulator_pack(
["mac"], str(self.emulators), self.db, str(self.root / "bios"),
str(out), offline=True,
)
self.assertIn("REFUSED: ROM", report.getvalue())
with zipfile.ZipFile(zip_path) as archive:
self.assertNotIn("ROM", archive.namelist())
def test_verify_does_not_call_it_ok(self):
from verify import Severity, verify_emulator
result = verify_emulator(["mac"], str(self.emulators), self.db)
self.assertEqual(result["severity_counts"][Severity.OK], 0)
class AlternativesAgreeWithThePack(unittest.TestCase):
"""Two versions declared at one path: the held one has a contradicted
md5, the other is not held. The pack ships the held copy with a
discrepancy; verify took the worst status and said missing."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self.root = Path(self._tmp.name)
bios = self.root / "bios"
self.emulators = self.root / "emulators"
for directory in (bios, self.emulators):
directory.mkdir(parents=True)
payload = b"held version"
(bios / "game.grp").write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
files = {sha1: {
"path": str(bios / "game.grp"), "name": "game.grp", "size": len(payload),
"sha1": sha1, "md5": hashlib.md5(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000004",
}}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
profile = {
"emulator": "Eng", "type": "libretro", "cores": ["eng"], "systems": ["x"],
"files": [
{"name": "game.grp", "path": "slot.grp", "required": True, "md5": "0" * 32},
{"name": "other.grp", "path": "slot.grp", "required": True},
],
}
(self.emulators / "eng.yml").write_text(yaml.dump(profile))
common._emulator_profiles_cache.clear()
def tearDown(self):
common._emulator_profiles_cache.clear()
self._tmp.cleanup()
def test_verify_counts_what_the_pack_ships(self):
import zipfile
from verify import Status, verify_emulator
out = self.root / "dist"
out.mkdir()
with contextlib.redirect_stdout(io.StringIO()):
zip_path = builder.generate_emulator_pack(
["eng"], str(self.emulators), self.db, str(self.root / "bios"),
str(out), offline=True,
)
with zipfile.ZipFile(zip_path) as archive:
self.assertIn("slot.grp", archive.namelist())
result = verify_emulator(["eng"], str(self.emulators), self.db)
self.assertEqual(result["status_counts"].get(Status.MISSING, 0), 0)
self.assertEqual(result["status_counts"].get(Status.UNTESTED), 1)
class AMaskedArchiveIsMissing(unittest.TestCase):
"""Profile a ships a loose pack.zip; profile b needs pack.zip as an
archive holding a member a's copy lacks. The pack skipped b's archive
in silence and verify counted it OK on b's own copy."""
def setUp(self):
import zipfile
self._tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
self.root = Path(self._tmp.name)
self.emulators = self.root / "emulators"
files = {}
for folder, members in (("A", {"x.bin": b"x"}), ("B", {"x.bin": b"x", "y.bin": b"y"})):
target = self.root / "bios" / folder / "pack.zip"
target.parent.mkdir(parents=True)
with zipfile.ZipFile(target, "w") as archive:
for name, data in members.items():
archive.writestr(name, data)
payload = target.read_bytes()
sha1 = hashlib.sha1(payload).hexdigest()
files[sha1] = {"path": str(target), "name": "pack.zip", "size": len(payload),
"sha1": sha1, "md5": hashlib.md5(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(), "crc32": "0"}
self.sha1_b = sha1
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
self.emulators.mkdir()
a_sha1 = next(k for k in files if k != sha1)
(self.emulators / "a.yml").write_text(yaml.dump({
"emulator": "A", "type": "libretro", "cores": ["a"], "systems": ["x"],
"files": [{"name": "pack.zip", "required": True, "sha1": a_sha1}],
}))
(self.emulators / "b.yml").write_text(yaml.dump({
"emulator": "B", "type": "libretro", "cores": ["b"], "systems": ["x"],
"files": [{"name": "y.bin", "archive": "pack.zip", "required": True,
"crc32": f"{__import__('zlib').crc32(b'y'):08x}"}],
}))
common._emulator_profiles_cache.clear()
def tearDown(self):
common._emulator_profiles_cache.clear()
self._tmp.cleanup()
def test_both_report_the_archive_missing(self):
from verify import Status, verify_emulator
out = self.root / "dist"
out.mkdir()
report = io.StringIO()
with contextlib.redirect_stdout(report):
builder.generate_emulator_pack(
["a", "b"], str(self.emulators), self.db, str(self.root / "bios"),
str(out), offline=True,
)
self.assertIn("pack.zip", report.getvalue().split("MISSING", 1)[-1])
result = verify_emulator(["a", "b"], str(self.emulators), self.db)
statuses = [d["status"] for d in result["details"] if d["name"] == "pack.zip"]
self.assertIn(Status.MISSING, statuses)
if __name__ == "__main__":
unittest.main()