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)