mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 13:33:24 -05:00
198 lines
7.6 KiB
Python
198 lines
7.6 KiB
Python
"""Which tails of a stored path can answer a declared destination."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
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; 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; 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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|