mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-05 09:33:52 -06:00
fix: let a deep path answer for the file it names
This commit is contained in:
1 parent
e0c60f2f5a
commit
3c16f6f640
5 files changed
+102451
-543
No files matched your search
@@ -0,0 +1,186 @@
|
||||
"""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_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()
|
||||
Reference in new issue
Block a user