Files
libretro/tests/test_game_data_homonyms.py
T

116 lines
4.5 KiB
Python

"""Game data laid out in a directory is not served by another game's file.
Engines read generic names from their own tree: `pak0.pak` is Quake II's
under `baseq2/` and Half-Life's under `valve/`. The name step of the resolver
ignores directories, so once one of them is collected every other entry of
that name resolves to it and a pack ships the wrong game's bytes under the
right path. The collection stores such a file under the tail the engine
reads (`.../baseq2/pak0.pak`), which lets its own entry resolve by path; an
entry that only reaches the same file by its bare name is a homonym, and
carries `unsourceable:` until its own bytes are held.
"""
from __future__ import annotations
import os
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import ( # noqa: E402
build_zip_contents_index,
load_data_dir_registry,
load_database,
load_emulator_profiles,
resolve_local_file,
)
def homonyms(claims: list[tuple[str, str, str, str]]) -> list[tuple[str, str, str]]:
"""(profile, destination, file) for name-only claims on a file that
another entry reaches by its path.
claims: (profile, destination, status, local file).
"""
by_path = {local for _, _, status, local in claims if status == "path_exact"}
found = []
for profile, dest, status, local in claims:
if status != "name_exact" or local not in by_path:
continue
if local.casefold().endswith("/" + dest.casefold()):
continue
if local.rsplit("/", 1)[-1].casefold() != dest.rsplit("/", 1)[-1].casefold():
# Reached through an alias: the profile says the two are one file.
continue
found.append((profile, dest, local))
return sorted(found)
class HomonymRule(unittest.TestCase):
def test_a_name_claim_on_a_file_another_entry_reaches_by_path_is_named(self):
claims = [
("quake2", "baseq2/pak0.pak", "path_exact", "bios/Q2/baseq2/pak0.pak"),
("halflife", "valve/pak0.pak", "name_exact", "bios/Q2/baseq2/pak0.pak"),
]
self.assertEqual(
homonyms(claims), [("halflife", "valve/pak0.pak", "bios/Q2/baseq2/pak0.pak")]
)
def test_a_file_only_ever_found_by_name_is_not_judged(self):
claims = [
("mame2003", "mame2003/cheat.dat", "name_exact", "bios/Arcade/cheat.dat"),
("mame2010", "mame2010/cheat.dat", "name_exact", "bios/Arcade/cheat.dat"),
]
self.assertEqual(homonyms(claims), [])
def test_a_declared_alias_is_the_profile_speaking(self):
claims = [
("m", "crosshair/cross7.png", "path_exact", "bios/m/crosshair/cross7.png"),
("m", "crosshair/cross8.png", "name_exact", "bios/m/crosshair/cross7.png"),
]
self.assertEqual(homonyms(claims), [])
def test_two_entries_sharing_a_path_agree(self):
claims = [
("a", "etmain/pak1.pk3", "path_exact", "bios/ET/etmain/pak1.pk3"),
("b", "etmain/pak1.pk3", "path_exact", "bios/ET/etmain/pak1.pk3"),
]
self.assertEqual(homonyms(claims), [])
class CollectionCarriesNoGameDataHomonym(unittest.TestCase):
def test_the_profiles_resolve_no_game_data_to_another_game(self):
database = REPO_ROOT / "database.json"
if not database.is_file():
self.skipTest("no database.json")
previous = os.getcwd()
os.chdir(REPO_ROOT)
try:
db = load_database(str(database))
profiles = load_emulator_profiles("emulators", skip_aliases=False)
zip_index = build_zip_contents_index(db)
registry = load_data_dir_registry("platforms")
claims = []
for name, profile in sorted(profiles.items()):
for entry in profile.get("files") or []:
if entry.get("category") != "game_data" or entry.get("unsourceable"):
continue
dest = str(entry.get("path") or entry.get("name") or "")
if "/" not in dest.strip("/"):
continue
local, status = resolve_local_file(
entry, db, zip_index, dest_hint=dest, data_dir_registry=registry
)
if local:
claims.append((name, dest, status, local))
finally:
os.chdir(previous)
self.assertEqual(homonyms(claims), [])
if __name__ == "__main__":
unittest.main()