Files
libretro/tests/test_path_suffix.py
T
Abdessamad Derraz 5e84244cb9 fix: a game folder copy yields to a neutral one
hypseus-singe, flycast-dojo, lindbergh-loader and vvvvvv read gamecontrollerdb.txt without naming a hash. The name step only set aside copies in other emulators' trees, so a copy in a game's folder under Game Engines/ ranked with any neutral one, and index order handed them C-Dogs' 98-line database. A game folder's copy now ranks after a neutral copy. The community SDL_GameControllerDB is collected at commit ff333823 (git blob 25146be7) as that neutral copy.
2026-10-19 23:16:38 +02:00

309 lines
13 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"))
class TestSizeGateOnThePathStep(unittest.TestCase):
"""The path step took any file at the tail when no hash was declared,
while the name step already refused one outside the checked size."""
def test_a_file_outside_the_checked_size_is_not_path_proof(self):
with tempfile.TemporaryDirectory() as root:
previous = os.getcwd()
os.chdir(root)
self.addCleanup(os.chdir, previous)
rel = "bios/Other/x/cpc/cpc6128.rom"
Path(rel).parent.mkdir(parents=True)
Path(rel).write_bytes(b"x" * 32768)
sha1 = "e" * 40
db = {
"files": {sha1: {"path": rel, "name": "cpc6128.rom", "md5": "",
"size": 32768, "crc32": "", "sha1": sha1}},
"indexes": {"by_md5": {}, "by_name": {"cpc6128.rom": [sha1]},
"by_crc32": {}, "by_path_suffix": {"cpc/cpc6128.rom": [sha1]}},
}
entry = {"name": "cpc6128.rom", "size": 49152, "validation": ["size"]}
_path, status = resolve_local_file(entry, db, dest_hint="cpc/cpc6128.rom")
self.assertNotEqual(status, "path_exact")
class TestAGamesFolderGoesAfterANeutralCopy(unittest.TestCase):
"""Four profiles that name gamecontrollerdb.txt without a hash took
C-Dogs' 98-line copy from its game folder, by index order."""
def test_the_neutral_copy_wins_the_name_step(self):
with tempfile.TemporaryDirectory() as root:
previous = os.getcwd()
os.chdir(root)
self.addCleanup(os.chdir, previous)
paths = {
"a" * 40: "bios/Game Engines/C-Dogs SDL/data/gamecontrollerdb.txt",
"b" * 40: "bios/SDL/GameControllerDB/gamecontrollerdb.txt",
}
files = {}
for sha1, rel in paths.items():
Path(rel).parent.mkdir(parents=True)
Path(rel).write_bytes(sha1[:4].encode())
files[sha1] = {"path": rel, "name": "gamecontrollerdb.txt", "md5": "",
"size": 4, "crc32": "", "sha1": sha1}
db = {"files": files, "indexes": {
"by_md5": {}, "by_crc32": {}, "by_path_suffix": {},
"by_name": {"gamecontrollerdb.txt": list(paths)},
}}
path, status = resolve_local_file(
{"name": "gamecontrollerdb.txt", "source_profile": "hypseus-singe"}, db,
)
self.assertEqual((path, status), (paths["b" * 40], "name_exact"))
if __name__ == "__main__":
unittest.main()