fix: ask a card setup for its own card

A card system chosen on a computer, from --platform, the setup list, its
filter or the line-mode menu, always asks for its card. The card screen
lists only the cards whose files say they run that system, offers Look
again, and checks a browsed, typed or --dest folder as --dest is checked.
The line-mode Card root prompt lists the mounted drives, asks again on a
missing or foreign root, and asks before creating several folders. On the
device the detected folder is offered, and the bound card is forgotten
when another setup is chosen.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 00:04:56 +01:00
1 parent 44378ae5c9
commit 44f94ab703
15 files changed
+629 -153

No files matched your search

+288 -14
View File
@@ -40,6 +40,7 @@ sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(REPO_ROOT / "tests"))
import install # noqa: E402
from test_install_app import make_card # noqa: E402
from test_install_selection import FakeRepository, _QuietHandler # noqa: E402
try:
@@ -1294,6 +1295,20 @@ class ControlCharacters(unittest.TestCase):
class SmallFixes(unittest.TestCase):
def test_a_system_name_is_said_once_with_a_pack(self):
"""tui_audit_5 P3-8.1: "Onion gets the RetroArch file list" was
printed by the pack check, then again where the platform is read."""
pack = mock.Mock(files=[Path("RetroArch_BIOS_Pack.zip")])
pack.name = "RetroArch_BIOS_Pack.zip"
pack.platforms.return_value = ["retroarch"]
args = argparse.Namespace(manifest=None, from_pack=Path("p.zip"), platform="onion",
**{"from": None})
with mock.patch.object(install, "PackSource", return_value=pack), \
contextlib.redirect_stdout(io.StringIO()) as out:
install._open_local_sources(args)
self.assertNotIn("Onion gets", out.getvalue())
install.LOCAL.reset()
def test_a_command_line_region_the_list_lacks(self):
state = checked_state()
state.cli_regions = ["europe"]
@@ -1727,7 +1742,8 @@ class AuditRoundTwo(unittest.TestCase):
state = fresh_state()
state.stack = ["platforms"]
press(state, "/", "m", "u", "o", "s")
self.assertEqual([row.value for row in install.tui_rows(state)][1:], ["retroarch"])
self.assertEqual([row.value for row in install.tui_rows(state)][1:],
[("retroarch", "muOS")])
def test_p2_13_a_long_preview_wraps_only_what_is_in_view(self):
state = checked_state()
@@ -2130,6 +2146,11 @@ class AuditRoundThree(unittest.TestCase):
def test_p1_1_a_drive_letter_names_the_card(self):
self.assertEqual(install._mount_name(PureWindowsPath("E:\\")), "E:\\")
self.assertEqual(install._mount_name(Path("/media/me/MUOS")), "MUOS")
# tui_audit_5 P3-2: under WSL the card screen listed "d (card or drive)".
with mock.patch.dict(install._WSL, {"active": True}):
self.assertEqual(install._mount_name(Path("/mnt/d")), "D:")
self.assertEqual(install._mount_name(Path("/mnt/wslg")), "wslg")
self.assertEqual(install._mount_name(Path("/mnt/d")), "d")
def test_p1_1_on_the_device_its_card_is_not_offered_twice(self):
root = self.card(self.CARDS["muOS"][0])
@@ -2849,23 +2870,201 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
self.assertEqual(install.card_install_dest("knulli", "knulli", True),
knulli / "bios")
def test_the_interface_opens_the_card_screen_with_the_cards_first(self):
def ask(self, *answers: str):
"""_ask answering in turn, and the prompts it was asked."""
given = iter(answers)
asked: list = []
def answer(prompt: str) -> str:
asked.append(prompt)
return next(given)
return mock.patch.object(install, "_ask", side_effect=answer), asked
def test_a_typed_root_that_does_not_exist_is_asked_again(self):
"""code_audit_12 P2-3: a typo four folders deep was created and
filled, and a menu answer (8) became the card."""
onion = self.card(self.ONION)
typo = REPO_ROOT / "tmp" / "no-such" / "media" / "NEXT"
patcher, asked = self.ask("8", str(typo), str(onion))
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest("onion", "retroarch", interactive=True)
self.assertEqual(dest, onion / "BIOS")
self.assertEqual(len(asked), 3)
self.assertIn("8 does not exist, or is not a folder. Type the root of the Onion card.",
out.getvalue())
self.assertFalse((REPO_ROOT / "tmp" / "no-such").exists())
def test_a_typed_root_that_is_no_card_of_that_system_is_asked_again(self):
"""code_audit_12 P3-4: a folder that was no Onion card ended the run."""
blank, onion = self.card({"roms/a": ""}), self.card(self.ONION)
patcher, asked = self.ask(str(blank), str(onion))
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest("onion", "retroarch", interactive=True)
self.assertEqual((dest, len(asked)), (onion / "BIOS", 2))
self.assertIn(f"{blank} is not a Onion card: it holds no .tmp_update.", out.getvalue())
def test_a_minui_root_is_a_minui_card(self):
blank = self.card({"roms/a": ""})
(blank / "BIOS").mkdir()
minui = self.card({".system/version.txt": "NextUI-20250101\n", "Bios/x": ""})
patcher, asked = self.ask(str(minui / "Bios"), str(blank), str(minui))
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest("nextui", "nextui", interactive=True)
self.assertEqual((dest, len(asked)), (minui, 3))
self.assertIn(f"is inside a NextUI card, whose root is {minui}", out.getvalue())
self.assertIn("holds no .system/version.txt: it is not a NextUI card", out.getvalue())
def test_the_mounted_drives_are_listed_and_taken_by_number(self):
"""tui_audit_5 P3-8.4: the screen lists the mounted cards, the
prompt did not."""
muos, blank = self.card({"MUOS/bios/x": ""}), self.card({"roms/a": ""})
patcher, _asked = self.ask("2")
with mock.patch.object(install, "_mounted_cards", return_value=[muos, blank]), \
patcher, contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest("knulli", "knulli", interactive=True)
self.assertEqual(dest, blank / "bios")
self.assertIn(f"2) {blank} (card or drive)", out.getvalue())
self.assertIn(f"1) {muos} (muOS card)", out.getvalue())
def test_creating_several_folders_on_the_card_is_asked(self):
root = self.card({"roms/a.txt": ""})
rocknix = root / "R"
rocknix.mkdir()
patcher, asked = self.ask(str(rocknix), "n")
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
contextlib.redirect_stdout(io.StringIO()), \
self.assertRaises(SystemExit) as stop:
install.card_install_dest("rocknix", "rocknix", interactive=True)
self.assertEqual(stop.exception.code, 2)
self.assertIn("[y/N]", asked[-1])
self.assertFalse((rocknix / "roms").exists())
def test_the_menu_asks_a_card_system_for_its_card(self):
"""code_audit_12 P3-3: ROCKNIX picked from the menu on a PC was
offered /storage/roms/bios."""
card = self.card({"roms/a": ""})
make_card(card, "ROCKNIX")
patcher, asked = self.ask(str(card))
with mock.patch.object(install, "_choose_platform", return_value="rocknix"), \
mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
contextlib.redirect_stdout(io.StringIO()):
chosen = install._prompt_manual_platform("linux")
self.assertEqual(chosen, [("rocknix", card / "roms" / "bios")])
self.assertEqual(asked, ["Card root (Ctrl+C to stop): "])
def test_a_card_of_another_system_is_not_offered(self):
"""tui_audit_5 P1-1: A on the first mount wrote Onion's BIOS/ on a
muOS card, which reads MUOS/bios."""
muos, blank = self.card({"MUOS/bios/x": ""}), self.card({"roms/a": ""})
found = [("retroarch", SYSTEM_DIR)]
with mock.patch.object(install, "_mounted_cards", return_value=[blank]), \
with mock.patch.object(install, "_mounted_cards", return_value=[muos, blank]), \
mock.patch.object(install, "detect_platforms", return_value=found), \
mock.patch.object(install, "detect_frontends", return_value=[]):
state, effects = install.tui_start(_args(platform="onion"), "linux")
self.assertEqual((state.screen, effects), ("folder", []))
rows = install.tui_rows(state)
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
effects = install.tui_update(state, Key("enter"))
self.assertEqual([row.action for row in rows], ["card", "browse", "type"])
self.assertEqual([row.action for row in rows], ["browse", "type", "rescan"])
self.assertEqual(state.focus["folder"], 0)
self.assertIn("Where is the SD card?", text)
self.assertIn("Onion keeps its BIOS files on its SD card", text)
self.assertEqual(effects, [("fetch", "retroarch")])
self.assertEqual(state.dest, blank / "BIOS")
self.assertIn("No Onion card is mounted. Insert it, then choose Look again.", text)
def test_only_the_cards_of_that_system_are_listed(self):
first, second = self.card(self.ONION), self.card(self.ONION)
muos = self.card({"MUOS/bios/x": ""})
state = fresh_state()
state.stack = ["setup", "platforms"]
state.filters["platforms"] = "onion"
state.focus["platforms"] = 1
with mock.patch.object(install, "_mounted_cards", return_value=[muos, first, second]):
press(state, "enter")
self.assertEqual(state.screen, "folder")
rows = install.tui_rows(state)
install.tui_update(state, Key("enter"))
self.assertEqual([(row.action, row.value) for row in rows][:2],
[("card", first), ("card", second)])
self.assertNotIn("rescan", [row.action for row in rows])
self.assertEqual(state.dest, first / "BIOS")
# Titled as the card found by itself (tui_audit_5 P3-7).
self.assertEqual(install.detected_os("retroarch", first / "BIOS"), "Onion")
def test_look_again_finds_the_card_inserted(self):
onion = self.card(self.ONION)
mounts: list = []
with mock.patch.object(install, "_mounted_cards", side_effect=lambda: list(mounts)), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]):
state, _effects = install.tui_start(_args(platform="onion"), "linux")
press(state, "down", "down", "enter")
self.assertEqual(state.status, "Still no Onion card is mounted.")
mounts.append(onion)
press(state, "enter")
rows = install.tui_rows(state)
self.assertEqual(rows[state.focus["folder"]].action, "card")
self.assertEqual(state.status, "")
def test_a_folder_that_is_no_card_of_that_system_is_refused(self):
blank = self.card({"roms/a": ""})
muos = self.card({"MUOS/bios/x": ""})
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]):
state, _effects = install.tui_start(_args(platform="onion"), "linux")
press(state, "enter")
self.assertEqual(state.screen, "browse")
install._browse_to(state, blank)
effects = press(state, "enter")
self.assertEqual((effects, state.screen), ([], "browse"))
self.assertIn(f"{blank} is not a Onion card: it holds no .tmp_update.",
state.status)
self.assertIsNone(state.dest)
# A muOS card gets the folder muOS reads, as --dest does.
install._browse_to(state, muos)
press(state, "enter")
self.assertEqual(state.dest, muos / "MUOS" / "bios")
def test_a_typed_card_root_is_checked_on_the_path_screen(self):
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]):
state, _effects = install.tui_start(_args(platform="rocknix"), "linux")
press(state, "down", "enter")
self.assertEqual(state.screen, "path")
state.path_text = str(REPO_ROOT / "tmp" / "no-such-card")
effects = press(state, "enter")
self.assertEqual((effects, state.screen), ([], "path"))
self.assertIn("does not exist, or is not a folder. Choose the root of the "
"ROCKNIX card.", state.status)
def test_a_dest_that_is_no_card_of_that_system_is_refused(self):
blank = self.card({"roms/a": ""})
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]):
state, effects = install.tui_start(_args(platform="onion", dest=str(blank)),
"linux")
self.assertEqual((effects, state.screen), ([], "folder"))
self.assertIn("is not a Onion card", state.status)
def test_change_folder_names_the_folder_it_reuses(self):
"""code_audit_12 P3-1: Use this folder on the card screen named
nothing."""
onion = self.card(self.ONION)
state = checked_state()
state.platform, state.card_bound = "retroarch", ("Onion", "BIOS")
state.dest = onion / "BIOS"
press(state, "o")
rows = install.tui_rows(state)
state.focus["options"] = [row.action for row in rows].index("folder")
press(state, "enter")
with mock.patch.object(install, "_mounted_cards", return_value=[]):
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertEqual(state.screen, "folder")
self.assertIn("Where is the SD card?", text)
self.assertIn("The BIOS folder chosen for Onion:", text)
self.assertEqual(state.folder_default, onion / "BIOS")
def test_the_interface_takes_the_one_onion_card_mounted(self):
onion = self.card(self.ONION)
@@ -2914,17 +3113,22 @@ class TheSetupFilterKnowsDevices(unittest.TestCase):
state = fresh_state()
state.stack = ["platforms"]
state.filters["platforms"] = needle
return [row for row in install.tui_rows(state) if row.action == "platform"]
return [row for row in install.tui_rows(state)
if row.action in ("platform", "cardsystem")]
def test_each_device_finds_every_setup_it_runs(self):
for needle, setups in self.TYPED.items():
with self.subTest(needle=needle):
self.assertEqual({row.value for row in self.rows(needle)}, setups)
self.assertEqual({row.value[0] if row.action == "cardsystem" else row.value
for row in self.rows(needle)}, setups)
def test_the_right_column_names_what_matched(self):
retroarch = {row.value: row for row in self.rows("miyoo")}["retroarch"]
self.assertEqual(retroarch.right.split(", "),
["spruceOS", "Onion", "Allium", "Koriki", "MiyooCFW"])
rows = self.rows("miyoo")
retroarch = {row.value: row for row in rows}["retroarch"]
self.assertEqual(retroarch.right, "MiyooCFW")
self.assertEqual([row.label for row in rows if row.action == "cardsystem"],
["spruceOS, SD card", "Onion, SD card", "Allium, SD card",
"Koriki, SD card"])
deck = {row.value: row for row in self.rows("steam deck")}
self.assertEqual(deck["retroarch"].right, "Steam Deck")
self.assertIn("muOS", {row.value: row for row in self.rows("")}["retroarch"].right)
@@ -2943,6 +3147,76 @@ class TheSetupFilterKnowsDevices(unittest.TestCase):
tuple(str(name) for name in system["devices"]))
class ACardSetupIsOneDesign(unittest.TestCase):
"""code_audit_12 P2-1 to P3-2, tui_audit_5 P1-2: a setup that runs from
its SD card, chosen on a computer, always asks for that card."""
def platforms(self, needle: str = "") -> install.TuiState:
state = fresh_state()
state.stack = ["setup", "platforms"]
state.filters["platforms"] = needle
state.focus["platforms"] = 1
return state
def test_a_handheld_typed_in_the_filter_asks_for_its_card(self):
for needle in ("onion", "miyoo mini", "rg35xx"):
with self.subTest(needle=needle):
state = self.platforms(needle)
with mock.patch.object(install, "_mounted_cards", return_value=[]):
row = install.tui_rows(state)[1]
effects = press(state, "enter")
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertEqual(row.action, "cardsystem")
self.assertEqual((effects, state.screen), ([], "folder"))
self.assertEqual(state.card_bound, install.card_bound(row.value[1], "retroarch"))
self.assertIn("Where is the SD card?", text)
self.assertNotIn("default", [r.action for r in install.tui_rows(state)])
self.assertNotIn(str(SYSTEM_DIR), text)
def test_retroarch_typed_is_still_this_computer(self):
state = self.platforms("retroarch")
press(state, "enter")
self.assertIsNone(state.card_bound)
self.assertEqual(state.folder_default, SYSTEM_DIR)
def test_a_card_backed_out_of_is_forgotten(self):
state = fresh_state()
state.stack = ["setup"]
state.focus["setup"] = 2
press(state, "enter")
state.focus["platforms"] = install._platform_focus(state, "rocknix")
with mock.patch.object(install, "_mounted_cards", return_value=[]):
press(state, "enter")
self.assertEqual(state.card_bound, ("ROCKNIX", "roms/bios"))
press(state, "esc", "esc")
self.assertEqual(state.screen, "setup")
state.focus["setup"] = 0
with mock.patch.object(install.Path, "exists", return_value=True):
press(state, "enter")
self.assertIsNone(state.card_bound)
self.assertEqual((state.platform, state.dest), ("retroarch", SYSTEM_DIR))
install._open_folder_screen(state, state.dest, "chosen")
title = install.tui_view(state, 80, 24)[0]
self.assertIn("Which folder?", title)
self.assertNotIn("card", [row.action for row in install.tui_rows(state)])
def test_on_the_device_its_own_folder_is_offered(self):
share = Path("/userdata/bios")
state = fresh_state()
state.setups = [("knulli", share, "KNULLI")]
state.stack = ["setup", "platforms"]
state.focus["platforms"] = install._platform_focus(state, "knulli")
with mock.patch.dict(install._DETECTED_OS, {("knulli", share): "KNULLI"}), \
mock.patch.object(install, "_mounted_cards", return_value=[]):
press(state, "enter")
rows = [row.action for row in install.tui_rows(state)]
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertEqual(rows, ["default", "browse", "type"])
self.assertEqual((state.folder_default, state.folder_kind), (share, "found"))
self.assertIn("Which folder? KNULLI reads its BIOS files from: /userdata/bios", text)
self.assertNotIn("No KNULLI card is mounted", text)
class OneSystemInFewPresses(unittest.TestCase):
"""tui_audit_4 P3-10.11: one system with a controller took 15 presses on
the fixture list, about 60 on muxterm, which has no page key."""