From d8ee7439f5d94069cc8dfb92d0c706c9b46e9050 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:21:22 +0100 Subject: [PATCH] chore: tests follow the tick lists and found cards --- install.py | 15 +- tests/test_install_tui.py | 386 ++++++++++++++++++++++---------------- 2 files changed, 236 insertions(+), 165 deletions(-) diff --git a/install.py b/install.py index b14943937..ad918de5f 100644 --- a/install.py +++ b/install.py @@ -7904,9 +7904,11 @@ def fold_line(text: str, hang: str = " ") -> str: cut after a slash: left to the terminal, below 40 columns "Found Onion at /media/moi/ONION/BIOS" broke inside "(RetroArch files)" and a folder inside its name (tui_audit_17 P3-8).""" - cols = max(terminal_size()[0] - 1, 8) - if cell_width(text) <= cols or not _is_terminal(sys.stdout): - # A pipe or a log folds nothing: the line is kept whole there. + width = terminal_size()[0] + cols = max(width - 1, 8) + if cell_width(text) <= cols or width >= TUI_MIN_COLS or not _is_terminal(sys.stdout): + # A pipe or a log folds nothing, and from 40 columns a folder is left + # whole for the terminal to fold, copied as one line. return text lead = text[:len(text) - len(text.lstrip(" "))] prefix = lead + hang @@ -8626,6 +8628,11 @@ def all_in_place(files: list[dict], trees: list[dict], bios_path: Path) -> bool: for tree in trees)) +def _live_stdout() -> bool: + """Whether the standard output draws a moving line (Terminal.live).""" + return Terminal(sys.stdout).live + + def read_in_place(pairs: list[tuple[dict, Path]]) -> list[str]: """_classify_local of each (entry, path), read before the line menu as the check after it reads: on --jobs workers, a count moving at a @@ -8634,7 +8641,7 @@ def read_in_place(pairs: list[tuple[dict, Path]]) -> list[str]: size only, and the check after Enter reads none of them again.""" if not pairs: return [] - live = _is_terminal(sys.stdout) + live = _live_stdout() verdicts: list[str] = [] last = 0.0 with concurrent.futures.ThreadPoolExecutor(max_workers=LOCAL.jobs) as pool: diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 63c888c59..245730517 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -472,9 +472,12 @@ class Flow(unittest.TestCase): focus = next(i for i, row in enumerate(install.tui_rows(state)) if row.value == "nintendo-gba") state.focus["systems"] = focus - # Nothing ticked: Enter takes the system in focus alone. - self.assertEqual(press(state, "enter"), [("run", "confirm")]) + # A ticks the system in focus, B goes back to the plan, and Install + # there counts the choice first (tui_audit_17 P3-1). + self.assertEqual(press(state, "enter", "esc"), []) self.assertEqual(state.ticked, {"nintendo-gba"}) + self.assertEqual(state.screen, "plan") + self.assertEqual(press(state, "enter"), [("run", "confirm")]) self.assertEqual(state.screen, "confirm") self.assertIn("--system=nintendo-gba", install.tui_argv(state, "confirm", "py", "i.py")) self.assertIn("--dry-run", install.tui_argv(state, "confirm", "py", "i.py")) @@ -507,7 +510,7 @@ class Flow(unittest.TestCase): press(state, "enter") self.assertEqual(state.screen, "regions") # Ticked in the order of preference, then moved up. - regions = [row.value for row in install.tui_rows(state)] + regions = [row.value for row in install.tui_rows(state) if row.action == "region"] self.assertEqual(regions, ["japan", "north-america"]) press(state, "enter", "down", "enter") self.assertEqual(state.regions, ["japan", "north-america"]) @@ -516,7 +519,7 @@ class Flow(unittest.TestCase): # The rank moves, the row stays where it is, under the focus. self.assertEqual(state.focus["regions"], 1) self.assertEqual([row.label for row in install.tui_rows(state)], - ["[2] Japan", "[1] North America"]) + ["[2] Japan", "[1] North America", "None", "Done"]) press(state, "shift-down") self.assertEqual(state.regions, ["japan", "north-america"]) press(state, "esc", "down", "enter", "down", "right", "down", "down", "right", "right") @@ -534,8 +537,8 @@ class Flow(unittest.TestCase): state.focus["options"] = 3 press(state, "enter") self.assertEqual(state.screen, "cores") - self.assertEqual([row.value for row in install.tui_rows(state)][3:], - ["mesen", "pcsx_rearmed"]) + self.assertEqual([row.value for row in install.tui_rows(state) + if row.action == "core"], ["mesen", "pcsx_rearmed"]) self.assertEqual(install.tui_rows(state)[state.focus["cores"]].value, "mesen") # Every core ticked while none narrows the run: one untick keeps # the others, ticked back it is every core again. @@ -544,8 +547,11 @@ class Flow(unittest.TestCase): press(state, "right") self.assertEqual(state.cores, []) press(state, "/", "p", "c") - self.assertEqual([row.value for row in install.tui_rows(state)][3:], ["pcsx_rearmed"]) - press(state, "enter", "left", "enter") + self.assertEqual([row.value for row in install.tui_rows(state) + if row.action == "core"], ["pcsx_rearmed"]) + # Enter ends the typing on the first match, Left unticks it, and B + # clears the filter, then leaves. + press(state, "enter", "left", "esc", "esc") self.assertEqual(state.screen, "options") self.assertEqual(install.tui_flags(state), [("--core", "mesen")]) @@ -804,8 +810,9 @@ class Widgets(unittest.TestCase): state = self.systems_state(112) press(state, "end") lines = install.tui_view(state, 40, 15) - self.assertIn("> [x] system-111", "\n".join(lines)) - # The All, None and Filter rows are not counted among the systems. + self.assertIn("> Done", "\n".join(lines)) + self.assertIn("[x] system-111", "\n".join(lines)) + # The Filter, All, None and Done rows are not counted among the systems. self.assertTrue(any(re.fullmatch(r" \d+-112 of 112", line) for line in lines), lines) press(state, "pgup") self.assertLess(state.focus["systems"], 114) @@ -826,7 +833,8 @@ class Widgets(unittest.TestCase): "present": 0, "missing": 1, "wrong_hash": 0, "bytes_needed": 3}) press(state, "/", "M", "E", "G") - self.assertEqual([row.value for row in install.tui_rows(state)][3:], ["sega-genesis"]) + self.assertEqual([row.value for row in install.tui_rows(state) + if row.action == "system"], ["sega-genesis"]) press(state, "esc") self.assertEqual(state.filters["systems"], "") self.assertEqual(state.screen, "systems") @@ -844,7 +852,8 @@ class Widgets(unittest.TestCase): self.assertEqual(state.ticked, set()) press(state, "a") self.assertEqual(len(state.ticked), 5) - state.focus["systems"] = 1 + state.focus["systems"] = next(i for i, row in enumerate(install.tui_rows(state)) + if row.action == "none") # X on None does nothing: only A, an explicit choice, unticks all. press(state, " ") self.assertEqual(len(state.ticked), 5) @@ -1453,8 +1462,8 @@ class SmallFixes(unittest.TestCase): state.cli_regions = ["europe"] state.regions = list(state.cli_regions) state.stack += ["options", "regions"] - self.assertEqual([row.value for row in install.tui_rows(state)], - ["europe", "japan", "north-america"]) + self.assertEqual([row.value for row in install.tui_rows(state) + if row.action == "region"], ["europe", "japan", "north-america"]) press(state, "enter") self.assertEqual(state.regions, []) self.assertIn("europe", [row.value for row in install.tui_rows(state)]) @@ -1584,7 +1593,8 @@ class SmallFixes(unittest.TestCase): self.assertIn("[x] Sega - Saturn", narrow) self.assertNotIn("sega-saturn", narrow) press(state, "/", "s", "a", "t", "u") - self.assertEqual([row.value for row in install.tui_rows(state)][3:], ["sega-saturn"]) + self.assertEqual([row.value for row in install.tui_rows(state) + if row.action == "system"], ["sega-saturn"]) class CleanupSurvivesABrokenTerminal(unittest.TestCase): @@ -1690,8 +1700,8 @@ class AuditRoundTwo(unittest.TestCase): self.assertEqual(state.filters["systems"], "") self.assertEqual(self.focused(state).value, "sony-playstation") self.assertEqual(state.ticked, {"nintendo-gba"}) - self.assertIn("A/Enter Continue", install.tui_view(state, 80, 24)[-1]) - state.focus["systems"] = 2 + self.assertIn("A/Enter Tick", install.tui_view(state, 80, 24)[-1]) + state.focus["systems"] = 0 press(state, " ") self.assertEqual(state.editing, "") self.assertEqual(state.ticked, {"nintendo-gba"}) @@ -1702,7 +1712,8 @@ class AuditRoundTwo(unittest.TestCase): press(state, "down") self.assertTrue(install.tui_view(state, 80, 24)[-1].strip().startswith("A/Enter Open")) state = self.systems() - state.focus["systems"] = 0 + state.focus["systems"] = next(i for i, row in enumerate(install.tui_rows(state)) + if row.action == "all") self.assertIn("A/Enter Tick all", install.tui_view(state, 80, 24)[-1]) def test_p2_2_b_esc_and_q_stop_an_install(self): @@ -1717,7 +1728,7 @@ class AuditRoundTwo(unittest.TestCase): def test_p2_3_options_read_the_plan_again(self): state = self.systems() press(state, "n") - state.focus["systems"] = 3 + state.focus["systems"] = 1 press(state, "right") ticked = set(state.ticked) press(state, "esc", "o") @@ -1880,7 +1891,7 @@ class AuditRoundTwo(unittest.TestCase): def test_p2_11_a_narrow_bar_keeps_the_keys_to_finish(self): state = self.systems() self.assertEqual(install.tui_view(state, 40, 15)[-1].strip(), - "A Continue B Back X Tick ? Help") + "A Tick B Done ? Help") def test_p2_12_a_device_name_finds_its_setup(self): state = fresh_state() @@ -2270,9 +2281,10 @@ class AuditRoundThree(unittest.TestCase): # Only a marker on the part its BIOS folder is on names the card: # plumOS's and the Anbernic firmware's are on another part. for setup in install.card_setups(): - if setup.rules: - with self.subTest(system=setup.name): - self.assertEqual(rules[setup.name].bios, setup.below) + for rule in setup.rules: + with self.subTest(system=setup.name, rule=rule.values): + self.assertEqual(rule.bios, setup.below) + self.assertIn("ArkOS", rules) self.assertEqual(install.DEFAULT_DESTS["knulli"], Path("/userdata") / "bios") self.assertEqual(install.DEFAULT_DESTS["rocknix"], Path("/storage") / "roms/bios") @@ -3427,8 +3439,10 @@ class OneSystemInFewPresses(unittest.TestCase): # The buttons each route sends, as the keys the interface reads. PADS = { - "gptokeyb": {"A": "enter", "X": " ", "Start": "enter", "R1": "pgdn"}, - "muxterm": {"A": "enter", "X": " ", "Start": "enter", "R1": "right"}, + "gptokeyb": {"A": "enter", "B": "esc", "X": " ", "Start": "enter", "R1": "pgdn", + "L1": "pgup"}, + "muxterm": {"A": "enter", "B": "esc", "X": " ", "Start": "enter", "R1": "right", + "L1": "left"}, } def long_list(self, pad: str) -> install.TuiState: @@ -3448,17 +3462,22 @@ class OneSystemInFewPresses(unittest.TestCase): return install.tui_update(state, Key("char", name) if len(name) == 1 else Key(name)) def go_to(self, state, pad: str, target: str, paging: bool) -> int: - """Presses from the focus to the target system, as a person does.""" + """Presses from the focus to the target system, or to the row of that + action (none), as a person does.""" count = 0 while True: rows = install.tui_rows(state) focus = state.focus.get("systems", 0) - index = next(i for i, row in enumerate(rows) if row.value == target) + index = next(i for i, row in enumerate(rows) + if target in (row.value, row.action)) if index == focus: return count # A page is the rows drawn (tui_audit_9 P2-2). page = install._page_step(state) - button = "R1" if paging and index - focus >= page else "down" + if index > focus: + button = "R1" if paging and index - focus >= page else "down" + else: + button = "L1" if paging and focus - index >= page else "up" self.press_button(state, pad, button) count += 1 @@ -3471,23 +3490,18 @@ class OneSystemInFewPresses(unittest.TestCase): self.press_button(state, pad, button) count += 1 self.assertEqual(state.screen, "systems") + # On its first system (tui_audit_17 P3-1), None at the list's end. + self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "system") paging = pad != "muxterm" or fast - if fast: - self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "none") - self.press_button(state, pad, "A") - count += 1 - count += self.go_to(state, pad, "sys-45", paging) - effects = self.press_button(state, pad, "A") - count += 1 - else: - state.focus["systems"] = 3 - for button in ("up", "up", "A"): - self.press_button(state, pad, button) - count += 1 - count += self.go_to(state, pad, "sys-45", paging) - self.press_button(state, pad, "X") - effects = self.press_button(state, pad, "Start") - count += 2 + count += self.go_to(state, pad, "none", paging) + self.press_button(state, pad, "A") + count += 1 + count += self.go_to(state, pad, "sys-45", paging) + # A ticks it, B goes back to the plan, A there counts the choice. + self.press_button(state, pad, "A") + self.press_button(state, pad, "B") + effects = self.press_button(state, pad, "A") + count += 3 self.assertEqual(state.ticked, {"sys-45"}) self.assertEqual(effects, [("run", "confirm")]) # Install on the confirm screen, then Exit on the result. @@ -3499,10 +3513,10 @@ class OneSystemInFewPresses(unittest.TestCase): # focus, and muxterm had no page key). # A page is the rows drawn since tui_audit_9 P2-2, ten here where it # was nine: sys-45 is four pages and five Downs away, not five pages. - self.assertEqual(self.presses("gptokeyb", fast=False), 19) - self.assertEqual(self.presses("gptokeyb", fast=True), 16) - self.assertEqual(self.presses("muxterm", fast=False), 55) - self.assertEqual(self.presses("muxterm", fast=True), 16) + # None now at the list's end (tui_audit_17 P3-1): reached by pages. + self.assertEqual(self.presses("gptokeyb", fast=True), self.presses("gptokeyb", False)) + self.assertLessEqual(self.presses("gptokeyb", fast=True), 22) + self.assertLessEqual(self.presses("muxterm", fast=True), 22) def test_a_on_a_system_with_none_ticked_takes_it_alone(self): state = self.long_list("keys") @@ -3510,19 +3524,21 @@ class OneSystemInFewPresses(unittest.TestCase): state.ticked = set() state.focus["systems"] = 5 text = install.tui_view(state, 80, 24)[-1] - self.assertIn("Enter Only this system", text) + self.assertIn("Enter Tick", text) focused = install.tui_rows(state)[5].value - self.assertEqual(press(state, "enter"), [("run", "confirm")]) + self.assertEqual(press(state, "enter"), []) self.assertEqual(state.ticked, {focused}) def test_enter_still_continues_with_the_ticks_as_they_are(self): state = self.long_list("keys") state.stack.append("systems") state.focus["systems"] = 5 - # tui_audit_6 P2-1: Continue opens Confirm, which waits for A. - self.assertEqual(press(state, "enter"), [("run", "confirm")]) - self.assertEqual((state.screen, state.must_confirm), ("confirm", True)) - self.assertEqual(len(state.ticked), 66) + # tui_audit_6 P2-1: nothing runs from the list. A unticks the system + # in focus (tui_audit_17 P3-1), B goes back to the plan. + self.assertEqual(press(state, "enter"), []) + self.assertEqual((state.screen, len(state.ticked)), ("systems", 65)) + press(state, "esc") + self.assertEqual(state.screen, "plan") def test_left_and_right_page_inside_muxterm_only(self): for pad in ("muxterm", "gptokeyb_muxterm", "gptokeyb"): @@ -3681,12 +3697,16 @@ class AuditFive(unittest.TestCase): state = self.long_list() everything = set(state.ticked) press(state, "down", "enter") - self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "none") - press(state, "enter") + # The list opens on its first system, None at its end (tui_audit_17 P3-1). + self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "system") + press(state, "n") self.assertEqual(state.ticked, set()) - press(state, "up", "up") - self.assertEqual(install._primary(state), "A/Enter First system") + self.assertEqual(install._primary(state), "A/Enter Tick") + # Done with nothing ticked is refused, and says why. press(state, "esc") + self.assertEqual(state.screen, "systems") + self.assertIn("No system is ticked", " ".join(install.tui_view(state, 80, 24))) + press(state, "a", "esc") self.assertEqual(state.screen, "plan") self.assertEqual(state.ticked, everything) text = " ".join(install.tui_view(state, 80, 24)) @@ -3758,7 +3778,7 @@ class AuditFive(unittest.TestCase): state.files = [{"event": "file", "dest": "mame.zip", "size": 10, "systems": ["sys-00", "sys-01"]}] state.stack.append("systems") - state.focus["systems"] = 3 + state.focus["systems"] = install._first_item(state) lines = install.tui_view(state, 40, 12) self.assertIn(" Shared, counted once", lines) @@ -3778,12 +3798,12 @@ class AuditFive(unittest.TestCase): state.pad = "miyoo" press(state, "down", "enter") state.ticked = set() - state.focus["systems"] = 3 + state.focus["systems"] = install._first_item(state) bar = install.tui_view(state, 40, 15)[-1] - self.assertIn("A Tick", bar) - self.assertTrue(bar.strip().startswith("Start Only this")) - # tui_audit_6 P3-9.1: Back was the entry given up at 40 columns. - self.assertIn("R2 Back", bar) + # A is Space on a Miyoo, and ticks as Start does (tui_audit_17 P3-1). + self.assertTrue(bar.strip().startswith("A Tick")) + # tui_audit_6 P3-9.1: the way out was the entry given up at 40 columns. + self.assertIn("R2 Done", bar) def test_muxterm_help_names_no_home_key(self): state = self.long_list() @@ -4805,13 +4825,17 @@ class EndToEnd(unittest.TestCase): term.wait("> Install") term.send(DOWN, "\r") term.wait("Choose systems") - # D-pad and A only: up to None, A (on the first system), down to the - # second system, Right. - term.send(UP, UP, "\r", DOWN, RIGHT) - screen = term.wait("1 of 2 systems") - self.assertIn("> [x] sony-playstation", screen) + # D-pad and A only: the list opens on its first system, A unticks + # it, B goes back to the plan (tui_audit_17 P3-1). term.send("\r") - screen = term.wait("> Install") + screen = term.wait("1 of 2 systems") + self.assertIn("> [ ] nintendo-gba", screen) + self.assertIn("[x] sony-playstation", screen) + term.send("\x1b") + term.wait("for 1 of 2 systems") + # Install counts the choice on Confirm first. + term.send("\r") + screen = term.wait("files to download") self.assertIn("3 files to download", screen) term.send("\r") term.wait("3 files installed.") @@ -6028,14 +6052,14 @@ class AuditSix(unittest.TestCase): state = checked_state() press(state, "down", "enter") self.assertEqual(state.screen, "systems") - state.focus["systems"] = 3 - self.assertEqual(install._primary(state), "A/Enter Continue") - self.assertEqual(press(state, "enter"), [("run", "confirm")]) - self.assertEqual(state.screen, "confirm") - # The confirm run's plan is the same: it still waits for A. - install.tui_receive(state, ("event", state.run_id, dict(CHECK))) - self.assertEqual(install.tui_receive(state, ("ended", state.run_id, 0, [])), []) - self.assertEqual(state.screen, "confirm") + # A ticks there, and Done goes back to the plan: nothing runs from + # the list (tui_audit_17 P3-1). + self.assertEqual(install._primary(state), "A/Enter Tick") + self.assertEqual(press(state, "enter"), []) + self.assertEqual(state.screen, "systems") + self.assertEqual(press(state, "enter"), []) + self.assertEqual(press(state, "esc"), []) + self.assertEqual(state.screen, "plan") def test_offline_with_nothing_on_hand_runs_nothing_from_the_list(self): state = checked_state() @@ -6043,9 +6067,13 @@ class AuditSix(unittest.TestCase): state.focus["plan"] = [row.action for row in install.tui_rows(state)].index( "systems") press(state, "enter") - state.focus["systems"] = 3 - effects = press(state, "enter") - self.assertEqual(effects, [("run", "confirm")]) + # A ticks: the list runs nothing (tui_audit_17 P3-1). The way to + # Confirm is Space on the plan's Install. + self.assertEqual(press(state, "enter", "esc"), []) + state.focus["plan"] = 0 + state.must_confirm = True + install._push(state, "confirm") + install._start_run(state, "confirm") install.tui_receive(state, ("event", state.run_id, dict(CHECK))) for event in FILES: install.tui_receive(state, ("event", state.run_id, dict(event))) @@ -6277,12 +6305,12 @@ class AuditSix(unittest.TestCase): "systems": ["sys-00", "sys-01"]}] state.stack.append("systems") footers = set() - for focus in range(3, 12): + for focus in range(1, 10): state.focus["systems"] = focus lines = install.tui_view(state, 40, 12) footers.add(next(i for i, line in enumerate(lines) if "of 66 systems:" in line)) self.assertEqual(len(footers), 1, footers) - state.focus["systems"] = 3 + state.focus["systems"] = 1 self.assertIn(" +2 shared: counted once", install.tui_view(state, 40, 12)) @@ -8021,10 +8049,12 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, self.assertIn(option, command) self.assertNotIn("Copying", text) self.assertEqual(list(self.here.iterdir()), []) - # The screen: Choose systems, A on one system, then Confirm. + # The screen: Choose systems, A ticks, B back to the plan, whose + # Install counts the choice on Confirm first. state = checked_state() press(state, "down", "enter") self.assertEqual(state.screen, "systems") + press(state, "enter", "esc") effects = press(state, "enter") self.assertEqual((state.screen, effects), ("confirm", [("run", "confirm")])) @@ -8282,7 +8312,10 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings, """tui_audit_10 P3-13: "Detecting platform..." and "No supported platform found here", where the screen says setup and "Nothing was found here.\"""" - text, _asked = self.line_run(_args(declared_only=False), []) + # The standalone emulators' lists come from the network: none here + # (code_audit_24 P3-13), and none found. + with mock.patch.object(install, "_install_standalone_folders", return_value=False): + text, _asked = self.line_run(_args(declared_only=False), []) self.assertIn("Looking for setups here...", text) # Said once, the question alone after it (tui_audit_16 P3-2). self.assertEqual(text.count("Nothing was found here."), 1) @@ -9575,10 +9608,11 @@ class AuditSixteenAndNineScreens(unittest.TestCase): state = checked_state() state.pad = "miyoo" state.stack.append("systems") - state.focus["systems"] = 1 + state.focus["systems"] = next(i for i, row in enumerate(install.tui_rows(state)) + if row.action == "none") bar = install.tui_view(state, 40, 12)[-1] self.assertNotIn("A Tick", bar) - state.focus["systems"] = 3 + state.focus["systems"] = install._first_item(state) self.assertIn("A Tick", install.tui_view(state, 40, 12)[-1]) def test_system_given_is_said_and_widened(self): @@ -9893,9 +9927,10 @@ class AuditEighteenAndEleven(_CardFixtures, unittest.TestCase): """P3-11.""" state = checked_state() state.cli_systems, state.cores, state.regions = ["sony-playstation"], ["x"], ["us"] - state.cli_cores = ["x"] + state.cli_cores, state.cli_regions = ["x"], ["us"] text = " ".join(" ".join(install.tui_view(state, 120, 40)).split()) - self.assertIn("(--system), --core, --region.", text) + # The flag after the words, never alone (tui_audit_17 P3-4). + self.assertIn("(--system), 0 of 1 cores (--core), regions ranked (--region).", text) def test_pc_and_short_prefixes_find_the_computer_setups(self): """P3-17.""" @@ -10311,7 +10346,7 @@ class MouseOnEveryScreen(unittest.TestCase): self.assertEqual(effects, []) self.assertEqual(state.stack, base.stack) again = drawn(state, cols, rows) - self.assertIn(install.TUI_ARMED, str(again[-1])) + self.assertIn(install.TUI_ARMED_SHORT, str(again[-1])) at = next(first for row, first, _end, mark in state.hits.targets if mark == target) effects = tap(state, at, line) @@ -10712,10 +10747,10 @@ class AuditNineteenAndTwelve(_CardFixtures, unittest.TestCase): def test_the_help_and_the_bar_say_how_to_tap(self): state = self.systems_state() bar = str(install.tui_view(state, 80, 24)[-1]) - self.assertIn("X/Space/tap Tick", bar) + self.assertIn("A/Enter/tap Tick", bar) state.help = True text = " ".join(" ".join(install.tui_view(state, 80, 24)).split()) - self.assertIn("Tap Tick or untick a row, as Space", text) + self.assertIn("Tap Tick or untick a row, as A", text) self.assertIn("Any key or a tap closes this help", text) plan = checked_state() plan.mouse, plan.help = True, True @@ -10729,7 +10764,7 @@ class AuditNineteenAndTwelve(_CardFixtures, unittest.TestCase): if row.action == "system") drawn(state, 80, 24) ticked = set(state.ticked) - tap(state, bar_line.index("X/Space/tap"), len(lines) - 1) + tap(state, bar_line.index("A/Enter/tap"), len(lines) - 1) self.assertNotEqual(state.ticked, ticked) # No mouse, no word of a tap. plain = checked_state() @@ -11175,7 +11210,7 @@ class TapsOnAPty(unittest.TestCase): self.click(term, 20, row) term.wait("RetroBIOS: Choose systems") bar = term.settle().splitlines()[-1] - self.assertIn("X/Space/tap Tick", bar) + self.assertIn("A/Enter/tap Tick", bar) row = self.line_of(term, "sony-playstation") self.click(term, 20, row) self.assertIn("[ ] sony-playstation", term.settle()) @@ -11399,8 +11434,9 @@ class AuditTwentyAndThirteen(unittest.TestCase): values = [row.value for row in install.tui_rows(state) if row.action == "core"] self.assertEqual(values, ["pcsx_rearmed"]) rows = install.tui_rows(state) - self.assertEqual(rows[-1].label, "Cores of the other systems: 1") - state.focus["cores"] = len(rows) - 1 + more = next(i for i, row in enumerate(rows) if row.action == "morecores") + self.assertEqual(rows[more].label, "Cores of the other systems: 1") + state.focus["cores"] = more install.tui_update(state, Key("enter")) self.assertEqual([row.value for row in install.tui_rows(state) if row.action == "core"], ["mesen", "pcsx_rearmed"]) @@ -11556,7 +11592,7 @@ class AuditTwentyAndThirteen(unittest.TestCase): self.assertIn("? Help", bar) state.help = True text = " ".join(" ".join(install.tui_view(state, 40, 12)).split()) - self.assertIn("Tap Tick or untick a row, as Space", text) + self.assertIn("Tap Tick or untick a row, as A", text) def test_one_spelling_of_a_folder_on_the_plan_and_the_result(self): """tui_audit_13 P3-3: "/home...i_audit13/rb/bios" on the plan, the @@ -11950,9 +11986,11 @@ class DoubleTapsAcrossScreens(unittest.TestCase): slow = self.plan_on("systems") self.double_tap(slow, install.TUI_TAP_SETTLE + 0.2) self.assertEqual(once.screen, "systems") - # On Choose systems the first entry goes on to Confirm: the slow - # tap pressed it. - self.assertEqual(slow.screen, "confirm") + # On Choose systems the first entry ticks: the slow tap pressed it + # once more, and the box is back as it was (tui_audit_17 P3-1). + self.assertEqual(slow.screen, "systems") + self.assertEqual(slow.ticked, once.ticked ^ { + install.tui_rows(once)[once.focus["systems"]].value}) def test_a_release_whose_press_came_before_the_frame_does_nothing(self): state = self.plan_on("systems") @@ -12020,6 +12058,16 @@ class TwoTapsOnlyWhereFilesAreWritten(unittest.TestCase): install.tui_update(state, Key("enter")) self.assertEqual(state.screen, "systems") lines = drawn(state, 80, 24) + # A tap on the bar's first entry ticks (tui_audit_17 P3-1), B goes + # back, and Install counts the choice on Confirm first. + self.assertEqual(tap(state, 2, len(lines) - 1), []) + self.assertEqual(state.screen, "systems") + lines = drawn(state, 80, 24) + self.assertEqual(tap(state, str(lines[-1]).index("B/Esc"), len(lines) - 1), []) + self.assertEqual(state.screen, "plan") + lines = drawn(state, 80, 24) + state.focus["plan"] = 0 + lines = drawn(state, 80, 24) self.assertEqual(tap(state, 2, len(lines) - 1), [("run", "confirm")]) self.assertEqual(state.screen, "confirm") install.tui_receive(state, ("event", state.run_id, CHECK)) @@ -12124,10 +12172,20 @@ class TapsOnAPtySeeFirst(unittest.TestCase): row = self.line_of(term, "sony-playstation") self.click(term, 20, row) term.settle() - col, line = self.bar_entry(term, "Continue") + col, line = self.bar_entry(term, "Done") + report = sgr(col + 1, line + 1) + sgr(col + 1, line + 1, release=True) + # A double tap: the first goes back to the plan, the second lands on + # its Back inside the settle time and does nothing (tui_audit_14 + # P2-1). + time.sleep(install.TUI_TAP_SETTLE) + term.send(report + report) + screen = term.wait("for 1 of 2 systems") + screen = term.settle(0.6) + self.assertIn("RetroBIOS: RetroArch", screen) + self.assertEqual(repo.installed(), []) + # Install on the plan counts the choice on Confirm, which waits. + col, line = self.bar_entry(term, "Install") report = sgr(col + 1, line + 1) + sgr(col + 1, line + 1, release=True) - # A double tap: the first opens Confirm, the second lands on its - # Install inside the settle time and does nothing (tui_audit_14 P2-1). time.sleep(install.TUI_TAP_SETTLE) term.send(report + report) term.wait("Counted again for your choices") @@ -12135,6 +12193,7 @@ class TapsOnAPtySeeFirst(unittest.TestCase): self.assertIn("RetroBIOS: Confirm", screen) self.assertEqual(repo.installed(), []) # A tap made later installs: Confirm is the look before the install. + time.sleep(install.TUI_TAP_SETTLE) term.send(report) term.wait("files installed") self.assertNotEqual(repo.installed(), []) @@ -12317,7 +12376,9 @@ class AuditTwentyOneAndFourteen(unittest.TestCase): self.assertNotIn("Run the installer again", text) state.ticked = {"sony-playstation"} text = self.screen(state, 100, 40) - self.assertIn("About 0 files, 0 B, for 1 of 3 systems, 3 need the internet", text) + self.assertIn("3 files need the internet, which RETROBIOS_INSTALL_OFFLINE=1 does not " + "use, for 1 of 3 systems", text) + self.assertNotIn("About 0 files", text) def easyroms(self, name: str = "EASYROMS") -> Path: """A handheld's games part as a computer mounts it, with the folders @@ -12339,15 +12400,14 @@ class AuditTwentyOneAndFourteen(unittest.TestCase): mock.patch.object(install, "detect_frontends", return_value=[]), \ mock.patch.object(install.Path, "home", return_value=home): state, _effects = install.tui_start(_args(), "linux") - self.assertEqual(state.unnamed, [card]) text = self.screen(state) - self.assertIn(f"A card is in the reader ({name}): choose its system", - text) + # Named on the first screen, its plan one Enter away + # (tui_audit_17 P2-3). + self.assertIn(f"> ArkOS SD card ({name}", text) self.assertNotIn("Nothing was found here", text) install.tui_update(state, Key("enter")) - self.assertEqual(state.screen, "folder") - rows = install.tui_rows(state) - self.assertEqual(rows[0].value, card / "bios") + self.assertEqual(state.dest, card / "bios") + self.assertEqual(state.platform, "retroarch") install.tui_update(state, Key("enter")) self.assertEqual(state.dest, card / "bios") shutil.rmtree(card) @@ -12618,13 +12678,11 @@ class AuditTwentyOneAndFourteen(unittest.TestCase): def test_line_mode_puts_the_arkos_card_first(self): card = self.easyroms() text, asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True, - declared_only=False), ["1", ""], mounts=[card]) - self.assertIn("A card is in the reader (EASYROMS): choose its system.", text) + declared_only=False), [""], mounts=[card]) + # Found as the screen finds it, under Enter (tui_audit_17 P2-3). + self.assertIn(f"Found ArkOS at {card / 'bios'} (RetroArch files)", text) self.assertNotIn("Nothing was found here", text) - self.assertIn(f"The card in the reader keeps BIOS files in its bios folder: " - f"{card / 'bios'}.", text) - self.assertIn(f"1) EASYROMS: bios (SD card, {card / 'bios'})", text) - self.assertIn("Enter for the card's bios folder", asked[-2]) + self.assertIn("Enter for ArkOS SD card (EASYROMS)", asked[0]) self.assertIn(f"BIOS folder: {card / 'bios'}", text) @@ -12680,9 +12738,9 @@ class AuditTwentyTwoAndFifteen(unittest.TestCase): mounts = [data, stick] with mock.patch.object(install, "_mounted_cards", return_value=mounts), \ mock.patch.object(install, "_mount_points", return_value=frozenset()): - self.assertEqual(install.unnamed_cards(), []) + self.assertEqual(install.unproven_drives(), [data, stick]) state = self.first_screen(mounts) - self.assertEqual(state.unnamed, []) + self.assertEqual(state.setups, []) self.assertIn("Nothing was found here", self.screen(state)) state, text, rows = self.folder_screen(mounts) self.assertNotIn(rows[0].value, (data / "bios", stick / "bios")) @@ -12713,15 +12771,15 @@ class AuditTwentyTwoAndFifteen(unittest.TestCase): mounts = [arkos, amber, emuelec] with mock.patch.object(install, "_mounted_cards", return_value=mounts), \ mock.patch.object(install, "_mount_points", return_value=frozenset()): - self.assertEqual(install.unnamed_cards(), [arkos]) self.assertEqual([install.games_part(mount).name for mount in mounts], ["ArkOS", "AmberELEC", "EmuELEC"]) + # The three are their systems' own cards (tui_audit_17 P2-3). self.assertEqual([install.card_system(mount) for mount in mounts], - [None, ("amberelec", amber, "AmberELEC"), + [("retroarch", arkos / "bios", "ArkOS"), + ("amberelec", amber, "AmberELEC"), ("emuelec", emuelec, "EmuELEC")]) # One of these the system calls fixed is a disk, not a card. with mock.patch.object(install, "_drive_not_removable", return_value=True): - self.assertEqual(install.unnamed_cards(), []) self.assertEqual([install.card_system(mount) for mount in mounts], [None, None, None]) @@ -12733,18 +12791,16 @@ class AuditTwentyTwoAndFifteen(unittest.TestCase): card = games_part(self.root / "media" / "EASYROMS") mounts = [data, card] state = self.first_screen(mounts) - self.assertEqual(state.unnamed, [card]) - self.assertIn("A card is in the reader (EASYROMS): choose its system", self.screen(state)) - state, text, rows = self.folder_screen(mounts) - self.assertEqual(rows[0].value, card / "bios") - self.assertIn(self.UNPROVEN_ONE, text) + # The card is the setup found, under Enter (tui_audit_17 P2-3). + text = self.screen(state) + self.assertIn("> ArkOS SD card (EASYROMS", text) + with mock.patch.object(install, "_mounted_cards", return_value=mounts), \ + mock.patch.object(install, "_mount_points", return_value=frozenset()): + install.tui_update(state, Key("enter")) + self.assertEqual(state.dest, card / "bios") text, asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True, - declared_only=False), ["1", ""], mounts=mounts) - self.assertIn("A card is in the reader (EASYROMS): choose its system.", text) - self.assertIn(f"The card in the reader keeps BIOS files in its bios folder: " - f"{card / 'bios'}.", text) - self.assertIn(f"1) EASYROMS: bios (SD card, {card / 'bios'})", text) - self.assertIn(self.UNPROVEN_ONE, text) + declared_only=False), [""], mounts=mounts) + self.assertIn("Enter for ArkOS SD card (EASYROMS)", asked[0]) self.assertIn(f"BIOS folder: {card / 'bios'}", text) def test_two_cards_are_spoken_of_in_the_plural(self): @@ -12754,15 +12810,16 @@ class AuditTwentyTwoAndFifteen(unittest.TestCase): second = games_part(self.root / "media" / "ROMS2") mounts = [arkos, second] state = self.first_screen(mounts) - self.assertIn("Cards are in the reader (EASYROMS, ROMS2): choose their systems", - self.screen(state)) - state, text, rows = self.folder_screen(mounts) - self.assertIn("EASYROMS keeps BIOS files in its bios folder, first below.", text) + # Each card its folders prove is a setup found, by its own name + # (tui_audit_17 P2-3). + text = self.screen(state) + self.assertIn("ArkOS SD card (EASYROMS", text) + self.assertIn("ArkOS SD card (ROMS2", text) text, asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True, - declared_only=False), ["1", ""], mounts=mounts) - self.assertIn("Cards are in the reader (EASYROMS, ROMS2): choose their systems.", text) - self.assertIn(f"EASYROMS keeps BIOS files in its bios folder: {arkos / 'bios'}.", text) - self.assertNotIn("The card in the reader", text) + declared_only=False), [], mounts=mounts) + self.assertIn("ArkOS SD card (EASYROMS)", text) + self.assertIn("ArkOS SD card (ROMS2)", text) + self.assertNotIn("in the reader", text) def test_a_fixed_partition_is_told_from_a_card(self): """code_audit_22 P2-1: every mount under /media and /mnt, and on @@ -13492,14 +13549,16 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): mounts = [*cards, data, music] with mock.patch.object(install, "_mounted_cards", return_value=mounts), \ mock.patch.object(install, "_mount_points", return_value=frozenset()): - self.assertEqual(install.unnamed_cards(), cards) + self.assertEqual([install.card_system(card) for card in cards], + [("retroarch", card / "bios", "ArkOS") for card in cards]) self.assertEqual([install.games_part(card).name for card in cards], ["ArkOS"] * len(cards)) self.assertIsNone(install.games_part(data)) self.assertIsNone(install.games_part(music)) self.assertEqual(install.unproven_drives(), [data]) with mock.patch.object(install, "_drive_not_removable", return_value=True): - self.assertEqual(install.unnamed_cards(), []) + self.assertEqual([install.card_system(card) for card in cards], + [None] * len(cards)) # Root cause 2: counts and states that tell the truth. @@ -13747,7 +13806,9 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): for cols in (80, 39, 30): with self.subTest(cols=cols): with mock.patch.object(install.shutil, "get_terminal_size", - return_value=os.terminal_size((cols, 11))): + return_value=os.terminal_size((cols, 11))), \ + mock.patch.object(install, "_install_standalone_folders", + return_value=False): text, _asked = self.line_run(_args(declared_only=False), []) block = text.split("Which setup?", 1)[1].split(" q) quit", 1)[0] lines = [line for line in block.splitlines() if line.strip()] @@ -13760,27 +13821,29 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): def test_on_wsl_the_card_comes_first_in_both_modes(self): """code_audit_23 P3-6: line mode numbered Windows RetroArch 1) and - the card 2), where the screen put the card first.""" - card = games_part(self.root / "media" / "EASYROMS") + the card 2), where the screen put the card first. One order for + both: the other side first, then the drives (an ArkOS card is now a + setup found, tui_audit_17 P2-3).""" + data = self.drive("DATA", ("bios", "snes")) windows = Path("/mnt/c/RetroArch-Win64/system") side = [("Windows RetroArch: C:\\RetroArch-Win64\\system", windows)] - with mock.patch.object(install, "_mounted_cards", return_value=[card]), \ + with mock.patch.object(install, "_mounted_cards", return_value=[data]), \ mock.patch.object(install, "_mount_points", return_value=frozenset()), \ mock.patch.object(install, "other_side", return_value=side), \ mock.patch.object(install, "_ask", return_value="q"), \ contextlib.redirect_stdout(io.StringIO()) as out, \ contextlib.suppress(SystemExit): install.ask_platform_folder("wsl", "retroarch") - state = self.first_screen([card]) - self.assertRegex(out.getvalue(), r"\n 1\) EASYROMS: bios \(SD card") - self.assertRegex(out.getvalue(), r"\n 2\) Windows RetroArch") - with mock.patch.object(install, "_mounted_cards", return_value=[card]), \ + state = self.first_screen([data]) + self.assertRegex(out.getvalue(), r"\n 1\) Windows RetroArch") + self.assertRegex(out.getvalue(), r"\n 2\) DATA: bios \(drive") + with mock.patch.object(install, "_mounted_cards", return_value=[data]), \ mock.patch.object(install, "_mount_points", return_value=frozenset()), \ mock.patch.object(install, "other_side", return_value=side): state.os_type = "wsl" install.tui_update(state, Key("enter")) rows = [row for row in install.tui_rows(state) if row.action == "drive"] - self.assertEqual([row.value for row in rows], [card / "bios", windows]) + self.assertEqual([row.value for row in rows], [windows, data / "bios"]) def test_nothing_found_is_said_once(self): """tui_audit_16 P3-2: "Nothing was found here." twice, and after "A @@ -13789,7 +13852,8 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): self.assertEqual(text.count("Nothing was found here."), 1) card = games_part(self.root / "media" / "EASYROMS") text, _asked = self.line_run(_args(declared_only=False), [], mounts=[card]) - self.assertIn("A card is in the reader (EASYROMS): choose its system.", text) + # The card its folders prove is found (tui_audit_17 P2-3). + self.assertIn("ArkOS SD card (EASYROMS)", text) self.assertNotIn("Nothing was found here", text) def test_the_bios_only_drive_note_is_one_plain_sentence(self): @@ -13798,15 +13862,13 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): them" after a title.""" data = self.drive("DATA", ("bios", "snes", "Music")) stick = self.drive("STICK", ("bios",)) - card = games_part(self.root / "media" / "EASYROMS") - mounts = [card, data, stick] + mounts = [data, stick] state, text, rows = self.folder_screen(mounts) self.assertIn("DATA and STICK have a bios folder but no folder a handheld system " "writes. Pick one below if your files go there.", text) - self.assertIn("The card in the reader keeps BIOS files in its bios folder", text) labels = [row.label for row in rows] self.assertIn("DATA: bios (drive)", labels) - self.assertIn("EASYROMS: bios (SD card)", labels) + self.assertIn("STICK: bios (drive)", labels) line, _asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True, declared_only=False), ["1", ""], mounts=mounts) for said in (text, line): @@ -13873,7 +13935,7 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): at 40x12 dropped the folder "Use this folder" stands for.""" data = self.drive("DATA", ("bios", "snes", "Music")) stick = self.drive("STICK", ("bios",)) - for mounts in ([data, stick], [games_part(self.root / "media" / "EASYROMS"), data]): + for mounts in ([data, stick], [data]): with self.subTest(mounts=[mount.name for mount in mounts]): state, _text, _rows = self.folder_screen(mounts) with mock.patch.object(install, "_mounted_cards", return_value=mounts), \ @@ -13916,7 +13978,7 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): press(state, " ") self.assertEqual(self.boxes(state), {"mesen": "[x]", "pcsx_rearmed": "[x]"}) self.assertEqual(state.cores, []) - press(state, " ", "enter") + press(state, " ", "esc") self.assertEqual(state.screen, "options") row = next(row for row in install.tui_rows(state) if row.action == "cores") self.assertEqual(row.label, "Cores: 1 of 2") @@ -13929,6 +13991,7 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): state.focus["cores"] = next(i for i, row in enumerate(rows) if row.action == "none") press(state, "enter") self.assertEqual(self.boxes(state), {"mesen": "[ ]", "pcsx_rearmed": "[ ]"}) + state.focus["cores"] = next(i for i, row in enumerate(rows) if row.action == "done") press(state, "enter") self.assertEqual(state.screen, "cores") self.assertIn("No core is ticked", str(state.status)) @@ -13955,7 +14018,7 @@ class AuditTwentyThreeAndSixteen(unittest.TestCase): self.assertIn("Every core's extra files are kept: Enter keeps them all", text) head = text.rsplit("Counted again for your choices", 1)[1] self.assertIn("1 of 2 cores", head) - self.assertEqual(install.cores_words(0, 2), "all cores") + self.assertEqual(install.cores_words(0, 2), "0 of 2 cores") self.assertEqual(install.cores_words(2, 2), "all cores") # Root cause 6: the pages say what the installer shows. @@ -14269,8 +14332,9 @@ class AuditTwentyFourAndSeventeen(unittest.TestCase): (self.here / "a.bin").write_bytes(payload) listing = core_list(self.root / "read.json", [self.entry("a.bin", payload, ["sony-playstation"])]) - text, _asked = self.line_run(_args(manifest=listing, dest=self.here, - declared_only=False), []) + with mock.patch.object(install, "_live_stdout", return_value=True): + text, _asked = self.line_run(_args(manifest=listing, dest=self.here, + declared_only=False), []) self.assertIn("\r read 1/1", text) self.assertLess(text.index("read 1/1"), text.index("every one in place"))