From fb9b5592a58cbff1c20d3c54fe505b4c966b3026 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:26:36 +0100 Subject: [PATCH] feat: let --emulator take an alias name --emulator mednafen_psx refused with 'use --emulator beetle_psx' although the profile names its target. The alias now selects that profile and says so on stderr, in verify, generate_pack and cross_reference alike. Launchers are still refused, and pipeline step 5 skips aliases since they compare under the profile they name. --- scripts/common.py | 26 +++++++++++++-------- scripts/pipeline.py | 2 ++ tests/test_audit_regressions.py | 41 +++++++++++++++++++++++++-------- tests/test_e2e.py | 13 +++++++---- tests/test_native_mode.py | 12 ++++++---- 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/scripts/common.py b/scripts/common.py index 7bf60f02e..64b28fe08 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1870,12 +1870,12 @@ def select_emulator_profiles( all_profiles: dict, standalone: bool = False, ) -> list[tuple[str, dict]]: - """Resolve named profiles, refusing the ones that answer for others. + """Resolve named profiles, refusing the ones that answer for no core. - An alias is the same binary under another name and a launcher only - starts an emulator, so neither has requirements of its own: naming one - is a question about the wrong profile, and the error says which one to - ask instead. Raising rather than exiting lets the verifier stop and the + An alias is the same binary under another name, so naming one selects + the profile it is an alias of, said on stderr. A launcher only starts an + emulator and has no requirements of its own: naming one is a question + about the wrong profile, and the error says so. Raising rather than exiting lets the verifier stop and the builder return empty-handed from one rule. """ selected: list[tuple[str, dict]] = [] @@ -1916,11 +1916,17 @@ def select_emulator_profiles( ), None, ) - raise ProfileSelectionError( - f"{name} is an alias of {target}: use --emulator {target}" - if target - else f"{name} is an alias with no profile of its own to name" - ) + if not target or target not in all_profiles: + raise ProfileSelectionError( + f"{name} is an alias with no profile of its own to name" + ) + # The name a user reads in RetroArch or on the buildbot: the tool + # knows the profile, so it says which and carries on. + print(f"{name} is {target}: using its profile", file=sys.stderr) + name, profile = target, all_profiles[target] + kind = profile.get("type", "libretro") + if any(key == name for key, _ in selected): + continue if kind == "launcher": raise ProfileSelectionError( f"{name} is a launcher: use the emulator it launches" diff --git a/scripts/pipeline.py b/scripts/pipeline.py index 11d8c3f24..93d6e5210 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -541,6 +541,8 @@ def check_emulator_packs( all_ok = True compared = 0 for name in sorted(profiles): + if profiles[name].get("type") == "alias": + continue # selects the profile it names, compared under that name for standalone in (False, True): try: select_emulator_profiles([name], profiles, standalone) diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 3b2bfa674..3cc9cd6cf 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -5610,12 +5610,17 @@ class ANamedPackThatIsAbsentFails(unittest.TestCase): -class AnAliasNamesItsProfile(unittest.TestCase): - """"flycast_gles2 is an alias of flycast -use --emulator flycast": the - dash read as an option, and an alias without alias_of advised - "--emulator ?".""" +class AnAliasSelectsItsProfile(unittest.TestCase): + """`--emulator mednafen_psx` refused with "use --emulator beetle_psx", + a step the tool could take itself: the name a user reads in RetroArch + now selects the profile it is an alias of, and says so on stderr. An + alias without alias_of is found through the profile whose cores: names + it.""" + + def test_the_alias_selects_its_profile(self): + import contextlib as _contextlib + import io as _io - def test_the_message(self): from scripts.common import ProfileSelectionError, select_emulator_profiles profiles = { @@ -5623,12 +5628,28 @@ class AnAliasNamesItsProfile(unittest.TestCase): "flycast_gles2": {"type": "alias", "alias_of": "flycast"}, "orphan": {"type": "alias"}, "core": {"type": "libretro", "cores": ["orphan"]}, + "lost": {"type": "alias"}, } - for name, expected in (("flycast_gles2", "alias of flycast: use --emulator flycast"), - ("orphan", "alias of core: use --emulator core")): - with self.subTest(name=name), self.assertRaises(ProfileSelectionError) as caught: - select_emulator_profiles([name], profiles) - self.assertIn(expected, str(caught.exception)) + for name, expected in (("flycast_gles2", "flycast"), ("orphan", "core")): + err = _io.StringIO() + with self.subTest(name=name), _contextlib.redirect_stderr(err): + selected = select_emulator_profiles([name], profiles) + self.assertEqual([key for key, _ in selected], [expected]) + self.assertIn(f"{name} is {expected}", err.getvalue()) + with _contextlib.redirect_stderr(_io.StringIO()): + both = select_emulator_profiles(["flycast", "flycast_gles2"], profiles) + self.assertEqual([key for key, _ in both], ["flycast"]) + with self.assertRaises(ProfileSelectionError): + select_emulator_profiles(["lost"], profiles) + + def test_verify_runs_the_alias(self): + result = subprocess.run( + [sys.executable, "scripts/verify.py", "--emulator", "mednafen_psx"], + cwd=ROOT, capture_output=True, text=True, timeout=300, check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("beetle_psx", result.stdout) + self.assertIn("mednafen_psx is beetle_psx", result.stderr) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 4d0b203cd..b715cbb7f 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -17,7 +17,9 @@ Covers: from __future__ import annotations +import contextlib import hashlib +import io import os import shutil import sys @@ -1444,10 +1446,13 @@ class TestE2E(unittest.TestCase): with self.assertRaises(SystemExit): verify_emulator(["test_game"], self.emulators_dir, self.db, standalone=True) - def test_93_verify_emulator_alias_rejected(self): - """Alias profile produces error with redirect message.""" - with self.assertRaises(SystemExit): - verify_emulator(["test_alias"], self.emulators_dir, self.db) + def test_93_verify_emulator_alias_selects_its_profile(self): + """An alias verifies the profile it is an alias of.""" + with contextlib.redirect_stderr(io.StringIO()): + via_alias = verify_emulator(["test_alias"], self.emulators_dir, self.db) + direct = verify_emulator(["test_emu"], self.emulators_dir, self.db) + self.assertEqual(via_alias["emulators"], direct["emulators"]) + self.assertEqual(via_alias["details"], direct["details"]) def test_94_verify_emulator_launcher_rejected(self): """Launcher profile produces error.""" diff --git a/tests/test_native_mode.py b/tests/test_native_mode.py index e913bb930..100412e98 100644 --- a/tests/test_native_mode.py +++ b/tests/test_native_mode.py @@ -10,6 +10,8 @@ it, and these tests make the agreement checkable instead of reviewable. from __future__ import annotations +import contextlib +import io import json import sys import unittest @@ -446,10 +448,12 @@ class OneProfileSelector(unittest.TestCase): self.assertIsNone(error) self.assertEqual([n for n, _p in selected], ["real"]) - def test_an_alias_names_the_profile_to_ask_instead(self): - _selected, error = self._select(["ghost"]) - self.assertIn("alias of real", error) - self.assertIn("--emulator real", error) + def test_an_alias_selects_the_profile_it_names(self): + with contextlib.redirect_stderr(io.StringIO()) as err: + selected, error = self._select(["ghost"]) + self.assertIsNone(error) + self.assertEqual([n for n, _p in selected], ["real"]) + self.assertIn("ghost is real", err.getvalue()) def test_a_launcher_is_refused(self): _selected, error = self._select(["starter"])