mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-18 17:33:30 -05:00
Dolphin files the Australian country code under Region::PAL, whose IPL lives in GC/EUR. Tagged europe alone, --region australia dropped it and kept the American IPL, which Dolphin never reads for those discs.
767 lines
30 KiB
Python
767 lines
30 KiB
Python
"""Region vocabulary, hierarchy, and rank resolution."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
|
|
|
import common # noqa: E402
|
|
import region # noqa: E402
|
|
from packextras import emulator_region_drops # noqa: E402
|
|
|
|
|
|
class TestVocabulary(unittest.TestCase):
|
|
def test_world_is_a_region(self):
|
|
self.assertIn(region.WORLD, region.REGIONS)
|
|
|
|
def test_super_regions_and_members_are_all_regions(self):
|
|
for parent, members in region.REGION_TREE.items():
|
|
self.assertIn(parent, region.REGIONS)
|
|
for member in members:
|
|
self.assertIn(member, region.REGIONS)
|
|
|
|
def test_hierarchy_is_two_levels(self):
|
|
for members in region.REGION_TREE.values():
|
|
for member in members:
|
|
self.assertNotIn(member, region.REGION_TREE)
|
|
|
|
def test_no_member_has_two_parents(self):
|
|
seen: set[str] = set()
|
|
for members in region.REGION_TREE.values():
|
|
for member in members:
|
|
self.assertNotIn(member, seen)
|
|
seen.add(member)
|
|
|
|
|
|
class TestComparable(unittest.TestCase):
|
|
def test_equal(self):
|
|
self.assertTrue(region.comparable("japan", "japan"))
|
|
|
|
def test_parent_matches_child(self):
|
|
self.assertTrue(region.comparable("europe", "france"))
|
|
|
|
def test_child_matches_parent(self):
|
|
self.assertTrue(region.comparable("france", "europe"))
|
|
|
|
def test_siblings_do_not_match(self):
|
|
self.assertFalse(region.comparable("france", "germany"))
|
|
|
|
def test_disjoint_super_regions_do_not_match(self):
|
|
self.assertFalse(region.comparable("europe", "asia"))
|
|
|
|
def test_saturn_fused_slug_is_under_asia(self):
|
|
self.assertTrue(region.comparable("asia", "asia-ntsc"))
|
|
|
|
|
|
class TestNormalizeDeclared(unittest.TestCase):
|
|
def test_none_is_empty(self):
|
|
self.assertEqual(region.normalize_declared(None), set())
|
|
|
|
def test_list_passes_through(self):
|
|
self.assertEqual(region.normalize_declared(["japan"]), {"japan"})
|
|
|
|
def test_legacy_signal_string_maps_to_geography(self):
|
|
self.assertEqual(region.normalize_declared("NTSC-J"), {"japan"})
|
|
self.assertEqual(region.normalize_declared("NTSC-U"), {"north-america"})
|
|
self.assertEqual(region.normalize_declared("PAL"), {"europe"})
|
|
|
|
def test_legacy_auto_and_world_map_to_world(self):
|
|
self.assertEqual(region.normalize_declared("Auto"), {region.WORLD})
|
|
self.assertEqual(region.normalize_declared("World"), {region.WORLD})
|
|
|
|
def test_unknown_value_raises(self):
|
|
with self.assertRaises(ValueError):
|
|
region.normalize_declared("atlantis")
|
|
|
|
|
|
class TestParseRequested(unittest.TestCase):
|
|
def test_single_alias(self):
|
|
self.assertEqual(region.parse_requested("us"), ["north-america"])
|
|
|
|
def test_order_is_preserved(self):
|
|
self.assertEqual(
|
|
region.parse_requested("us,eu,jp"),
|
|
["north-america", "europe", "japan"],
|
|
)
|
|
|
|
def test_duplicates_collapse_keeping_first_position(self):
|
|
self.assertEqual(
|
|
region.parse_requested("us,usa,eu"), ["north-america", "europe"]
|
|
)
|
|
|
|
def test_whitespace_and_case_tolerated(self):
|
|
self.assertEqual(
|
|
region.parse_requested(" US , Eu "), ["north-america", "europe"]
|
|
)
|
|
|
|
def test_unknown_region_raises(self):
|
|
with self.assertRaises(ValueError):
|
|
region.parse_requested("us,atlantis")
|
|
|
|
def test_empty_raises(self):
|
|
with self.assertRaises(ValueError):
|
|
region.parse_requested(" ")
|
|
|
|
|
|
class TestRank(unittest.TestCase):
|
|
def test_exact_match_is_zero(self):
|
|
self.assertEqual(region.rank({"north-america"}, ["north-america"]), 0)
|
|
|
|
def test_second_choice_is_one(self):
|
|
self.assertEqual(region.rank({"europe"}, ["north-america", "europe"]), 1)
|
|
|
|
def test_no_match_is_lowest(self):
|
|
self.assertEqual(region.rank({"japan"}, ["north-america", "europe"]), 2)
|
|
|
|
def test_child_matches_requested_parent(self):
|
|
self.assertEqual(region.rank({"france"}, ["europe"]), 0)
|
|
|
|
def test_parent_matches_requested_child(self):
|
|
self.assertEqual(region.rank({"europe"}, ["france"]), 0)
|
|
|
|
def test_best_of_several_declared_regions_wins(self):
|
|
self.assertEqual(
|
|
region.rank({"japan", "north-america"}, ["north-america", "japan"]), 0
|
|
)
|
|
|
|
def test_empty_declared_is_lowest(self):
|
|
self.assertEqual(region.rank(set(), ["north-america"]), 1)
|
|
|
|
|
|
class TestRegionTag(unittest.TestCase):
|
|
def test_single(self):
|
|
self.assertEqual(region.region_tag(["north-america"]), "NorthAmerica")
|
|
|
|
def test_multiple_joined(self):
|
|
self.assertEqual(
|
|
region.region_tag(["north-america", "japan"]), "NorthAmerica_Japan"
|
|
)
|
|
|
|
|
|
class TestRegionIndex(unittest.TestCase):
|
|
def setUp(self):
|
|
self.profiles = {
|
|
"dolphin": {
|
|
"files": [
|
|
{
|
|
"name": "IPL.bin",
|
|
"path": "GC/USA/IPL.bin",
|
|
"region": ["north-america"],
|
|
},
|
|
{
|
|
"name": "IPL.bin",
|
|
"path": "GC/EUR/IPL.bin",
|
|
"region": ["europe"],
|
|
},
|
|
{
|
|
"name": "IPL.bin",
|
|
"path": "GC/JAP/IPL.bin",
|
|
"region": ["japan"],
|
|
},
|
|
{"name": "dsp_rom.bin"},
|
|
]
|
|
},
|
|
"beetle_psx": {
|
|
"files": [
|
|
{"name": "scph5501.bin", "region": "NTSC-U"},
|
|
{"name": "psxonpsp660.bin", "region": "Auto"},
|
|
]
|
|
},
|
|
"swanstation": {
|
|
"files": [{"name": "scph5501.bin", "region": "NTSC-U"}]
|
|
},
|
|
"launcher_profile": {
|
|
"type": "launcher",
|
|
"files": [{"name": "ignored.bin", "region": ["japan"]}],
|
|
},
|
|
}
|
|
self.index = region.build_region_index(self.profiles)
|
|
|
|
def test_path_keyed_entries_stay_separate(self):
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "GC/USA/IPL.bin", "IPL.bin"),
|
|
{"north-america"},
|
|
)
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "GC/JAP/IPL.bin", "IPL.bin"),
|
|
{"japan"},
|
|
)
|
|
|
|
def test_bare_name_lookup_unions_ambiguous_entries(self):
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "", "IPL.bin"),
|
|
{"north-america", "europe", "japan"},
|
|
)
|
|
|
|
def test_destination_suffix_bridges_platform_entry(self):
|
|
self.assertEqual(
|
|
region.lookup_regions(
|
|
self.index, "dolphin-emu/GC/EUR/IPL.bin", "IPL.bin"
|
|
),
|
|
{"europe"},
|
|
)
|
|
|
|
def test_untagged_file_has_no_regions(self):
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "dsp_rom.bin", "dsp_rom.bin"), set()
|
|
)
|
|
|
|
def test_unknown_file_has_no_regions(self):
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "nope.bin", "nope.bin"), set()
|
|
)
|
|
|
|
def test_legacy_string_values_are_normalised(self):
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "scph5501.bin", "scph5501.bin"),
|
|
{"north-america"},
|
|
)
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "psxonpsp660.bin", "psxonpsp660.bin"),
|
|
{region.WORLD},
|
|
)
|
|
|
|
def test_two_profiles_agreeing_merge(self):
|
|
entry = self.index["scph5501.bin"]
|
|
self.assertEqual(entry["regions"], {"north-america"})
|
|
self.assertEqual(entry["emulators"], ["beetle_psx", "swanstation"])
|
|
|
|
def test_launcher_profiles_are_skipped(self):
|
|
self.assertEqual(
|
|
region.lookup_regions(self.index, "ignored.bin", "ignored.bin"), set()
|
|
)
|
|
|
|
def test_unknown_value_names_the_profile_and_file(self):
|
|
with self.assertRaises(ValueError) as ctx:
|
|
region.build_region_index(
|
|
{"badcore": {"files": [{"name": "x.bin", "region": ["atlantis"]}]}}
|
|
)
|
|
self.assertIn("badcore", str(ctx.exception))
|
|
self.assertIn("x.bin", str(ctx.exception))
|
|
|
|
|
|
class TestResolveRegionDrops(unittest.TestCase):
|
|
def setUp(self):
|
|
self.index = region.build_region_index(
|
|
{
|
|
"psx": {
|
|
"files": [
|
|
{"name": "scph5501.bin", "region": ["north-america"]},
|
|
{"name": "scph5500.bin", "region": ["japan"]},
|
|
{"name": "scph5502.bin", "region": ["europe"]},
|
|
{"name": "psxonpsp660.bin", "region": ["world"]},
|
|
{"name": "gamepad.cfg"},
|
|
]
|
|
},
|
|
"fds": {"files": [{"name": "disksys.rom", "region": ["japan"]}]},
|
|
"o2em": {"files": [{"name": "c52.bin", "region": ["france"]}]},
|
|
}
|
|
)
|
|
|
|
def _drops(self, groups, requested):
|
|
return region.resolve_region_drops(groups, self.index, requested)
|
|
|
|
def test_a_world_file_the_code_ranks_lower_keeps_the_regional_ones(self):
|
|
"""DuckStation looks for its PS1 images (5) before the world PS2 ones
|
|
(100): asked for Brazil, it kept only the PS2 fallbacks."""
|
|
index = region.build_region_index({"duckstation": {"files": [
|
|
{"name": "scph5501.bin", "region": ["north-america"], "priority": 5},
|
|
{"name": "ps2_scph30001.bin", "region": ["north-america"], "priority": 150},
|
|
{"name": "ps2_scph10000.bin", "region": ["world"], "priority": 100},
|
|
]}})
|
|
groups = {"psx": [(n, n) for n in ("scph5501.bin", "ps2_scph30001.bin",
|
|
"ps2_scph10000.bin")]}
|
|
self.assertEqual(
|
|
region.resolve_region_drops(groups, index, ["brazil"]), {"ps2_scph30001.bin"}
|
|
)
|
|
|
|
def test_without_ranks_the_world_file_still_wins(self):
|
|
groups = {"psx": [("scph5501.bin", "scph5501.bin"),
|
|
("psxonpsp660.bin", "psxonpsp660.bin")]}
|
|
self.assertEqual(self._drops(groups, ["brazil"]), {"scph5501.bin"})
|
|
|
|
def test_best_rank_wins_and_siblings_drop(self):
|
|
groups = {
|
|
"psx": [
|
|
("scph5501.bin", "scph5501.bin"),
|
|
("scph5500.bin", "scph5500.bin"),
|
|
("scph5502.bin", "scph5502.bin"),
|
|
]
|
|
}
|
|
self.assertEqual(
|
|
self._drops(groups, ["north-america"]),
|
|
{"scph5500.bin", "scph5502.bin"},
|
|
)
|
|
|
|
def test_priority_order_selects_second_choice(self):
|
|
groups = {
|
|
"psx": [
|
|
("scph5500.bin", "scph5500.bin"),
|
|
("scph5502.bin", "scph5502.bin"),
|
|
]
|
|
}
|
|
self.assertEqual(
|
|
self._drops(groups, ["north-america", "europe", "japan"]),
|
|
{"scph5500.bin"},
|
|
)
|
|
|
|
def test_group_with_no_match_is_kept_whole(self):
|
|
groups = {"fds": [("disksys.rom", "disksys.rom")]}
|
|
self.assertEqual(self._drops(groups, ["north-america"]), set())
|
|
|
|
def test_world_never_competes(self):
|
|
groups = {
|
|
"psx": [
|
|
("scph5501.bin", "scph5501.bin"),
|
|
("scph5500.bin", "scph5500.bin"),
|
|
("psxonpsp660.bin", "psxonpsp660.bin"),
|
|
]
|
|
}
|
|
self.assertEqual(self._drops(groups, ["north-america"]), {"scph5500.bin"})
|
|
|
|
def test_untagged_files_never_drop(self):
|
|
groups = {
|
|
"psx": [
|
|
("scph5501.bin", "scph5501.bin"),
|
|
("gamepad.cfg", "gamepad.cfg"),
|
|
("scph5500.bin", "scph5500.bin"),
|
|
]
|
|
}
|
|
self.assertEqual(self._drops(groups, ["north-america"]), {"scph5500.bin"})
|
|
|
|
def test_child_region_satisfies_requested_parent(self):
|
|
groups = {"o2em": [("c52.bin", "c52.bin")]}
|
|
self.assertEqual(self._drops(groups, ["europe"]), set())
|
|
|
|
def test_destination_kept_by_any_group_is_not_dropped(self):
|
|
groups = {
|
|
"psx": [
|
|
("scph5501.bin", "scph5501.bin"),
|
|
("scph5500.bin", "scph5500.bin"),
|
|
],
|
|
"psx-import": [("scph5500.bin", "scph5500.bin")],
|
|
}
|
|
self.assertEqual(self._drops(groups, ["north-america"]), set())
|
|
|
|
def test_no_requested_regions_drops_nothing(self):
|
|
groups = {"psx": [("scph5500.bin", "scph5500.bin")]}
|
|
self.assertEqual(self._drops(groups, []), set())
|
|
|
|
|
|
class TestFallbackGroups(unittest.TestCase):
|
|
def setUp(self):
|
|
self.index = region.build_region_index(
|
|
{
|
|
"psx": {
|
|
"files": [
|
|
{"name": "scph5501.bin", "region": ["north-america"]}
|
|
]
|
|
},
|
|
"fds": {"files": [{"name": "disksys.rom", "region": ["japan"]}]},
|
|
}
|
|
)
|
|
|
|
def test_only_unmatched_groups_are_reported(self):
|
|
groups = {
|
|
"psx": [("scph5501.bin", "scph5501.bin")],
|
|
"fds": [("disksys.rom", "disksys.rom")],
|
|
}
|
|
self.assertEqual(
|
|
region.fallback_groups(groups, self.index, ["north-america"]), ["fds"]
|
|
)
|
|
|
|
def test_an_untagged_bios_answers_every_region(self):
|
|
"""VICE's generic kernal is untagged beside its Japanese one: the C64
|
|
was reported as having no BIOS for North America."""
|
|
groups = {"fds": [("disksys.rom", "disksys.rom"), ("generic.rom", "generic.rom")]}
|
|
self.assertEqual(region.fallback_groups(groups, self.index, ["north-america"]), [])
|
|
|
|
def test_a_file_no_system_claims_is_no_fallback(self):
|
|
"""Dolphin's GC/EUR/IPL.bin, alone in its group, named the GameCube
|
|
in the pack README as lacking a North American BIOS it carries."""
|
|
groups = {"Dolphin:file:GC/EUR/IPL.bin": [("GC/EUR/IPL.bin", "IPL.bin")],
|
|
"fds": [("disksys.rom", "disksys.rom")]}
|
|
self.assertEqual(
|
|
region.fallback_groups(groups, self.index, ["north-america"]), ["fds"]
|
|
)
|
|
|
|
def test_the_readme_names_a_slot_as_a_reader_does(self):
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
|
from packreadme import _group_label
|
|
|
|
self.assertEqual(_group_label("PicoDrive:variant:megacd"), "megacd (PicoDrive)")
|
|
self.assertEqual(_group_label("sega-mega-cd"), "sega-mega-cd")
|
|
|
|
def test_no_requested_regions_reports_nothing(self):
|
|
groups = {"fds": [("disksys.rom", "disksys.rom")]}
|
|
self.assertEqual(region.fallback_groups(groups, self.index, []), [])
|
|
|
|
|
|
class AustraliaReadsThePalIpl(unittest.TestCase):
|
|
"""Dolphin files the Australian country code U under Region::PAL
|
|
(Source/Core/DiscIO/Enums.cpp), whose IPL is GC/EUR. Tagged [europe]
|
|
alone, `--region australia` dropped it and kept the American one."""
|
|
|
|
def test_the_pal_ipl_answers_oceania(self):
|
|
profiles = common.load_emulator_profiles(
|
|
os.path.join(os.path.dirname(__file__), "..", "emulators")
|
|
)
|
|
for name in ("dolphin", "primehack", "ishiiruka", "triforce"):
|
|
with self.subTest(profile=name):
|
|
pal = [f for f in profiles[name]["files"]
|
|
if "/EUR/" in str(f.get("path", ""))]
|
|
self.assertTrue(pal)
|
|
self.assertTrue(all("oceania" in f.get("region", []) for f in pal))
|
|
|
|
|
|
class TestProfileConformance(unittest.TestCase):
|
|
"""Every region: value in the repo is a list of canonical slugs."""
|
|
|
|
def setUp(self):
|
|
import glob
|
|
|
|
import yaml
|
|
|
|
self.repo_profiles = sorted(
|
|
glob.glob(
|
|
os.path.join(os.path.dirname(__file__), "..", "emulators", "*.yml")
|
|
)
|
|
)
|
|
if not self.repo_profiles:
|
|
self.skipTest("emulators/ not present")
|
|
self.yaml = yaml
|
|
|
|
def test_all_region_values_are_canonical_lists(self):
|
|
offenders: list[str] = []
|
|
for path in self.repo_profiles:
|
|
with open(path, encoding="utf-8") as fh:
|
|
profile = self.yaml.safe_load(fh) or {}
|
|
for f in profile.get("files") or []:
|
|
if not isinstance(f, dict) or "region" not in f:
|
|
continue
|
|
raw = f["region"]
|
|
if not isinstance(raw, list):
|
|
offenders.append(
|
|
f"{os.path.basename(path)}: {f.get('name')} not a list"
|
|
)
|
|
continue
|
|
for value in raw:
|
|
if value not in region.REGIONS:
|
|
offenders.append(
|
|
f"{os.path.basename(path)}: {f.get('name')} -> {value!r}"
|
|
)
|
|
self.assertEqual(offenders, [], "\n".join(offenders))
|
|
|
|
def test_index_builds_over_every_repo_profile(self):
|
|
profiles = {}
|
|
for path in self.repo_profiles:
|
|
with open(path, encoding="utf-8") as fh:
|
|
profiles[os.path.basename(path)[:-4]] = self.yaml.safe_load(fh) or {}
|
|
index = region.build_region_index(profiles)
|
|
self.assertGreater(len(index), 0)
|
|
|
|
|
|
class TestSchemaMatchesModule(unittest.TestCase):
|
|
def test_schema_enum_equals_module_regions(self):
|
|
import json
|
|
|
|
path = os.path.join(
|
|
os.path.dirname(__file__), "..", "schemas", "emulator.schema.json"
|
|
)
|
|
if not os.path.exists(path):
|
|
self.skipTest("schema not present")
|
|
with open(path, encoding="utf-8") as fh:
|
|
schema = json.load(fh)
|
|
node = schema["properties"]["files"]["items"]["properties"]["region"]
|
|
self.assertEqual(set(node["items"]["enum"]), set(region.REGIONS))
|
|
|
|
|
|
class TestVerifyModesHonourRegion(unittest.TestCase):
|
|
"""verify.py must apply --region in every mode, or refuse it.
|
|
|
|
It silently ignored the flag in --emulator and --system mode: the report
|
|
was identical with and without, so a filtered pack could not be checked.
|
|
"""
|
|
|
|
def _run(self, *args: str) -> str:
|
|
"""The report, from a run that succeeded.
|
|
|
|
A run that crashed produced an empty stdout, different from the
|
|
plain report, and the test read the difference as narrowing.
|
|
"""
|
|
import subprocess
|
|
|
|
repo = os.path.join(os.path.dirname(__file__), "..")
|
|
completed = subprocess.run(
|
|
[sys.executable, "scripts/verify.py", *args],
|
|
capture_output=True, text=True, cwd=repo, timeout=900, check=False,
|
|
)
|
|
self.assertEqual(completed.returncode, 0, completed.stderr[-2000:])
|
|
self.assertTrue(completed.stdout.strip(), "an empty report is not a report")
|
|
return completed.stdout
|
|
|
|
def test_emulator_mode_narrows(self):
|
|
plain = self._run("--emulator", "duckstation")
|
|
if "duckstation" not in plain:
|
|
self.skipTest("duckstation profile not present")
|
|
filtered = self._run("--emulator", "duckstation", "--region", "us")
|
|
self.assertNotEqual(plain, filtered)
|
|
|
|
def test_system_mode_narrows(self):
|
|
plain = self._run("--system", "sony-playstation")
|
|
filtered = self._run("--system", "sony-playstation", "--region", "us")
|
|
self.assertNotEqual(plain, filtered)
|
|
|
|
|
|
class TestReportAndBuilderNarrowTogether(unittest.TestCase):
|
|
"""A region run must withdraw the same files from report and pack.
|
|
|
|
The two sides grouped their candidates separately: the builder grouped the
|
|
platform files and the core extras, the report only the platform files.
|
|
So the report kept every core extra a region run withdraws from the pack
|
|
-on recalbox --region us, 59 files it described as covered.
|
|
"""
|
|
|
|
def _fixture(self):
|
|
import hashlib
|
|
import tempfile
|
|
|
|
tmp = tempfile.TemporaryDirectory()
|
|
root = Path(tmp.name)
|
|
(root / "emulators").mkdir()
|
|
db = {"files": {}, "indexes": {
|
|
"by_name": {}, "by_md5": {}, "by_sha256": {},
|
|
"by_crc32": {}, "by_path_suffix": {},
|
|
}}
|
|
# Two regional alternatives of one system, both held locally.
|
|
for fname, tag in (("us.bin", b"US BYTES"), ("jp.bin", b"JP BYTES")):
|
|
blob = root / fname
|
|
blob.write_bytes(tag)
|
|
sha1 = hashlib.sha1(tag).hexdigest()
|
|
db["files"][sha1] = {
|
|
"path": str(blob), "name": fname, "size": len(tag),
|
|
"sha1": sha1, "md5": hashlib.md5(tag).hexdigest(),
|
|
"sha256": hashlib.sha256(tag).hexdigest(), "crc32": "00000000",
|
|
}
|
|
db["indexes"]["by_name"][fname] = [sha1]
|
|
db["indexes"]["by_md5"][db["files"][sha1]["md5"]] = sha1
|
|
(root / "emulators" / "demo.yml").write_text(
|
|
"emulator: demo\n"
|
|
"type: libretro\n"
|
|
"display_name: Demo\n"
|
|
"systems: [demo-system]\n"
|
|
"cores: [demo]\n"
|
|
"files:\n"
|
|
" - name: us.bin\n"
|
|
" system: demo-system\n"
|
|
" region: [north-america]\n"
|
|
" required: true\n"
|
|
" - name: jp.bin\n"
|
|
" system: demo-system\n"
|
|
" region: [japan]\n"
|
|
" required: true\n"
|
|
)
|
|
config = {
|
|
"platform": "Demo",
|
|
"verification_mode": "existence",
|
|
"cores": ["demo"],
|
|
"base_destination": "",
|
|
"systems": {},
|
|
}
|
|
return tmp, root, db, config
|
|
|
|
def test_the_report_withdraws_what_the_builder_withdraws(self):
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
|
import common
|
|
import generate_pack as builder
|
|
import verify as reporter
|
|
|
|
tmp, root, db, config = self._fixture()
|
|
try:
|
|
common._emulator_profiles_cache.clear()
|
|
profiles = common.load_emulator_profiles(str(root / "emulators"))
|
|
groups, extra_dests = builder.platform_region_groups(
|
|
config, config["systems"], str(root / "emulators"),
|
|
db, "", profiles,
|
|
)
|
|
drops = region.resolve_region_drops(
|
|
groups, region.build_region_index(profiles), ["north-america"]
|
|
)
|
|
self.assertEqual(
|
|
{d for d in drops}, {"jp.bin"},
|
|
"the builder must drop the Japanese alternative, not the US one",
|
|
)
|
|
|
|
plain = reporter.verify_platform(
|
|
config, db, str(root / "emulators"), profiles,
|
|
supplemental_names=set(),
|
|
)
|
|
narrowed = reporter.verify_platform(
|
|
config, db, str(root / "emulators"), profiles,
|
|
supplemental_names=set(), regions=["north-america"],
|
|
)
|
|
before = {u["name"] for u in plain["undeclared_files"]}
|
|
after = {u["name"] for u in narrowed["undeclared_files"]}
|
|
self.assertEqual(before, {"us.bin", "jp.bin"})
|
|
self.assertEqual(
|
|
before - after,
|
|
{d for d in drops},
|
|
"the report must withdraw exactly the builder's drop set",
|
|
)
|
|
finally:
|
|
common._emulator_profiles_cache.clear()
|
|
tmp.cleanup()
|
|
|
|
def test_one_grouping_pass_serves_both_sides(self):
|
|
"""A second hand-rolled grouping is how the two drifted apart.
|
|
|
|
Every module is read, not only the two that once held a copy: the
|
|
builder was split into several modules and a grouping can move with
|
|
it. packextras.py holds the two shared passes, platform and emulator.
|
|
"""
|
|
scripts = Path(__file__).resolve().parent.parent / "scripts"
|
|
for path in sorted(scripts.rglob("*.py")):
|
|
if path.name == "packextras.py":
|
|
continue
|
|
with self.subTest(module=path.name):
|
|
self.assertNotIn(
|
|
"region_groups.setdefault(", path.read_text(encoding="utf-8"),
|
|
"region grouping belongs to packextras: platform_region_groups "
|
|
"or emulator_region_drops",
|
|
)
|
|
|
|
def test_the_verifier_reads_the_grouping_where_it_lives(self):
|
|
"""Imported through the builder's CLI module, the verdict of a
|
|
--region run depended on that module compiling, and seven scripts
|
|
entered the site workflow's trigger paths for one function."""
|
|
source = (Path(__file__).resolve().parent.parent / "scripts" / "verify.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
self.assertNotIn("from generate_pack import platform_region_groups", source)
|
|
self.assertIn("from packextras import platform_region_groups", source)
|
|
|
|
def test_emulator_report_keeps_what_the_emulator_pack_keeps(self):
|
|
"""O2EM: --region us left the Videopac BIOSes in the pack, not in the report."""
|
|
repo = Path(__file__).resolve().parent.parent
|
|
if not (repo / "emulators" / "o2em.yml").is_file():
|
|
self.skipTest("no o2em profile")
|
|
from verify import verify_emulator
|
|
from common import load_database
|
|
|
|
db = load_database(str(repo / "database.json"))
|
|
full = verify_emulator(["o2em"], str(repo / "emulators"), db)
|
|
narrowed = verify_emulator(["o2em"], str(repo / "emulators"), db, regions=["north-america"])
|
|
names = {d["name"] for d in narrowed["details"]}
|
|
for kept in ("c52.bin", "g7400.bin", "jopac.bin"):
|
|
self.assertIn(kept, names, "a system with no candidate in the region falls back")
|
|
self.assertEqual(full["total_files"], narrowed["total_files"])
|
|
|
|
def test_every_grouping_call_carries_the_target(self):
|
|
"""Integrity grouped every core's extras while the builder grouped the target's.
|
|
|
|
Under --target with --region, a fallback the pack kept was withdrawn
|
|
from the check: 14 Batocera rk3568 files were never verified.
|
|
"""
|
|
import ast
|
|
|
|
scripts = Path(__file__).resolve().parent.parent / "scripts"
|
|
for path in sorted(scripts.rglob("*.py")):
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for func in ast.walk(tree):
|
|
if not isinstance(func, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
continue
|
|
params = {a.arg for a in func.args.args + func.args.kwonlyargs}
|
|
if "target_cores" not in params:
|
|
continue
|
|
for call in ast.walk(func):
|
|
if (
|
|
isinstance(call, ast.Call)
|
|
and getattr(call.func, "id", None) == "platform_region_groups"
|
|
):
|
|
with self.subTest(module=path.name, function=func.name):
|
|
self.assertIn(
|
|
"target_cores", {k.arg for k in call.keywords}
|
|
)
|
|
|
|
def test_pal_console_region_reaches_oceania(self):
|
|
"""PSX cores pick these BIOSes for the PAL console region, not for Europe alone.
|
|
|
|
DuckStation's ConsoleRegion::PAL, Beetle PSX, SwanStation, PCSX ReARMed's
|
|
EU slot and Rustation all serve Australian and New Zealand discs from the
|
|
same images; annotated [europe] only, --region australia dropped them.
|
|
"""
|
|
import yaml
|
|
|
|
repo = Path(__file__).resolve().parent.parent
|
|
for emu in ("duckstation", "beetle_psx", "swanstation", "pcsx_rearmed", "rustation"):
|
|
profile = yaml.safe_load((repo / "emulators" / f"{emu}.yml").read_text())
|
|
for entry in profile.get("files", []):
|
|
regions = entry.get("region") or []
|
|
if "europe" in regions:
|
|
with self.subTest(emulator=emu, name=entry.get("name")):
|
|
self.assertIn("oceania", regions)
|
|
|
|
|
|
|
|
class RegionalRequestsKeepWhatTheCodeLoads(unittest.TestCase):
|
|
"""A --region request never drops the file the core opens for it.
|
|
|
|
mednafen_pce_fast tagged option-selected cards with territories, beetle_psx
|
|
tagged its override BIOS [world], mednafen tagged the ST-V NA BIOS for
|
|
America alone and the PAL PSX BIOS for Europe alone.
|
|
"""
|
|
|
|
CASES = (
|
|
("mednafen_pce_fast", ["north-america"], "syscard3.pce"),
|
|
("beetle_psx", ["brazil"], "scph5501.bin"),
|
|
("mednafen", ["south-korea", "europe"], "epr-17952a.ic8"),
|
|
("mednafen", ["australia", "north-america"], "scph5502.bin"),
|
|
("bluemsx", ["south-korea"], "Machines/Shared Roms/KANJI.rom"),
|
|
("bluemsx", ["south-korea"], "Machines/Shared Roms/MSXKANJI.rom"),
|
|
("bluemsx", ["japan"], "Machines/Shared Roms/HANGUL.rom"),
|
|
("ace-dl", ["france"], "private/firmware/OS6128_UK.rom"),
|
|
("ace-dl", ["spain"], "private/firmware/OS664_UK.rom"),
|
|
("ace-dl", ["spain"], "private/firmware/OS464_FR.rom"),
|
|
("ace-dl", ["denmark"], "private/firmware/XMEM_FW_3_15_UK.rom"),
|
|
("ace-dl", ["spain"], "private/hacker/hacker481fr.rom"),
|
|
("ace-dl", ["france"], "private/firmware/Amstrad_s4.cpr"),
|
|
("duckstation", ["brazil"], "scph5501.bin"),
|
|
("duckstation", ["south-korea"], "scph5500.bin"),
|
|
)
|
|
|
|
def test_each_request_keeps_its_file(self):
|
|
profiles = common.load_emulator_profiles(
|
|
os.path.join(os.path.dirname(__file__), "..", "emulators")
|
|
)
|
|
for name, regions, kept in self.CASES:
|
|
with self.subTest(profile=name, regions=regions):
|
|
drops = emulator_region_drops([(name, profiles[name])], False, regions)
|
|
self.assertNotIn(kept, drops)
|
|
|
|
|
|
|
|
class FontRomsCarryNoTerritory(unittest.TestCase):
|
|
"""A CJK font ROM is a language, not a territory. blueMSX tagged KANJI.rom
|
|
and MSXKANJI.rom [japan], which a --region south-korea request withdrew
|
|
from the generic MSX2+ machine whose config.ini loads both."""
|
|
|
|
def test_no_kanji_or_hangul_font_declares_a_region(self):
|
|
profiles = common.load_emulator_profiles(
|
|
os.path.join(os.path.dirname(__file__), "..", "emulators")
|
|
)
|
|
tagged = [
|
|
f"{name}:{entry['name']}"
|
|
for name, profile in profiles.items()
|
|
for entry in profile.get("files", [])
|
|
if any(word in entry.get("name", "").lower() for word in ("kanji", "hangul"))
|
|
and entry.get("region")
|
|
]
|
|
self.assertEqual(tagged, [])
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|