mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-21 11:19:11 -05:00
PCSX2x6 reads bin/resources/GameIndex.yaml, an arcade database keyed by NM gameid. The shortened tail resources/GameIndex.yaml matched four files, and the resolver set aside the three in other emulators' trees before measuring ambiguity, so PCSX2's console database passed for path proof and shipped in the PCSX2x6 pack. Ambiguity is now read before that filtering. A tail indexed from an identical copy is kept even when the database records the other copy's path. PCSX2x6's GameIndex.yaml and RedumpDatabase.yaml are collected from its repository at the pinned 583f0e90 (git blobsed8232e3and8669b3af).
256 lines
10 KiB
Python
256 lines
10 KiB
Python
"""Which tails of a stored path can answer a declared destination."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
|
|
|
from common import resolve_local_file # noqa: E402
|
|
from generate_db import _path_suffixes # noqa: E402
|
|
|
|
|
|
class TestPathSuffixes(unittest.TestCase):
|
|
"""Every tail down to two segments, never the bare filename."""
|
|
|
|
def test_longest_first_and_never_the_bare_name(self):
|
|
self.assertEqual(
|
|
_path_suffixes("bios/Nintendo/GameCube/GC/USA/IPL.bin"),
|
|
[
|
|
"Nintendo/GameCube/GC/USA/IPL.bin",
|
|
"GameCube/GC/USA/IPL.bin",
|
|
"GC/USA/IPL.bin",
|
|
"USA/IPL.bin",
|
|
],
|
|
)
|
|
|
|
def test_a_deep_tree_keeps_every_level(self):
|
|
# Seven 3DS system archives differ only past the sixth segment, so a
|
|
# single key per file merged all seven onto one.
|
|
suffixes = _path_suffixes(
|
|
"bios/Nintendo/3DS/Citra/nand/0000/title/0004009b/00014002/"
|
|
"content/00000000.app.romfs"
|
|
)
|
|
self.assertIn(
|
|
"nand/0000/title/0004009b/00014002/content/00000000.app.romfs",
|
|
suffixes,
|
|
)
|
|
self.assertEqual(suffixes[0].split("/")[0], "Nintendo")
|
|
|
|
def test_a_file_at_the_root_has_no_tail(self):
|
|
self.assertEqual(_path_suffixes("bios/thing.rom"), [])
|
|
|
|
def test_a_path_without_the_bios_prefix_is_still_split(self):
|
|
self.assertEqual(
|
|
_path_suffixes("Sony/PlayStation/scph5501.bin"),
|
|
["Sony/PlayStation/scph5501.bin", "PlayStation/scph5501.bin"],
|
|
)
|
|
|
|
def test_backslashes_and_empty_segments_are_normalised(self):
|
|
self.assertEqual(
|
|
_path_suffixes("bios\\\\Sega\\\\Saturn\\\\sat.bin"),
|
|
["Sega/Saturn/sat.bin", "Saturn/sat.bin"],
|
|
)
|
|
|
|
|
|
class TestResolutionAmbiguity(unittest.TestCase):
|
|
"""A tail that could name several files names none of them."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.root = Path(tempfile.mkdtemp(prefix="suffix-"))
|
|
cls.previous = os.getcwd()
|
|
cls.paths = {
|
|
"a" * 40: "bios/Other/emu_one/resources/shaders/convert.glsl",
|
|
"b" * 40: "bios/Other/emu_two/pcsx2/resources/shaders/convert.glsl",
|
|
"c" * 40: "bios/Other/emu_three/assets/sounds/unlock.wav",
|
|
"d" * 40: "bios/Vendor/Console/GC/JAP/IPL.bin",
|
|
}
|
|
for sha1, rel in cls.paths.items():
|
|
target = cls.root / rel
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_bytes(sha1[:4].encode())
|
|
files, by_name, by_suffix = {}, {}, {}
|
|
for sha1, rel in cls.paths.items():
|
|
name = rel.rsplit("/", 1)[-1]
|
|
files[sha1] = {"path": rel, "name": name, "md5": "", "size": 4,
|
|
"crc32": "", "sha1": sha1}
|
|
by_name.setdefault(name, []).append(sha1)
|
|
for suffix in _path_suffixes(rel):
|
|
if suffix != name:
|
|
by_suffix.setdefault(suffix, []).append(sha1)
|
|
cls.db = {
|
|
"files": files,
|
|
"indexes": {"by_md5": {}, "by_name": by_name, "by_crc32": {},
|
|
"by_path_suffix": by_suffix},
|
|
}
|
|
os.chdir(cls.root)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
os.chdir(cls.previous)
|
|
shutil.rmtree(cls.root, ignore_errors=True)
|
|
|
|
def _resolve(self, name: str, hint: str):
|
|
return resolve_local_file({"name": name}, self.db, dest_hint=hint)
|
|
|
|
def test_a_full_tail_claimed_twice_goes_to_the_closest_owner(self):
|
|
# emu_one stores it right at its destination while emu_two nests it one
|
|
# directory deeper. The one that IS the destination wins.
|
|
path, status = self._resolve(
|
|
"convert.glsl", "resources/shaders/convert.glsl"
|
|
)
|
|
self.assertEqual(status, "path_exact")
|
|
self.assertIn("emu_one", path)
|
|
|
|
def test_a_tail_two_segments_short_is_not_followed(self):
|
|
# "sounds/achievements/unlock.wav" would reach emu_three, which is a
|
|
# different emulator's tree, so the name step answers instead.
|
|
path, status = self._resolve(
|
|
"unlock.wav", "pcsx2/resources/sounds/unlock.wav"
|
|
)
|
|
self.assertEqual(status, "name_exact")
|
|
self.assertIn("emu_three", path)
|
|
|
|
def test_one_segment_of_descent_is_allowed(self):
|
|
path, status = self._resolve("IPL.bin", "system/GC/JAP/IPL.bin")
|
|
self.assertEqual(status, "path_exact")
|
|
self.assertIn("JAP", path)
|
|
|
|
def test_an_exact_tail_still_wins(self):
|
|
path, status = self._resolve("IPL.bin", "GC/JAP/IPL.bin")
|
|
self.assertEqual(status, "path_exact")
|
|
self.assertIn("JAP", path)
|
|
|
|
|
|
class TestSizeGateOnTheNameStep(unittest.TestCase):
|
|
"""A name answer must still be a plausible size for the entry."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.root = Path(tempfile.mkdtemp(prefix="sizegate-"))
|
|
cls.previous = os.getcwd()
|
|
small = cls.root / "bios/Apple/IIGS/ROM"
|
|
large = cls.root / "bios/Apple/Macintosh/ROM"
|
|
for path, payload in ((small, b"x" * 8), (large, b"y" * 64)):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(payload)
|
|
cls.db = {
|
|
"files": {
|
|
"a" * 40: {"path": "bios/Apple/IIGS/ROM", "name": "ROM",
|
|
"md5": "", "size": 8, "crc32": "", "sha1": "a" * 40},
|
|
"b" * 40: {"path": "bios/Apple/Macintosh/ROM", "name": "ROM",
|
|
"md5": "", "size": 64, "crc32": "", "sha1": "b" * 40},
|
|
},
|
|
"indexes": {
|
|
"by_md5": {}, "by_crc32": {}, "by_path_suffix": {},
|
|
"by_name": {"ROM": ["a" * 40, "b" * 40]},
|
|
},
|
|
}
|
|
os.chdir(cls.root)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
os.chdir(cls.previous)
|
|
shutil.rmtree(cls.root, ignore_errors=True)
|
|
|
|
def test_a_candidate_outside_the_declared_range_is_refused(self):
|
|
entry = {"name": "ROM", "min_size": 32, "max_size": 128,
|
|
"validation": ["size"]}
|
|
path, _ = resolve_local_file(entry, self.db)
|
|
self.assertIn("Macintosh", path)
|
|
|
|
def test_a_present_file_of_the_wrong_size_is_still_returned(self):
|
|
# Reported as present and untested, never as absent: the user has a
|
|
# file under that name and needs to be told it is the wrong one.
|
|
entry = {"name": "ROM", "size": 999, "validation": ["size"]}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertIsNotNone(path)
|
|
self.assertEqual(status, "hash_mismatch")
|
|
|
|
def test_a_list_of_accepted_sizes_gates_the_name_step(self):
|
|
# SC-55/ROM2.BIN accepts two sizes. A PlayStation 2 ROM2.BIN of a
|
|
# third size answered to the name because the list was ignored.
|
|
entry = {"name": "ROM", "size": [8, 999], "validation": ["size"]}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "name_exact")
|
|
self.assertIn("IIGS", path)
|
|
entry = {"name": "ROM", "size": [999, 1000], "validation": ["size"]}
|
|
path, status = resolve_local_file(entry, self.db)
|
|
self.assertEqual(status, "hash_mismatch")
|
|
|
|
def test_a_size_without_validation_rejects_nothing(self):
|
|
# Documented behaviour: a size is informative until the emulator
|
|
# itself checks it.
|
|
entry = {"name": "ROM", "size": 999}
|
|
path, _ = resolve_local_file(entry, self.db)
|
|
self.assertIsNotNone(path)
|
|
|
|
|
|
class TestForeignTreesAndSharedTails(unittest.TestCase):
|
|
"""PCSX2x6's arcade GameIndex.yaml met four copies under a shortened
|
|
tail: three in other emulators' trees and PCSX2's console database. The
|
|
three were set aside first, the fourth then looked unique and passed for
|
|
path proof."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.root = Path(tempfile.mkdtemp(prefix="foreign-"))
|
|
cls.previous = os.getcwd()
|
|
stored = {
|
|
"1" * 40: "bios/Other/aethersx2/resources/GameIndex.yaml",
|
|
"2" * 40: "bios/Other/armsx2/resources/GameIndex.yaml",
|
|
"3" * 40: "bios/Other/emucorex/resources/GameIndex.yaml",
|
|
"4" * 40: "bios/Sony/PlayStation 2/resources/GameIndex.yaml",
|
|
"5" * 40: "bios/Other/emucorex/resources/RedumpDatabase.yaml",
|
|
}
|
|
# Same bytes as emucorex's copy: the database keeps one path for it.
|
|
duplicate = ("5" * 40, "bios/Other/x6/bin/resources/RedumpDatabase.yaml")
|
|
files, by_name, by_suffix = {}, {}, {}
|
|
for sha1, rel in [*stored.items(), duplicate]:
|
|
target = cls.root / rel
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_bytes(sha1[:4].encode())
|
|
name = rel.rsplit("/", 1)[-1]
|
|
if sha1 not in files:
|
|
files[sha1] = {"path": rel, "name": name, "md5": "", "size": 4,
|
|
"crc32": "", "sha1": sha1}
|
|
by_name.setdefault(name, []).append(sha1)
|
|
for suffix in _path_suffixes(rel):
|
|
if sha1 not in by_suffix.setdefault(suffix, []):
|
|
by_suffix[suffix].append(sha1)
|
|
cls.db = {
|
|
"files": files,
|
|
"indexes": {"by_md5": {}, "by_name": by_name, "by_crc32": {},
|
|
"by_path_suffix": by_suffix},
|
|
}
|
|
os.chdir(cls.root)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
os.chdir(cls.previous)
|
|
shutil.rmtree(cls.root, ignore_errors=True)
|
|
|
|
def test_a_shared_shortened_tail_proves_nothing(self):
|
|
_path, status = resolve_local_file(
|
|
{"name": "GameIndex.yaml", "source_profile": "x6"}, self.db,
|
|
dest_hint="bin/resources/GameIndex.yaml",
|
|
)
|
|
self.assertNotEqual(status, "path_exact")
|
|
|
|
def test_a_tail_indexed_from_a_duplicate_is_the_askers(self):
|
|
path, status = resolve_local_file(
|
|
{"name": "RedumpDatabase.yaml", "source_profile": "x6"}, self.db,
|
|
dest_hint="bin/resources/RedumpDatabase.yaml",
|
|
)
|
|
self.assertEqual((status, path), ("path_exact", "bios/Other/emucorex/resources/RedumpDatabase.yaml"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|