From fe1e98917bfaaee5dfb32dc1a2f61c7cffcdfe85 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:17:26 +0100 Subject: [PATCH] fix: offer only what an install can do Back from the systems list with nothing ticked keeps the ticks it opened with, and None says First system once nothing is left to untick. Confirm under --offline with nothing on hand has no Install, and the line mode no longer asks to download what --offline never fetches. The line mode counts the data files before its question. Screen words: the checking clock starts at zero, a pack installs from the pack, the copies found are said on a line of their own, the screen of misplaced files says the copies stay, the save folder gets a line of its own, the shared note is one line, a size keeps its unit, the same save folder remedy on both screens, A Tick on the Miyoo bar, no Home key in the help of a pad, and a system name said once with a pack. --- install.ps1 | 2 +- install.py | 125 ++++++++++++++++----- install.sh | 2 +- install/launchers/index.json | 16 +-- tests/fixtures/tui/route-miyoo.txt | 12 +- tests/test_install_from.py | 2 +- tests/test_install_tui.py | 175 +++++++++++++++++++++++++++-- tests/test_save_root.py | 2 +- 8 files changed, 284 insertions(+), 52 deletions(-) diff --git a/install.ps1 b/install.ps1 index f5e2631ba..89272e0d0 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" - $defaultInstallSha256 = "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727" + $defaultInstallSha256 = "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index f9586c597..fa9ba6105 100644 --- a/install.py +++ b/install.py @@ -4220,7 +4220,7 @@ def place_save_entries( if place.by_core: where += f"{os.sep}" print(f" {_plural(len(saved), 'file')} read from {display}'s save directory " - f"go to {where}") + f"{'goes' if len(saved) == 1 else 'go'} to {where}") EVENTS.emit("saves", platform=str(manifest.get("platform", "")), files=len(saved), folder=where) return files, [] @@ -6512,7 +6512,7 @@ INSTALLED_ONLY = "installed" def _prompt_custom_selection( plat: str, files: list[dict], omitted: list[dict], installed: tuple[list[dict], list[dict]] | None = None, - name: str = "", dry_run: bool = False, + name: str = "", dry_run: bool = False, data_trees: list[dict] | None = None, ) -> tuple[list[str], list[str], list[str]] | str | None: """Offer to narrow the install by system, core and region. @@ -6521,7 +6521,15 @@ def _prompt_custom_selection( installed read) holds, or the three choices. Each empty list keeps everything on its axis. """ - print(f"\n{name or display_name(plat)}: {_sized(files)}.") + trees = data_trees or [] + data = sum(len(entry.get("files") or {}) for entry in trees) + data_size = sum(meta.get("size", 0) for entry in trees + for meta in (entry.get("files") or {}).values()) + print(f"\n{name or display_name(plat)}: {_sized(files)}" + + (f", and {_plural(data, 'data file')} ({format_size(data_size)})" if data else "") + + ".") + if _offline_only(): + print(" --offline: only the files found on this machine are installed.") # A dry run installs nothing: "install everything" was asked before it. print(" Enter) list everything, nothing is written" if dry_run else " Enter) install everything") @@ -8234,7 +8242,7 @@ def _check_copy_lines( if fixed: verb = "is" if fixed == 1 else "are" them = "it" if fixed == 1 else "them" - lines.append(f" {fixed} of these files {verb} in the BIOS folder at the wrong place: " + lines.append(f" {fixed} of these files {verb} in the BIOS folder in the wrong place: " f"a run without --check copies {them} where the cores open {them}, and " f"leaves your {'copy' if fixed == 1 else 'copies'} as " f"{'it is' if fixed == 1 else 'they are'}.") @@ -10189,6 +10197,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: selection = _prompt_custom_selection( plat_name, files, omitted_files, installed, display_name(plat_name, manifest), args.dry_run, + manifest.get("data_directories") or [], ) if selection is None: smaller = (" Nothing downloaded. To install less, run the same command " @@ -10387,9 +10396,23 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: continue if to_download: - dl_size = sum(f.get("size", 0) for f in to_download) fetched = [f for f in to_download if id(f) not in copies] + if interactive and not args.yes and _offline_only() and fetched: + # The screen offers no Install for these: the question asked + # to download 6.6 GB that --offline never fetches. + print(f"\n {_plural(len(fetched), 'file')} " + f"{'needs' if len(fetched) == 1 else 'need'} the internet, which " + "--offline does not use. Copy them from your old card or a folder " + "(--from), or run the installer again with a pack (--from-pack) or " + "without --offline.") + files_pending += len(fetched) + to_download = [f for f in to_download if id(f) in copies] + fetched = [] + dl_size = sum(f.get("size", 0) for f in to_download) fetch_size = _bytes(fetched) + if not to_download: + total_up_to_date += len(up_to_date) + continue if ( interactive and not args.yes @@ -10818,6 +10841,21 @@ def _split_word(word: str, room: int) -> list[str]: return pieces + [word] if word else pieces +_UNIT = re.compile(r"(B|KB|MB|GB)\b") + + +def _glued_words(text: str) -> list[str]: + """The words of text, a size kept with its unit: "5.1" / "KB" was + broken across two lines of a 40-column headline.""" + words: list[str] = [] + for word in text.split(): + if words and _UNIT.match(word) and re.fullmatch(r"\(?\d+(\.\d+)?", words[-1]): + words[-1] += f" {word}" + else: + words.append(word) + return words + + def _wrap(text: str, width: int, indent: str = " ") -> list[str]: """Lines of at most width cells, the ones after the first indented.""" if cell_width(text) <= width: @@ -10825,7 +10863,7 @@ def _wrap(text: str, width: int, indent: str = " ") -> list[str]: lead = text[:len(text) - len(text.lstrip(" "))] lines: list[str] = [] line = "" - for word in text.split(): + for word in _glued_words(text): for piece in _split_word(word, width - len(lead + indent)): prefix = lead + indent if lines else lead longer = f"{line} {piece}" if line else piece @@ -10884,7 +10922,9 @@ class TuiState: self.status: str = "" self.cols: int = 80 self.rows: int = 24 - self.now: float = 0.0 + # A check started before the loop's first tick counted the + # machine's uptime. + self.now: float = time.monotonic() # (platform, folder, label) for each setup found here. self.setups: list = [] self.notes: list = [] @@ -10964,6 +11004,9 @@ class TuiState: self.must_confirm: bool = False # Choices, each one an option of the command line. self.ticked: set = set() + # The ticks the systems list opened with: Back with none ticked + # brings them back, nothing having been chosen. + self.ticked_before: set | None = None self.cli_systems: list = [] self.regions: list = [] self.cli_regions: list = [] @@ -11375,6 +11418,9 @@ def tui_rows(state: TuiState) -> list[TuiRow]: if screen == "confirm": if state.running or state.dry is None: return [] + if _offline_only() and not _dry_copies(state): + # The plan's rule: Install would only fail every file. + return [TuiRow("back", "Back")] return [TuiRow("install", "Install"), TuiRow("back", "Back")] if screen == "error": rows = ([TuiRow("retry", "Try again")] @@ -11429,6 +11475,13 @@ def _plan_copies(state: TuiState, check: dict) -> list[str]: return [dest for dest in local if dest in sizes] +def _dry_copies(state: TuiState) -> list[str]: + """The files the confirm or preview run would install that this + machine holds.""" + local = {event["dest"] for event in state.found} + return [event["dest"] for event in state.dry_files if event["dest"] in local] + + def _short_of_space(state: TuiState, check: dict) -> bool: free = state.facts.get("free") return free is not None and check.get("bytes_needed", 0) > free @@ -11566,6 +11619,10 @@ def _back(state: TuiState) -> list[tuple]: state.stack.append("quit") state.focus["quit"] = 0 return effects + if state.screen == "systems" and not state.ticked and state.ticked_before: + # Enter on None, then Esc: the plan kept its headline over an + # Install the run refused. + state.ticked = set(state.ticked_before) state.stack.pop() if state.screen == "checking" and not state.running: # Stay after Esc on a check started from the command line: a @@ -11995,6 +12052,8 @@ def _activate(state: TuiState, row: TuiRow) -> list[tuple]: return _open_from_browser(state) if action in ("misplaced", "unreadable"): return _push(state, action) + if action == "systems": + state.ticked_before = set(state.ticked) if action == "systems" and len(state.systems) > TUI_LONG_LIST: # On None: picking one system of 66 began with Up, Up, A. return _push(state, action, 1) @@ -12687,6 +12746,9 @@ def _route_hint(state: TuiState, parts: tuple) -> tuple: named.append(key + sep + what) if state.pad == "muxterm" and state.screen == "path": named.append("Select Keyboard") + if state.pad == "miyoo" and state.screen in _CHECKLISTS: + tick = [entry for entry in named if entry.startswith("A ")] + named = named[:1] + tick + [entry for entry in named[1:] if entry not in tick] return tuple(named) @@ -12724,6 +12786,7 @@ _ROW_VERBS = { "jobs": "Change", "retry": "Try again", "back": "Back", "stay": "Stay", "fromcard": "Choose", "from": "Add a folder", "misplaced": "Show", "zip": "Copy from it", "unreadable": "Show", "exit": "Exit", "addapp": "Put it on the card", + "cardsystem": "Choose", "rescan": "Look again", } @@ -12955,15 +13018,18 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list: lines.append("You cannot write to this folder: choose another one.") counts = (f"{check['present']} in place, {check['missing']} missing, " f"{wrong} wrong {'copy' if wrong == 1 else 'copies'}") + lines.append(counts) if copied and fetch: # 47 to download above 49 missing and 1 wrong copy: the 3 found on - # this machine are the difference, said where the sum is read. - counts += (f": {len(copied)} of them {'is' if len(copied) == 1 else 'are'} " - f"copied from this machine, {fetch} downloaded") - lines.append(counts) - saves = (state.saves if check is state.check + # this machine are the difference, on a line of its own ("0 wrong + # copies: 2 of them" read as two of the zero). + lines.append(f"{need} to install: {len(copied)} copied from this machine, {fetch} " + + ("from the pack" if LOCAL.pack is not None else "downloaded")) + after_counts = len(lines) + saves =(state.saves if check is state.check else state.dry_saves if check is state.dry else None) - lines += [_styled(line, "path") for line in _saves_lines(state, saves)] + saves_lines = _saves_lines(state, saves) + lines += [_styled(line, "path" if index else "") for index, line in enumerate(saves_lines)] found_in = _retroarch_config_at(dest) if dest is not None else None if found_in: lines.append(_styled(f"Named by {_short_path(Path(found_in[0]), state.home)}", "path")) @@ -12980,7 +13046,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list: f"{'one' if wrong == 1 else 'ones'} kept in a backup folder.") misplaced = _tui_misplaced(state) if check is state.check else [] if misplaced: - lines.insert(3, f"{_plural(len(misplaced), 'file')} " + lines.insert(after_counts, f"{_plural(len(misplaced), 'file')} " f"{'is' if len(misplaced) == 1 else 'are'} in the wrong place, " f"{'it' if len(misplaced) == 1 else 'they'} will be copied where the " "emulators read " @@ -13003,7 +13069,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list: lines += _unavailable_lines(state) if check is state.check: lines += _unplaced_lines(state.check_summary, state, - "Choose it in Options, Save folder.") + "Choose it in Options, Save folder (--save-dir).") return lines[:2] + offline_lines + lines[2:] @@ -13015,13 +13081,16 @@ def _saves_lines(state: TuiState, saves: dict | None) -> list[str]: return [] name = display_name(state.platform) if state.platform else "the frontend" count = saves["files"] + # The folder alone is cut in the middle: cut with the sentence, it + # lost "directory goes to" even at 80 columns. return [f"{_plural(count, 'file')} read from {name}'s save directory " - f"{'goes' if count == 1 else 'go'} to " - f"{_short_path(Path(saves['folder']), state.home)}"] + f"{'goes' if count == 1 else 'go'} to:", + _short_path(Path(saves['folder']), state.home)] def _unplaced_lines(summary: dict | None, state: TuiState, - remedy: str = "Name it with --save-dir.") -> list[str]: + remedy: str = "Choose it in Options, Save folder (--save-dir), " + "next time.") -> list[str]: """The files read from the frontend's save directory that the run left out, that directory being unknown: said, never silent.""" count = sum(group.get("files", 0) for group in (summary or {}).get("skipped") or [] @@ -13244,8 +13313,7 @@ def _view_systems(state: TuiState, width: int, height: int) -> tuple: footer.insert(0, OTHER_HELP) elif "shared" in focused.right: count = _shared_needed(state)[focused.value][0] - footer.insert(0, f"{count} of these files are shared with other systems, " - "counted once in the total.") + footer.insert(0, f"+{count} shared: counted once") needle = state.filters.get("systems", "") after = [f"No system matches '{needle}'."] if len(rows) == 3 and needle else [] if state.status: @@ -13301,7 +13369,9 @@ _STATE_WORDS = {"missing": "missing", "wrong_hash": "wrong copy"} def _pager_items(state: TuiState) -> list: """The text of a screen that scrolls, the same for its view and its keys.""" if state.screen == "misplaced": - return [f"{event['dest']}: {event['reason']}" for event in _tui_misplaced(state)] + return ["They are copied where the emulators read them. Your copies stay where " + "they are.", ""] + [f"{event['dest']}: {event['reason']}" + for event in _tui_misplaced(state)] if state.screen == "unreadable": return [f"{event['path']}" + (f", {event['member']}" if event["member"] else "") + f": {event['reason']}" for event in state.unreadable] @@ -13338,7 +13408,7 @@ def _view_unreadable(state: TuiState, width: int, height: int) -> tuple: def _view_misplaced(state: TuiState, width: int, height: int) -> tuple: - return ("Files in the wrong place", _pager(state, width, height, _pager_items(state)), + return ("Files in the wrong place", _pager(state, width, height, _pager_items(state), 2), ("A/Enter Back", "Up/Down Scroll")) @@ -13451,7 +13521,8 @@ def _tui_sources(state: TuiState, installed: int, failed: int) -> str: name = _last_name(folder) folders[name] = folders.get(name, 0) + 1 copied = fixed + sum(folders.values()) - return sources_line(list(folders.items()), fixed, max(0, installed - copied), failed) + return sources_line(list(folders.items()), fixed, max(0, installed - copied), failed, + "from the pack" if LOCAL.pack is not None else "downloaded") def _unavailable(state: TuiState) -> tuple[list[dict], int]: @@ -13551,7 +13622,7 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]: ok = {dest for dest, good, _why in state.finished if good} if any(reason and dest in ok for dest, (_folder, reason) in state.local_dests.items()): # A fix copies: the misplaced files are the user's, never moved. - lines.append("Your copies at the wrong place were left as they are.") + lines.append("Your copies in the wrong place were left as they are.") lines.append(f"BIOS folder: {state.dest}") if installed + up_to_date: lines += _saves_lines(state, state.done_saves) @@ -13716,7 +13787,8 @@ def _help_lines(state: TuiState) -> list[tuple[str, str]]: keys = [(names["move"], "Move")] if len(tui_rows(state)) > TUI_LONG_LIST or screen in _PAGERS: keys += [*([(names["page"], "Page")] if names["page"] else []), - ("Home/End, g/G", "First, last")] + *([("Home/End, g/G", "First, last")] if state.pad in ("both", "keys") + else [])] primary = _primary(state) or _TUI_VIEWS[screen](state, 80, 24)[2][0] ok = names["ok"] if state.pad == "miyoo" and screen in _CHECKLISTS: @@ -13755,6 +13827,9 @@ def _primary(state: TuiState) -> str | None: verb = _ROW_VERBS.get(row.action) if state.screen == "systems" and row.action == "system" and not state.ticked: verb = "Only this system" + if state.screen == "systems" and row.action == "none" and not state.ticked: + # Nothing left to untick: A goes to the first system. + verb = "First system" return f"A/Enter {verb}" if verb else None diff --git a/install.sh b/install.sh index e13e81dda..bbf111048 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727" +DEFAULT_INSTALL_SHA256="d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 1d4f2da09..a747233fa 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -27,8 +27,8 @@ { "dest": "roms/ports/retrobios/install.py", "repo_path": "install.py", - "size": 646794, - "sha256": "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727", + "size": 650749, + "sha256": "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 699343, - "sha256": "42226d45d6a31c86b726b1af321e043f9894f9993d8c6de0f16cc84c04a2a1ed" + "size": 703298, + "sha256": "bb54c96f4ae4850f997cc925b2958b21477be655a658b6bc3551d1aef68fdbb5" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 646794, - "sha256": "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727", + "size": 650749, + "sha256": "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 646794, - "sha256": "8833256124ffdfa38865e7b065f2720ec20997e1a91ba340eab4067e31b20727", + "size": 650749, + "sha256": "d59defaa5802d2d7b73b4b02e693e9592618803b308eb625a6f436b8a5388464", "executable": false }, { diff --git a/tests/fixtures/tui/route-miyoo.txt b/tests/fixtures/tui/route-miyoo.txt index c47dc92a2..10ea786ff 100644 --- a/tests/fixtures/tui/route-miyoo.txt +++ b/tests/fixtures/tui/route-miyoo.txt @@ -550,7 +550,7 @@ 2 of 3 systems: 4 files, 1.0 MB - Start Continue R2 Back A Tick + Start Continue A Tick R2 Back === systems 40x15 RetroBIOS: Choose systems @@ -566,7 +566,7 @@ 2 of 3 systems: 4 files, 1.0 MB - Start Continue R2 Back A Tick + Start Continue A Tick R2 Back === systems 80x24 RetroBIOS: Choose systems @@ -591,7 +591,7 @@ 2 of 3 systems: 4 files, 1.0 MB - Start Continue R2 Back A Tick + Start Continue A Tick R2 Back === options 40x12 RetroBIOS: Options @@ -712,7 +712,7 @@ - Start Filter R2 Back A Tick + Start Filter A Tick R2 Back === cores 40x15 RetroBIOS: Cores @@ -728,7 +728,7 @@ - Start Filter R2 Back A Tick + Start Filter A Tick R2 Back === cores 80x24 RetroBIOS: Cores @@ -753,7 +753,7 @@ - Start Filter R2 Back A Tick + Start Filter A Tick R2 Back === preview 40x12 RetroBIOS: Preview diff --git a/tests/test_install_from.py b/tests/test_install_from.py index ef445ca50..d6bd96476 100644 --- a/tests/test_install_from.py +++ b/tests/test_install_from.py @@ -438,7 +438,7 @@ class MisplacedInTheBiosFolder(Scratch): for name, data in PAYLOAD.items(): self.place(f"system/{name}", data) out = self.check() - self.assertIn("4 of these files are in the BIOS folder at the wrong place: a run " + self.assertIn("4 of these files are in the BIOS folder in the wrong place: a run " "without --check copies them where the cores open them, and leaves " "your copies as they are.", out) self.assertFalse((self.dest / "scph5501.bin").exists()) diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index c2c1ad264..e885eb234 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -2044,7 +2044,7 @@ class CopyFromAnOldCard(unittest.TestCase): state.finished = [("scph5500.bin", True, ""), ("gba_bios.bin", True, "")] text = " ".join(" ".join(install.tui_outcome(state)[1]).split()) self.assertIn("1 file fixed from copies already in the BIOS folder, 1 downloaded. " - "Your copies at the wrong place were left as they are.", text) + "Your copies in the wrong place were left as they are.", text) def test_a_local_copy_is_no_download_speed(self): state = checked_state() @@ -2432,7 +2432,7 @@ class AuditRoundThree(unittest.TestCase): self.assertEqual(rows[three].right, "1 file, 100 B + 2 shared") state.focus["systems"] = three text = " ".join(" ".join(install.tui_view(state, 80, 24)).split()) - self.assertIn("2 of these files are shared with other systems, counted once", text) + self.assertIn("+2 shared: counted once", text) # The line mode counts the same files apart. self.assertIn("and 2 files, 500 B shared with other systems", install._system_sized([ {"dest": "panafz1.bin", "size": 100, "systems": ["3do"]}, @@ -2767,11 +2767,12 @@ class AuditRoundFour(unittest.TestCase): plan = " ".join(" ".join(install.tui_view(state, 120, 40)).split()) # The plan names the row that sets it, the result the option a # command carries. - self.assertIn(line + "Choose it in Options, Save folder.", plan) + self.assertIn(line + "Choose it in Options, Save folder (--save-dir).", plan) state.stack.append("result") state.code = 0 state.summary = {**summary, "mode": "install", "installed": 5} - self.assertIn(line + "Name it with --save-dir.", install.tui_outcome(state)[1]) + self.assertIn(line + "Choose it in Options, Save folder (--save-dir), next time.", + install.tui_outcome(state)[1]) class TheSaveFolderHasARow(unittest.TestCase): @@ -3441,6 +3442,162 @@ class OfflineInTheInterface(unittest.TestCase): self.assertIn("--offline", install.tui_command(state)) +class AuditFive(unittest.TestCase): + """tui_audit_5 P2-1, P2-2 and its P3 points.""" + + def long_list(self) -> install.TuiState: + state = OneSystemInFewPresses().long_list("keys") + state.cols, state.rows = 80, 24 + return state + + def test_back_from_none_keeps_the_ticks_it_opened_with(self): + 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") + self.assertEqual(state.ticked, set()) + press(state, "up", "up") + self.assertEqual(install._primary(state), "A/Enter First system") + press(state, "esc") + self.assertEqual(state.screen, "plan") + self.assertEqual(state.ticked, everything) + text = " ".join(install.tui_view(state, 80, 24)) + self.assertNotIn("No system is ticked", text) + + def test_back_after_a_choice_keeps_the_choice(self): + state = self.long_list() + press(state, "down", "enter", "enter") + press(state, " ") + chosen = set(state.ticked) + press(state, "esc") + self.assertEqual((state.screen, state.ticked), ("plan", chosen)) + + def test_confirm_offline_with_nothing_on_hand_has_no_install(self): + state = checked_state() + state.stack.append("confirm") + state.dry = dict(CHECK) + state.dry_files = [dict(event, state="missing") for event in FILES] + with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}): + rows = [row.action for row in install.tui_rows(state)] + text = " ".join(" ".join(install.tui_view(state, 120, 40)).split()) + self.assertEqual(rows, ["back"]) + self.assertIn("Copy them from your old card or a folder", text) + state.found = [{"event": "found", "platform": "retroarch", + "dest": FILES[0]["dest"], "folder": "/media/old", + "path": "/media/old/x", "member": "", "reason": ""}] + self.assertEqual(install.tui_rows(state)[0].action, "install") + + def test_the_checking_clock_starts_at_zero(self): + state = install.TuiState() + self.assertLess(abs(state.now - time.monotonic()), 5) + + def test_a_pack_installs_from_the_pack(self): + state = checked_state() + state.found = [{"event": "found", "platform": "retroarch", "dest": FILES[0]["dest"], + "folder": "/media/old", "path": "/media/old/x", "member": "", + "reason": "misplaced"}] + with mock.patch.object(install.LOCAL, "pack", mock.Mock()): + text = " ".join(" ".join(install.tui_view(state, 120, 40)).split()) + self.assertIn("1 copied from this machine,", text) + self.assertIn("from the pack", text) + self.assertNotIn("downloaded", text) + self.assertNotIn("of them", text) + state.finished = [(FILES[0]["dest"], True, ""), (FILES[1]["dest"], True, "")] + state.local_dests = {FILES[0]["dest"]: ("/media/old", "misplaced")} + self.assertIn("1 from the pack", install._tui_sources(state, 2, 0)) + + def test_the_wrong_place_screen_says_the_copies_stay(self): + state = checked_state() + state.stack.append("misplaced") + self.assertEqual(install._pager_items(state)[0], + "They are copied where the emulators read them. Your copies stay " + "where they are.") + + def test_the_save_folder_is_a_line_of_its_own(self): + state = checked_state() + saves = {"files": 1, "folder": str(HOME / ".config" / "retroarch" / "saves" / "")} + lines = install._saves_lines(state, saves) + self.assertEqual(lines, ["1 file read from RetroArch's save directory goes to:", + "~/.config/retroarch/saves/"]) + + def test_the_shared_note_is_one_line(self): + state = self.long_list() + state.cols, state.rows = 40, 12 + state.files = [{"event": "file", "dest": "mame.zip", "size": 10, + "systems": ["sys-00", "sys-01"]}] + state.stack.append("systems") + state.focus["systems"] = 3 + lines = install.tui_view(state, 40, 12) + self.assertIn(" +1 shared: counted once", lines) + + def test_the_save_folder_remedy_is_the_same_on_both_screens(self): + summary = {"skipped": [{"reason": "no_save_directory", "files": 1}]} + state = checked_state() + lines = install._unplaced_lines(summary, state) + self.assertIn("Options, Save folder (--save-dir)", lines[0]) + + def test_a_size_keeps_its_unit(self): + for width in range(20, 45): + for line in install._wrap("2 files to install from the pack, 5.1 KB", width): + self.assertNotEqual(line.strip(), "KB") + + def test_the_miyoo_bar_names_the_tick(self): + state = self.long_list() + state.pad = "miyoo" + press(state, "down", "enter") + state.ticked = set() + state.focus["systems"] = 3 + bar = install.tui_view(state, 40, 15)[-1] + self.assertIn("A Tick", bar) + self.assertTrue(bar.strip().startswith("Start Only this system")) + + def test_muxterm_help_names_no_home_key(self): + state = self.long_list() + state.pad = "muxterm" + press(state, "down", "enter") + keys = [key for key, _what in install._help_lines(state)] + self.assertNotIn("Home/End, g/G", keys) + state.pad = "keys" + self.assertIn("Home/End, g/G", [key for key, _what in install._help_lines(state)]) + + +class LineModeOffline(unittest.TestCase): + """tui_audit_5 P2-3 and P3-8.4: the line mode under --offline asked to + download, and said nothing of the data files.""" + + def test_nothing_is_offered_that_cannot_come(self): + with FakeRepository() as repo: + env = {**repo.env(), "TERM": "xterm", install.OFFLINE_VARIABLE: "1"} + term = Pty(["--plain", "--manifest", str(repo.root / "install" / "retroarch.json"), + "--dest", str(repo.dest), "--offline"], env) + term.wait("q) quit") + term.send("\r") + term.close() + out = term.raw.decode("utf-8", "replace") + self.assertIn("--offline: only the files found on this machine are installed.", out) + self.assertIn("5 files need the internet, which --offline does not use.", out) + self.assertNotIn("Downloading", out) + self.assertNotIn("failed", out.lower().replace("not failed", "")) + + def test_the_data_files_are_counted_before_the_question(self): + files = [{"dest": "a.bin", "size": 1024}] + trees = [{"ref": "t", "files": {"x": {"size": 2048}, "y": {"size": 2048}}}] + with mock.patch.object(install, "_ask", return_value=""), \ + contextlib.redirect_stdout(io.StringIO()) as out: + install._prompt_custom_selection("retroarch", files, [], data_trees=trees) + self.assertIn("RetroArch: 1 file, 1.0 KB, and 2 data files (4.0 KB).", out.getvalue()) + + def test_one_save_file_goes(self): + files = [{"dest": "keys.txt", "root": "save", "size": 1}] + place = install.SavePlace(Path("/saves"), False) + with mock.patch.object(install, "save_directory_for", return_value=place), \ + contextlib.redirect_stdout(io.StringIO()) as out: + install.place_save_entries(files, Path("/bios"), {}, None, "RetroArch") + self.assertIn("1 file read from RetroArch's save directory goes to /saves", + out.getvalue()) + + class NarrowSystemNames(unittest.TestCase): """At 40 columns the size column took half the line: Game Boy, Game Boy Advance and Game Boy Color all read "Nintendo Gam".""" @@ -3554,7 +3711,7 @@ class TheSaveFolderLineFollowsTheChoice(unittest.TestCase): SAVES = {"event": "saves", "platform": "retroarch", "files": 1, "folder": "/home/user/MySaves/"} - LINE = "1 file read from RetroArch's save directory goes to ~/MySaves/" + LINE = "1 file read from RetroArch's save directory goes to: ~/MySaves/" def said(self, lines) -> str: return " ".join(" ".join(lines).split()) @@ -4356,11 +4513,11 @@ class EndToEnd(unittest.TestCase): self.assertIn("6 files to download", screen) # The second folder the run writes to, named before # anything is written, and again on the result. - self.assertIn("1 file read from RetroArch's save directory goes to " - "~/MySaves", screen) + self.assertIn("1 file read from RetroArch's save directory goes to:\n" + " ~/MySaves", screen) term.send("\r") screen = term.wait("6 files installed.") - self.assertIn("save directory goes to ~/MySaves", screen) + self.assertIn("save directory goes to:\n ~/MySaves", screen) term.send("\r") self.assertEqual(term.close(), 0) results.append(written(repo, saves)) @@ -4741,7 +4898,7 @@ class ThePlanNumbersAddUp(unittest.TestCase): state.found = [found("scph5500.bin", str(SYSTEM_DIR), "one folder too deep")] screen = " ".join(" ".join(install.tui_view(state, 120, 40)).split()) self.assertIn("3 files to download", screen) - self.assertIn("1 in place, 3 missing, 1 wrong copy: 1 of them is copied from this " + self.assertIn("1 in place, 3 missing, 1 wrong copy 4 to install: 1 copied from this " "machine, 3 downloaded", screen) diff --git a/tests/test_save_root.py b/tests/test_save_root.py index e3cc58f81..0b1ae378a 100644 --- a/tests/test_save_root.py +++ b/tests/test_save_root.py @@ -736,7 +736,7 @@ class TheInstallerWritesThem(unittest.TestCase): b"keys below the save directory", ) self.assertFalse((app / "system" / "Azahar").exists()) - self.assertIn("read from RetroArch's save directory go to", proc.stdout) + self.assertIn("read from RetroArch's save directory goes to", proc.stdout) def test_where_save_dir_points(self): repo = _Repository(self, self.FILES)