Files
libretro/tests/test_path_suffix.py

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()