mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-30 23:57:20 -05:00
fix: replay the builder's held and one-file groups
install.region_drops now skips one-file groups (:file:) and lets no omitted match displace an installable file, as region.resolve_region_drops does on the builder side. The property test draws both cases and compares against the builder.
This commit is contained in:
1 parent
82acd55ebc
commit
c638c12ca8
4 files changed
+95
-20
No files matched your search
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||||
$defaultInstallSha256 = "376edf794195d74f21e598854b5f3e90fb1955513835264bc9326ad4d2acf9c9"
|
$defaultInstallSha256 = "973d64627af05b239555ab5a83a8902747b7a742bc1176f1b994cf7ac74774f9"
|
||||||
$maximumInstallerBytes = 2MB
|
$maximumInstallerBytes = 2MB
|
||||||
$installerExit = 0
|
$installerExit = 0
|
||||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||||
|
|||||||
+33
-10
@@ -142,6 +142,8 @@ _REGION_PARENT = {
|
|||||||
member: parent for parent, members in REGION_TREE.items() for member in members
|
member: parent for parent, members in REGION_TREE.items() for member in members
|
||||||
}
|
}
|
||||||
REGIONS = frozenset({WORLD_REGION} | set(REGION_TREE) | set(_REGION_PARENT))
|
REGIONS = frozenset({WORLD_REGION} | set(REGION_TREE) | set(_REGION_PARENT))
|
||||||
|
# Marks a region group named after one file that no system names.
|
||||||
|
FILE_GROUP = ":file:"
|
||||||
MAX_SELECTION_TAGS = 1000
|
MAX_SELECTION_TAGS = 1000
|
||||||
SELECTION_FIELDS = ("systems", "regions", "region_groups", "region_system_groups")
|
SELECTION_FIELDS = ("systems", "regions", "region_groups", "region_system_groups")
|
||||||
# The files cores load for systems the platform does not list (game engines,
|
# The files cores load for systems the platform does not list (game engines,
|
||||||
@@ -2220,7 +2222,10 @@ def _region_rank(file_regions: set[str], requested: list[str]) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def region_drops(
|
def region_drops(
|
||||||
entries: list[dict], requested: list[str], systems: list[str] | None = None
|
entries: list[dict],
|
||||||
|
requested: list[str],
|
||||||
|
systems: list[str] | None = None,
|
||||||
|
held: set[str] | None = None,
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
"""Destinations a region priority list withdraws.
|
"""Destinations a region priority list withdraws.
|
||||||
|
|
||||||
@@ -2228,10 +2233,16 @@ def region_drops(
|
|||||||
groups the manifest records. Per group, the best-ranked regional file
|
groups the manifest records. Per group, the best-ranked regional file
|
||||||
stays. A world file beats unmatched regional ones the code does not search
|
stays. A world file beats unmatched regional ones the code does not search
|
||||||
before it (`priority`, lower first). When nothing matches and no world
|
before it (`priority`, lower first). When nothing matches and no world
|
||||||
file exists, every regional file stays, so a group is never emptied. A file with no region never competes and always stays.
|
file exists, every regional file stays, so a group is never emptied. A
|
||||||
|
file with no region never competes and always stays.
|
||||||
|
|
||||||
A core extra competes whatever systems are chosen, while a platform's own
|
A core extra competes whatever systems are chosen, while a platform's own
|
||||||
declaration competes only when its system is kept, as in the pack.
|
declaration competes only when its system is kept, as in the pack.
|
||||||
|
|
||||||
|
``held`` names the destinations the manifest can install. A match it
|
||||||
|
lacks never displaces a held file: the American BIOS stays under
|
||||||
|
--region japan when the Japanese one is missing. A group named after a
|
||||||
|
single file (FILE_GROUP) competes with nothing and keeps nothing.
|
||||||
"""
|
"""
|
||||||
if not requested:
|
if not requested:
|
||||||
return set()
|
return set()
|
||||||
@@ -2253,7 +2264,9 @@ def region_drops(
|
|||||||
groups.setdefault(group, []).append((set(regions), entry["dest"], priority))
|
groups.setdefault(group, []).append((set(regions), entry["dest"], priority))
|
||||||
keep: set[str] = set()
|
keep: set[str] = set()
|
||||||
drop: set[str] = set()
|
drop: set[str] = set()
|
||||||
for members in groups.values():
|
for group_id, members in groups.items():
|
||||||
|
if FILE_GROUP in group_id:
|
||||||
|
continue
|
||||||
world = {dest for regions, dest, _p in members if WORLD_REGION in regions}
|
world = {dest for regions, dest, _p in members if WORLD_REGION in regions}
|
||||||
ranks = {dest: priority for _regions, dest, priority in members}
|
ranks = {dest: priority for _regions, dest, priority in members}
|
||||||
regional = [
|
regional = [
|
||||||
@@ -2265,12 +2278,19 @@ def region_drops(
|
|||||||
if not regional:
|
if not regional:
|
||||||
continue
|
continue
|
||||||
matched = [(rank, dest) for rank, dest in regional if rank < len(requested)]
|
matched = [(rank, dest) for rank, dest in regional if rank < len(requested)]
|
||||||
if matched:
|
winners, rivals = matched, world
|
||||||
best = min(rank for rank, _dest in matched)
|
if held is not None:
|
||||||
keep |= {dest for rank, dest in matched if rank == best}
|
winners = [(rank, dest) for rank, dest in matched if dest in held]
|
||||||
drop |= {dest for _rank, dest in regional}
|
rivals = world & held
|
||||||
elif world:
|
if winners:
|
||||||
world_ranks = [ranks[dest] for dest in world]
|
best = min(rank for rank, _dest in winners)
|
||||||
|
keep |= {
|
||||||
|
dest for rank, dest in matched
|
||||||
|
if rank == best or (rank < best and dest not in (held or ()))
|
||||||
|
}
|
||||||
|
drop |= {dest for _rank, dest in regional if dest not in keep}
|
||||||
|
elif rivals:
|
||||||
|
world_ranks = [ranks[dest] for dest in rivals]
|
||||||
best_world = min(world_ranks) if None not in world_ranks else None
|
best_world = min(world_ranks) if None not in world_ranks else None
|
||||||
for _rank, dest in regional:
|
for _rank, dest in regional:
|
||||||
ours = ranks[dest]
|
ours = ranks[dest]
|
||||||
@@ -2310,7 +2330,10 @@ def narrow(
|
|||||||
files = _filter_by_target(files, cores)
|
files = _filter_by_target(files, cores)
|
||||||
omitted = _filter_by_target(omitted, cores)
|
omitted = _filter_by_target(omitted, cores)
|
||||||
if regions:
|
if regions:
|
||||||
dropped = region_drops(files + omitted, regions, systems)
|
# The builder's held set: what the collection serves. An omission
|
||||||
|
# answers the request but takes no installable file's place.
|
||||||
|
held = {f["dest"] for f in files}
|
||||||
|
dropped = region_drops(files + omitted, regions, systems, held)
|
||||||
files = [f for f in files if f["dest"] not in dropped]
|
files = [f for f in files if f["dest"] not in dropped]
|
||||||
omitted = [o for o in omitted if o["dest"] not in dropped]
|
omitted = [o for o in omitted if o["dest"] not in dropped]
|
||||||
if systems:
|
if systems:
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
|||||||
TEMP_INSTALLER=""
|
TEMP_INSTALLER=""
|
||||||
TEMP_DIRECTORY=""
|
TEMP_DIRECTORY=""
|
||||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||||
DEFAULT_INSTALL_SHA256="376edf794195d74f21e598854b5f3e90fb1955513835264bc9326ad4d2acf9c9"
|
DEFAULT_INSTALL_SHA256="973d64627af05b239555ab5a83a8902747b7a742bc1176f1b994cf7ac74774f9"
|
||||||
MAX_INSTALLER_BYTES=2097152
|
MAX_INSTALLER_BYTES=2097152
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class OneRegionVocabulary(unittest.TestCase):
|
|||||||
self.assertEqual(install.REGION_ALIASES, region.ALIASES)
|
self.assertEqual(install.REGION_ALIASES, region.ALIASES)
|
||||||
self.assertEqual(install.WORLD_REGION, region.WORLD)
|
self.assertEqual(install.WORLD_REGION, region.WORLD)
|
||||||
self.assertEqual(install.REGIONS, region.REGIONS)
|
self.assertEqual(install.REGIONS, region.REGIONS)
|
||||||
|
self.assertEqual(install.FILE_GROUP, region.FILE_GROUP)
|
||||||
|
|
||||||
|
|
||||||
class TheRegionRuleIsTheBuilders(unittest.TestCase):
|
class TheRegionRuleIsTheBuilders(unittest.TestCase):
|
||||||
@@ -72,26 +73,68 @@ class TheRegionRuleIsTheBuilders(unittest.TestCase):
|
|||||||
index[dest] = {"regions": regions,
|
index[dest] = {"regions": regions,
|
||||||
"has_untagged": not regions, "emulators": [],
|
"has_untagged": not regions, "emulators": [],
|
||||||
"priorities": {priority}}
|
"priorities": {priority}}
|
||||||
groups.setdefault(f"g{g}", []).append((dest, dest))
|
# A lone core file no system names gets a group of its own.
|
||||||
if f"g{g}" not in entries[dest]["region_groups"]:
|
group = f"g{g}" if rng.random() < 0.85 else f"e{region.FILE_GROUP}{dest}"
|
||||||
entries[dest]["region_groups"].append(f"g{g}")
|
groups.setdefault(group, []).append((dest, dest))
|
||||||
|
if group not in entries[dest]["region_groups"]:
|
||||||
|
entries[dest]["region_groups"].append(group)
|
||||||
for entry in entries.values():
|
for entry in entries.values():
|
||||||
if not entry["regions"]:
|
if not entry["regions"]:
|
||||||
del entry["regions"], entry["region_groups"]
|
del entry["regions"], entry["region_groups"]
|
||||||
entry.pop("priority", None)
|
entry.pop("priority", None)
|
||||||
requested = rng.sample(["north-america", "europe", "japan", "uk", "canada"],
|
requested = rng.sample(["north-america", "europe", "japan", "uk", "canada"],
|
||||||
rng.randint(1, 3))
|
rng.randint(1, 3))
|
||||||
return groups, index, list(entries.values()), requested
|
held = {dest for dest in entries if rng.random() < 0.7}
|
||||||
|
return groups, index, list(entries.values()), requested, held
|
||||||
|
|
||||||
def test_two_thousand_random_platforms(self):
|
def test_two_thousand_random_platforms(self):
|
||||||
rng = random.Random(20261010)
|
rng = random.Random(20261010)
|
||||||
for trial in range(2000):
|
for trial in range(2000):
|
||||||
groups, index, entries, requested = self._case(rng)
|
groups, index, entries, requested, held = self._case(rng)
|
||||||
with self.subTest(trial=trial):
|
with self.subTest(trial=trial):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
install.region_drops(entries, requested),
|
install.region_drops(entries, requested),
|
||||||
region.resolve_region_drops(groups, index, requested),
|
region.resolve_region_drops(groups, index, requested),
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
install.region_drops(entries, requested, held=held),
|
||||||
|
region.resolve_region_drops(groups, index, requested, held),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_narrow_passes_the_installable_files_as_held(self):
|
||||||
|
"""The builder passes what the collection holds (generate_pack
|
||||||
|
_select_variants). An omission is the installer's unheld file."""
|
||||||
|
rng = random.Random(20261021)
|
||||||
|
for trial in range(500):
|
||||||
|
groups, index, entries, requested, held = self._case(rng)
|
||||||
|
files = [dict(e, size=1, cores=None) for e in entries if e["dest"] in held]
|
||||||
|
omitted = [dict(e, cores=None) for e in entries if e["dest"] not in held]
|
||||||
|
kept, still_omitted = install.narrow(files, omitted, [], [], requested)
|
||||||
|
drops = region.resolve_region_drops(groups, index, requested, held)
|
||||||
|
with self.subTest(trial=trial):
|
||||||
|
self.assertEqual(
|
||||||
|
{f["dest"] for f in kept},
|
||||||
|
{f["dest"] for f in files} - drops,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{o["dest"] for o in still_omitted},
|
||||||
|
{o["dest"] for o in omitted} - drops,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_lone_core_file_leaves_its_systems_drop(self):
|
||||||
|
"""Dolphin's GC/EUR/IPL.bin is a core extra no system names, plus the
|
||||||
|
GameCube's own declaration. The builder skips the one-file group, so
|
||||||
|
--region us drops it."""
|
||||||
|
entries = [
|
||||||
|
{"dest": "GC/USA/IPL.bin", "regions": ["north-america"],
|
||||||
|
"region_system_groups": ["nintendo-gamecube"]},
|
||||||
|
{"dest": "GC/EUR/IPL.bin", "regions": ["europe"],
|
||||||
|
"region_groups": [f"dolphin{region.FILE_GROUP}GC/EUR/IPL.bin"],
|
||||||
|
"region_system_groups": ["nintendo-gamecube"]},
|
||||||
|
]
|
||||||
|
self.assertEqual(
|
||||||
|
install.region_drops(entries, ["north-america"]), {"GC/EUR/IPL.bin"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Narrowing(unittest.TestCase):
|
class Narrowing(unittest.TestCase):
|
||||||
@@ -138,12 +181,21 @@ class Narrowing(unittest.TestCase):
|
|||||||
self.assertIn("scph5501.bin", files)
|
self.assertIn("scph5501.bin", files)
|
||||||
self.assertEqual(omitted, [])
|
self.assertEqual(omitted, [])
|
||||||
|
|
||||||
def test_an_unavailable_match_still_wins_as_the_builder_counts_it(self):
|
def test_an_unavailable_match_never_displaces_a_held_file(self):
|
||||||
|
"""The builder keeps the held BIOS when the requested one is missing
|
||||||
|
(region.resolve_region_drops, held). The missing one stays reported."""
|
||||||
files, omitted = self._dests([], [], ["europe"])
|
files, omitted = self._dests([], [], ["europe"])
|
||||||
self.assertNotIn("scph5500.bin", files)
|
self.assertIn("scph5500.bin", files)
|
||||||
self.assertNotIn("scph5501.bin", files)
|
self.assertIn("scph5501.bin", files)
|
||||||
self.assertEqual(omitted, ["scph5502.bin"])
|
self.assertEqual(omitted, ["scph5502.bin"])
|
||||||
|
|
||||||
|
def test_a_missing_japanese_bios_keeps_the_american_one(self):
|
||||||
|
files = [dict(self.FILES[1])]
|
||||||
|
omitted = [dict(self.OMITTED[0], dest="scph5500.bin", regions=["japan"])]
|
||||||
|
kept, still = install.narrow(files, omitted, [], [], ["japan"])
|
||||||
|
self.assertEqual([f["dest"] for f in kept], ["scph5501.bin"])
|
||||||
|
self.assertEqual([o["dest"] for o in still], ["scph5500.bin"])
|
||||||
|
|
||||||
def test_no_match_keeps_every_regional_file(self):
|
def test_no_match_keeps_every_regional_file(self):
|
||||||
files, _ = self._dests([], [], ["brazil"])
|
files, _ = self._dests([], [], ["brazil"])
|
||||||
self.assertIn("scph5500.bin", files)
|
self.assertIn("scph5500.bin", files)
|
||||||
|
|||||||
Reference in new issue
Block a user