mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-20 10:23:16 -05:00
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.
309 lines
13 KiB
Python
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()
|