mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-20 10:23:16 -05:00
Profiles, the wiki, comments, docstrings, command output and schema descriptions carried em-dashes, clause-joining semicolons, unicode arrows, box-drawing banners and check-mark glyphs. Each sentence was rewritten rather than having its character swapped. Hashes, paths, source_ref values and every cited file:line are unchanged, which a comparison of each parsed profile against the previous revision confirms. validate_pr comments now say PASS and FAIL in words, and source_ref values in a profile's structured blocks render as code. test_prose_style fails if any of these comes back, in tracked files or in the generated site.
343 lines
14 KiB
Python
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, while 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, but 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 while 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()
|