fix: one remedy per card and plainer line mode

A full or read-only card is freed or unlocked, checked again first,
never left for another drive. Line mode names --dest where it said
choose another one, numbers out of range, the folder copied from, the
frontend's save directory and the command after Enter. The words name
RETROBIOS_INSTALL_OFFLINE when it, not --offline, made a run offline.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 16:13:10 +01:00
1 parent 0fb1047231
commit 302c7737b2
6 files changed
+290 -57

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "af62c61703cf1becc150b0244b6e13c731e062e9471a07c184693bb9cd46ece3"
$defaultInstallSha256 = "c030192b6ecfcedfc114ecf0cbcfc509fcca44e398e6d8cefedc7766065df538"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+133 -41
View File
@@ -3691,9 +3691,9 @@ def fetch_manifest(plat: str) -> dict:
message=words if code != "network"
else f"the file list could not be fetched: {words}")
if code == "offline":
print(f" No file list for {plat}: {OFFLINE_WORDS}. Give it with "
print(f" No file list for {plat}: {offline_words()}. Give it with "
"--manifest FILE (a copy of install/<platform>.json) or "
"--from-pack PACK, or run without --offline.", file=sys.stderr)
f"--from-pack PACK, or run {offline_undo()}.", file=sys.stderr)
sys.exit(1)
if code != "network":
print(f" Could not fetch the file list for {plat}.", file=sys.stderr)
@@ -4990,7 +4990,7 @@ def _download_one(
if LOCAL.pack is not None:
return _extract_from_pack(f, bios_path, dest, progress)
if offline_requested():
return f["dest"], False, OfflineReason(f"not fetched: {OFFLINE_WORDS}")
return f["dest"], False, OfflineReason(f"not fetched: {offline_words()}")
if f.get("release_asset"):
url = RELEASE_URL.format(asset=urllib.parse.quote(f["release_asset"], safe=""))
else:
@@ -5634,6 +5634,7 @@ def _report_outcome(
sources: str = "",
offline: int = 0,
kinds: list[int] | None = None,
same_choice: str = "",
) -> None:
"""Close the run on what happened, and leave with a status that says so.
kinds is [BIOS files, game data files] installed, said apart.
@@ -5720,8 +5721,8 @@ def _report_outcome(
remedy = _offline_remedy()
if not downloaded and not errors:
print(f"\nNothing installed: {_plural(offline, 'file')} "
f"{'needs' if offline == 1 else 'need'} the internet, which --offline "
"does not use.")
f"{'needs' if offline == 1 else 'need'} the internet, which "
f"{offline_named()} does not use.")
print(f" {remedy}")
for line in kept:
print(line)
@@ -5732,12 +5733,12 @@ def _report_outcome(
failed = f", {errors} failed" if errors else ""
if downloaded:
print(f"\nFinished with files missing. {_plural(downloaded, 'file')} installed, "
f"{offline} not fetched (--offline){failed}.")
f"{offline} not fetched ({offline_named()}){failed}.")
else:
# Nothing written: "Finished with files missing. 0 files installed"
# read as a part done.
print(f"\nNothing installed: {_plural(offline, 'file')} not fetched "
f"(--offline){failed}.")
f"({offline_named()}){failed}.")
if sources:
print(f" {sources}")
# Under the files it is about: after the save-directory line, "They"
@@ -5792,6 +5793,12 @@ def _report_outcome(
print(line)
for line in hints or []:
print(line)
if same_choice:
# After the questions, as the screen's result ends: "the same
# command" was one that asked them (tui_audit_11 P3-21).
print(f"Same choice without questions: {screen_text(same_choice)}")
print("To check this later, run that command with --check.")
return
print("To check this later, run the same command with --check.")
@@ -5805,9 +5812,9 @@ def _offline_remedy(folders: list[str] | None = None) -> str:
if labels:
named = ", ".join(screen_text(shown_path(label)) for label in labels)
return (f"They are not in {named}: run the installer again with a pack "
"(--from-pack) or without --offline.")
f"(--from-pack) or {offline_undo()}.")
return ("Copy them from your old card or a folder (--from), or run the installer "
"again with a pack (--from-pack) or without --offline.")
f"again with a pack (--from-pack) or {offline_undo()}.")
def _check_fetch_line(fetch: int, beside: str) -> str:
@@ -5817,7 +5824,7 @@ def _check_fetch_line(fetch: int, beside: str) -> str:
if not fetch:
return " Run the same command without --check to copy them into place."
return (f" {_plural(fetch, 'file')} {'needs' if fetch == 1 else 'need'} the "
f"internet, which --offline does not use. {_offline_remedy()}")
f"internet, which {offline_named()} does not use. {_offline_remedy()}")
return (" Run the same command without --check to fetch them. A file with a wrong "
f"hash is replaced, and the one in place kept in {BACKUP_DIR} beside {beside}.")
@@ -5983,8 +5990,8 @@ def _print_plan(
# internet".
one = len(to_download) + tree_files == 1
print(f"\n{' and '.join(parts)} {'needs' if one else 'need'} the internet, which "
"--offline does not use. Without --offline they would go to "
f"{shown_path(bios_path)}:")
f"{offline_named()} does not use. Run {offline_undo()}, and they would go "
f"to {shown_path(bios_path)}:")
elif parts:
source = " from the pack" if LOCAL.pack is not None else ""
print(f"\nWould {verb} {' and '.join(parts)}{source} to {shown_path(bios_path)}:")
@@ -6033,7 +6040,8 @@ def _print_plan(
print("\n Nothing was written. Run without --dry-run to install.")
def _check_free_space(bios_path: Path, needed: int, plat: str = "", code: int = 2) -> None:
def _check_free_space(bios_path: Path, needed: int, plat: str = "", code: int = 2,
card: bool = False) -> None:
"""Stop before writing when the volume cannot hold the download.
Reported against the nearest existing ancestor, since the destination
@@ -6053,6 +6061,12 @@ def _check_free_space(bios_path: Path, needed: int, plat: str = "", code: int =
print(f"\nNot enough space on the drive holding {bios_path}.", file=sys.stderr)
print(f" Needed: {format_size(needed)}. Free: {format_size(free)}.",
file=sys.stderr)
if card:
# Its system reads this card alone: another drive, with RetroArch's
# list, was offered for an Onion card (code_audit_18 P3-2).
print(f" Free up {format_size(needed - free)} on the card, then run it again: "
"its system reads its BIOS files there only.", file=sys.stderr)
sys.exit(code)
if not plat:
print(f" Free up {format_size(needed - free)}, or use --dest to install "
"to another drive.", file=sys.stderr)
@@ -7065,9 +7079,10 @@ def _prompt_platform_choice(
if other is not None:
print(" o) another setup or folder")
print(" q) quit")
usage = (f" Type a number from 1 to {len(platforms)}"
+ (", o for another setup or folder" if other is not None else "")
+ ", or q to quit.")
# "a number from 1 to 1" for a single row (code_audit_18 P3-5).
usage = (" Type 1" if len(platforms) == 1 else
f" Type a number from 1 to {len(platforms)}") + (
", o for another setup or folder" if other is not None else "") + ", or q to quit."
while True:
choice = _ask(f"> [Enter for {named[0]}] ").lower()
if not choice:
@@ -7243,14 +7258,28 @@ def folder_warnings(plat: str, bios_path: Path, files: list[dict], hint: Invocat
a setup that runs from that card, whose own folder is the one it reads."""
lines = []
if not _folder_writable(bios_path):
lines.append("You cannot write to this folder: choose another one.")
lines += write_remedy(bios_path, plat, hint, commands)
lines += cannot_write_words(bios_path, plat, hint, commands, card)
short = _space_short(bios_path, files)
if short is not None:
lines.append(space_words(short[0], short[1], card))
return lines
def cannot_write_words(bios_path: Path, plat: str, hint: Invocation, commands: bool,
card: bool, screen: bool = False, remedy: bool = True) -> list[str]:
"""What the plan says of a folder this user cannot write, one remedy
for a card: unlock it, never another folder its system does not read.
Line mode has no folder row: "choose another one" there offered no way
to (code_audit_18 P3-3), so it names --dest, the screen its row."""
said = write_remedy(bios_path, plat, hint, commands) if remedy else []
if card and _on_read_only_mount(bios_path):
return said
another = ("choose another one" if screen
else "run it again with another --dest" if commands
else "run it again and choose another one")
return [f"You cannot write to this folder: {another}."] + said
def space_words(needed: int, free: int, card: bool) -> str:
"""Not enough room, and what to do: on a card setup's own card, room is
made on the card, another folder being one its system does not read."""
@@ -7364,7 +7393,7 @@ def _prompt_custom_selection(
if offline:
# Said before the menu: "install everything" was offered, then
# nothing came and the run said Done.
print(" --offline: only the files found on this machine are installed"
print(f" {offline_named()}: only the files found on this machine are installed"
+ ("." if LOCAL.folders else ", and no --from names a folder to copy "
"them from."))
# A dry run installs nothing: "install everything" was asked before it.
@@ -7469,7 +7498,9 @@ def _print_menu_head(
for line in folder_warnings(plat, bios_path, shown, _INVOCATION, True,
on_its_card(plat, bios_path)):
print(f" {line}")
for line in save_lines(shown, kept_omitted, bios_path, name or display_name(plat)):
# The frontend's save directory, named as the end of the run and the
# screen name it: "Onion's" before the menu, "RetroArch's" after.
for line in save_lines(shown, kept_omitted, bios_path, display_name(plat, manifest)):
print(f" {line}")
@@ -8206,7 +8237,30 @@ class _Offline(OSError):
# full-screen interface starts inherits. RETROBIOS_OFFLINE stays the build
# scripts' own: CI and the test suite set it for every process they start.
OFFLINE_VARIABLE = "RETROBIOS_INSTALL_OFFLINE"
OFFLINE_WORDS = "this run makes no network request (--offline)"
OFFLINE_WORDS = "this run makes no network request"
# The value --offline gives the variable: the words then name the option,
# and the variable itself where it was set (tui_audit_11 P3-12).
OFFLINE_FLAG_VALUE = "--offline"
def offline_named() -> str:
"""What made this run offline, as the words name it."""
value = os.environ.get(OFFLINE_VARIABLE, "")
return (f"{OFFLINE_VARIABLE}={value}" if value not in ("", "0", OFFLINE_FLAG_VALUE)
else "--offline")
def offline_undo() -> str:
"""How a run gets the network back: without the option, or with the
variable unset. "without --offline" told a user who never typed it to
remove it."""
return ("without --offline" if offline_named() == "--offline"
else f"with {OFFLINE_VARIABLE} unset")
def offline_words() -> str:
"""This run makes no network request, and what said so."""
return f"{OFFLINE_WORDS} ({offline_named()})"
def offline_requested() -> bool:
@@ -8217,7 +8271,7 @@ def _network_allowed(what: str) -> None:
if LOCAL.pack is not None:
raise _Offline(f"{what}: this run reads a pack and makes no network request")
if offline_requested():
raise _Offline(f"{what}: {OFFLINE_WORDS}")
raise _Offline(f"{what}: {offline_words()}")
def read_local_manifest(path: Path, plat: str) -> dict:
@@ -10220,12 +10274,13 @@ def main() -> None:
):
if given:
parser.error(reason)
if args.offline:
# The runs the full-screen interface starts read it from here.
os.environ[OFFLINE_VARIABLE] = "1"
if args.offline and not offline_requested():
# The runs the full-screen interface starts read it from here, and
# the words name the option, not a variable nobody set.
os.environ[OFFLINE_VARIABLE] = OFFLINE_FLAG_VALUE
if offline_requested() and (args.target or args.list_targets):
parser.error("--target and --list-targets read a core list from the network, "
"which --offline does not use")
f"which {offline_named()} does not use")
install_bounded_opener()
if args.from_pack and args.target:
parser.error("--target reads a core list from the network, which --from-pack "
@@ -10427,6 +10482,12 @@ def _ask_from_folder(files: list[dict] | None = None, bios_path: Path | None = N
_quit()
if raw.isdigit() and 1 <= int(raw) <= len(drives):
label = str(drives[int(raw) - 1])
elif raw.isdigit() and drives and not _is_dir(_typed_folder(raw)):
# A number out of range, said as one: "2" was read as the folder
# ./2 (tui_audit_11 P3-10).
count = "1" if len(drives) == 1 else f"a number from 1 to {len(drives)}"
print(f" Type {count}, a folder, or Enter.")
continue
else:
label = os.path.normpath(str(_typed_folder(raw).absolute()))
refused = copy_source_refusal(Path(label), bios_path)
@@ -12191,7 +12252,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
rest = (f" and {'copying' if LOCAL.pack is not None else 'downloading'} "
f"{len(fetched)}" if fetched else "")
where = f" from {LOCAL.pack.name}" if fetched and LOCAL.pack is not None else ""
print(f"\nCopying {_plural(len(copies), 'file')} found on this machine"
# Named by the folder they come from, as the line above
# names it: "found on this machine" followed "from E:\".
sources = list(dict.fromkeys(copy.index.label for copy in copies.values()))
named = ", ".join(screen_text(shown_path(label)) for label in sources)
origin = (f"copied from {named}" if all(copy.index is not bios_index for copy in copies.values())
else "found on this machine")
print(f"\nCopying {_plural(len(copies), 'file')} {origin}"
f"{rest}{where} ({format_size(dl_size)})...")
elif LOCAL.pack is not None:
print(f"\nCopying {len(to_download)} files ({format_size(dl_size)}) "
@@ -12202,7 +12269,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
"carries on from here.")
# Checked before the folder is created: a run stopped for space
# left an empty BIOS folder behind.
_check_free_space(bios_path, dl_size, plat_name, 1 if total_downloaded else 2)
_check_free_space(bios_path, dl_size, plat_name, 1 if total_downloaded else 2,
on_its_card(plat_name, bios_path))
created = not bios_path.exists()
try:
bios_path.mkdir(parents=True, exist_ok=True)
@@ -12270,7 +12338,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# Left out as the BIOS files above are: counted missing,
# not as one failure per tree.
print(f" {entry['ref']}: {counts['present']} already up to date, "
f"{counts['absent']} need the internet (--offline)")
f"{counts['absent']} need the internet ({offline_named()})")
total_offline += counts["absent"]
files_pending += counts["absent"]
continue
@@ -12357,6 +12425,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
platform_of={path: plat for plat, path in platforms},
hints=bulk_lines,
kinds=kinds_done,
same_choice=(_INVOCATION.command(*platforms[0])
if interactive and len(platforms) == 1 else ""),
sources=_run_sources(bios_indexes, total_downloaded,
total_errors + total_offline),
offline=total_offline,
@@ -13455,20 +13525,35 @@ def _plan_rows(state: TuiState) -> list[TuiRow]:
# Nothing here to copy, and the network refused: Install would
# only fail every file, in this folder or another.
rows = []
if state.facts.get("writable") is False or _short_of_space(state, check):
if _on_its_card(state) and (state.facts.get("writable") is False
or _short_of_space(state, check)):
# Its system reads this card alone: room is made on it, or it is
# unlocked, then counted again, first in focus. Install anyway on
# a mount nobody can write would only fail (tui_audit_11 P3-1).
locked = state.facts.get("writable") is False
rows = [TuiRow("recheck", "Check again"), TuiRow("folder", "Choose another folder"),
*([TuiRow("install", "Install anyway")] if rows and not locked else [])]
elif state.facts.get("writable") is False or _short_of_space(state, check):
# Every file would fail alike, or the run would stop before the
# first one: the way out is another folder.
rows = [TuiRow("folder", "Choose another folder"),
*([TuiRow("install", "Install anyway")] if rows else [])]
# --system given: the choice stays offered, the list widened when it is
# opened. With one system named it was neither shown nor changeable.
copy = TuiRow("fromcard", "Copy the BIOS files from your old card or a folder")
if not rows:
# Nothing can be installed: the remedy the plan names comes first,
# in focus. Choose systems was, whose systems need the internet
# too (tui_audit_11 P3-8).
rows.append(copy)
if "systems" in recorded_axes((state.manifest or {}).get("files", [])) and (
len(state.systems) > 1 or state.cli_systems):
rows.append(TuiRow("systems", "Choose systems"))
game_data = _game_data_row(state)
if game_data is not None:
rows.append(game_data)
rows.append(TuiRow("fromcard", "Copy the BIOS files from your old card or a folder"))
if copy not in rows:
rows.append(copy)
if _tui_misplaced(state):
rows.append(TuiRow("misplaced", "Show the files in the wrong place"))
if state.unreadable:
@@ -14327,6 +14412,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 == "recheck":
return _start_check(state)
if action == "gamedata":
# One press, then counted again, as Back from Options counts.
state.no_game_data = not state.no_game_data
@@ -15452,6 +15539,7 @@ _ROW_VERBS = {
"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", "elsewhere": "Choose", "drive": "Choose",
"recheck": "Check again", "gamedata": "Change", "share": "Choose",
}
@@ -15686,7 +15774,8 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
# folder off a 40x15 screen. The Options row spells it out.
where = ", ".join(f"{len(events)} found in {_last_name(folder)}"
for folder, events in elsewhere.items())
headline = (f"{where}, {fetch} need the internet (--offline)" if fetch and offline
headline = (f"{where}, {fetch} need the internet ({offline_named()})"
if fetch and offline
else f"{where}, {fetch} to download ({format_size(fetch_bytes)})"
if fetch else f"{where}, nothing to download")
elif need and fetch and offline:
@@ -15713,8 +15802,8 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
# they read as if they were on hand.
both = fetch + data > 1
headline += ((f" and {_plural(data, 'data file')}{data_size}" if data else "")
+ f" {'need' if both else 'needs'} the internet, which --offline does "
"not use")
+ f" {'need' if both else 'needs'} the internet, which "
f"{offline_named()} does not use")
elif need:
if data:
headline += f", and {_plural(data, 'data file')}{data_size}"
@@ -15732,7 +15821,10 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
_styled(f"BIOS folder: {_short_path(_plan_folder(state, dest), state.home)}", "path"),
]
if state.facts.get("writable") is False:
lines.append("You cannot write to this folder: choose another one.")
# The remedy itself comes below, after the counts.
lines += cannot_write_words(dest, state.platform, tui_hint(state),
state.pad not in TUI_PAD_ROUTES, _on_its_card(state),
screen=True, remedy=False)
# Said again where it is the BIOS folder: the browser said it, and the
# plan named ~ alone.
lines += filter(None, [_no_setup_reads(state, dest)])
@@ -15801,7 +15893,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
# by "They are not in USBSTICK".
offline_lines = [f"{', '.join(empty)} {'holds' if len(empty) == 1 else 'hold'} "
"none of these files: run the installer again with a pack "
"(--from-pack) or without --offline."]
f"(--from-pack) or {offline_undo()}."]
else:
offline_lines = [f"{name} holds none of these files." for name in empty] \
+ offline_lines
@@ -16465,11 +16557,11 @@ def tui_outcome(state: TuiState) -> tuple[str, list[str]]:
if total else "Stopped. Run again to carry on: what is done stays done.")
elif left and installed:
title = "Finished with files missing"
head = f"{installed} installed, {left} not fetched (--offline){errors}."
head = f"{installed} installed, {left} not fetched ({offline_named()}){errors}."
elif left:
title = "Nothing installed"
head = (f"Nothing was installed: {_plural(left, 'file')} not fetched "
f"(--offline){errors}.")
f"({offline_named()}){errors}.")
elif failed and installed:
title = "Finished with errors"
head = f"{installed} installed, {len(failed)} failed."
@@ -17123,11 +17215,11 @@ def _tui_fetch(plat: str, inbox: queue.Queue, log: io.StringIO) -> None:
# Asked for, not a failure: no Try again, and the two ways a
# run without the network gets its list.
inbox.put(("manifest_failed", plat, TUI_OFFLINE_TITLE, [
"This run makes no network request (--offline), and the list of "
"BIOS files comes from the network.",
f"This run makes no network request ({offline_named()}), and the list "
"of BIOS files comes from the network.",
"Run the installer again with the list saved as a file "
"(--manifest FILE, a copy of install/<platform>.json) or with a pack "
"(--from-pack PACK), or without --offline."], 1))
f"(--from-pack PACK), or {offline_undo()}."], 1))
return
title = {"clock": "The clock is wrong",
"no_ca": "No trusted certificates"}.get(code, "No connection")
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="af62c61703cf1becc150b0244b6e13c731e062e9471a07c184693bb9cd46ece3"
DEFAULT_INSTALL_SHA256="c030192b6ecfcedfc114ecf0cbcfc509fcca44e398e6d8cefedc7766065df538"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 808521,
"sha256": "af62c61703cf1becc150b0244b6e13c731e062e9471a07c184693bb9cd46ece3",
"size": 813666,
"sha256": "c030192b6ecfcedfc114ecf0cbcfc509fcca44e398e6d8cefedc7766065df538",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 861070,
"sha256": "b06cd724e1a4d41a6bd5ae7da6049402d374d3265cd816594c89dbb7e1593ee3"
"size": 866215,
"sha256": "cfe8dc0a1cda5a63d810ebcc8f28e1f1797446d5ef72447d0a64b3f68639a258"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 808521,
"sha256": "af62c61703cf1becc150b0244b6e13c731e062e9471a07c184693bb9cd46ece3",
"size": 813666,
"sha256": "c030192b6ecfcedfc114ecf0cbcfc509fcca44e398e6d8cefedc7766065df538",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 808521,
"sha256": "af62c61703cf1becc150b0244b6e13c731e062e9471a07c184693bb9cd46ece3",
"size": 813666,
"sha256": "c030192b6ecfcedfc114ecf0cbcfc509fcca44e398e6d8cefedc7766065df538",
"executable": false
},
{
+3 -3
View File
@@ -828,8 +828,8 @@ class OfflineLeavesFilesMissing(Scratch):
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertNotIn("Would download", proc.stdout)
self.assertIn("4 files (", proc.stdout)
self.assertIn("need the internet, which --offline does not use. Without --offline "
"they would go to", proc.stdout)
self.assertIn("need the internet, which --offline does not use. Run without "
"--offline, and they would go to", proc.stdout)
def test_an_archive_not_read_is_only_its_error(self):
listed = manifest()
@@ -967,7 +967,7 @@ class AuditFifteenOffline(Scratch):
# The screen reads that summary as missing files, not failures.
state = install.TuiState()
state.summary = summary
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: install.OFFLINE_FLAG_VALUE}):
title, lines = install.tui_outcome(state)
self.assertEqual(title, "Finished with files missing")
self.assertIn("1 installed, 3 not fetched (--offline).", lines[0])
+144 -3
View File
@@ -3468,10 +3468,26 @@ class OfflineInTheInterface(unittest.TestCase):
forbids."""
def setUp(self):
patcher = mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"})
# The value --offline gives the variable.
patcher = mock.patch.dict(os.environ,
{install.OFFLINE_VARIABLE: install.OFFLINE_FLAG_VALUE})
patcher.start()
self.addCleanup(patcher.stop)
def test_the_variable_is_named_when_it_made_the_run_offline(self):
"""tui_audit_11 P3-12: "(--offline)" and "without --offline" with
RETROBIOS_INSTALL_OFFLINE=1 and no --offline: nothing to remove."""
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
inbox = queue.Queue()
install._tui_fetch("retroarch", inbox, io.StringIO())
text = " ".join(inbox.get_nowait()[3])
self.assertIn("(RETROBIOS_INSTALL_OFFLINE=1)", text)
self.assertIn("with RETROBIOS_INSTALL_OFFLINE unset", text)
self.assertNotIn("--offline", text)
plan = " ".join(" ".join(install.tui_view(checked_state(), 120, 40)).split())
self.assertIn("which RETROBIOS_INSTALL_OFFLINE=1 does not use", plan)
self.assertNotIn("without --offline", plan)
def test_no_list_names_the_ways_that_work(self):
inbox = queue.Queue()
install._tui_fetch("retroarch", inbox, io.StringIO())
@@ -5962,7 +5978,7 @@ class AuditSix(unittest.TestCase):
state.found = [{"event": "found", "platform": "retroarch", "dest": FILES[0]["dest"],
"folder": "/media/old", "path": "/media/old/x", "member": "",
"reason": ""}]
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: install.OFFLINE_FLAG_VALUE}):
text = " ".join(" ".join(install.tui_view(state, 120, 40)).split())
self.assertNotIn("downloaded", text)
state.found = []
@@ -6919,6 +6935,82 @@ class LineModeAndScreenDecideAlike(unittest.TestCase):
self.assertIn("Done. 2 files installed", real.getvalue())
self.assertIn("Installed: 1 BIOS file and 1 game data file.", real.getvalue())
# code_audit_18 P3-5, tui_audit_11 P3-10 and P3-21: line mode wording.
def test_one_row_says_type_1(self):
with mock.patch.object(install, "_ask", side_effect=["2", "q"]), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._prompt_platform_choice([("retroarch", self.here)], other=lambda: [])
self.assertIn("Type 1, o for another setup or folder, or q to quit.", out.getvalue())
self.assertNotIn("1 to 1", out.getvalue())
def test_a_number_out_of_range_at_copy_from_is_said_as_one(self):
stick = self.root / "media" / "STICK"
stick.mkdir(parents=True)
with mock.patch.object(install, "_mounted_cards", return_value=[stick]), \
mock.patch.object(install, "_ask", side_effect=["2", ""]), \
contextlib.redirect_stdout(io.StringIO()) as out:
install._ask_from_folder([], self.here)
self.assertIn("Type 1, a folder, or Enter.", out.getvalue())
self.assertNotIn("does not exist", out.getvalue())
def test_the_copy_line_names_the_folder_it_copies_from(self):
payload = b"gba"
files = [self.entry("gba_bios.bin", payload, ["nintendo-gba"])]
listing = self.listing("retroarch", files)
old = self.root / "OLD"
old.mkdir()
(old / "gba_bios.bin").write_bytes(payload)
args = _args(manifest=listing, dest=self.here, declared_only=False, offline=True)
setattr(args, "from", [old])
text, _asked = self.line_run(args, [""])
self.assertIn(f"Copying 1 file copied from {old}", text)
self.assertNotIn("found on this machine (", text)
def test_the_save_lines_name_the_frontend_both_times(self):
onion = self.card("ONION", "Onion")
files = [self.entry("gba_bios.bin", b"g", ["nintendo-gba"]),
self.entry("x/keys.txt", b"k", ["nintendo-3ds"], root="save")]
listing = self.listing("retroarch", files, base_destination="RetroArch",
save_directory={"folder": "saves", "by_core": False})
text, _asked = self.line_run(_args(manifest=listing, dest=onion, declared_only=False,
offline=False), [], mounts=[onion])
self.assertNotIn("Onion's save directory", text)
self.assertIn("RetroArch's save directory", text)
def test_line_mode_repeats_the_command_after_enter(self):
payload = b"gba"
files = [self.entry("gba_bios.bin", payload, ["nintendo-gba"])]
listing = self.listing("retroarch", files)
old = self.root / "OLD"
old.mkdir()
(old / "gba_bios.bin").write_bytes(payload)
args = _args(manifest=listing, declared_only=False, offline=True)
setattr(args, "from", [old])
real = io.StringIO()
install.LOCAL.reset()
self.addCleanup(install.LOCAL.reset)
invocation = install.Invocation()
invocation.os_type = "linux"
invocation.record(args)
with mock.patch.object(install, "_INVOCATION", invocation), \
mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "detect_platforms",
return_value=[("retroarch", self.here)]), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install.sys.stdin, "isatty", return_value=True), \
mock.patch.object(install, "_is_terminal", return_value=True), \
mock.patch.object(install, "_ask", return_value=""), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()):
install._install(args, real)
said = real.getvalue()
self.assertIn("Same choice without questions: python3 install.py --platform retroarch "
f"--dest {self.here}", said)
self.assertIn("To check this later, run that command with --check.", said)
def test_line_mode_says_the_card_it_found_and_names_its_folder(self):
onion = self.card("ONION", "Onion")
with mock.patch.object(install, "_mounted_cards", return_value=[onion]), \
@@ -8064,7 +8156,7 @@ class AuditFifteenAndEightScreens(unittest.TestCase):
old.mkdir()
state = checked_state()
state.from_folders = [old]
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: install.OFFLINE_FLAG_VALUE}):
lines = [str(line) for line in install._plan_lines(state.check, state.dest,
state)]
said = [line for line in lines if "USBSTICK" in line]
@@ -8931,6 +9023,55 @@ class AuditEighteenAndEleven(unittest.TestCase):
self.assertNotIn("Cores:", out.getvalue())
self.assertEqual(asked.call_count, 0)
# code_audit_18 P3-2, P3-3, tui_audit_11 P3-1: the remedies of a card.
def test_a_full_card_is_freed_never_left_for_another_drive(self):
with mock.patch.object(install.shutil, "disk_usage",
return_value=mock.Mock(free=10)), \
contextlib.redirect_stderr(io.StringIO()) as err, \
self.assertRaises(SystemExit):
install._check_free_space(self.root, 1000, "retroarch", card=True)
said = err.getvalue()
self.assertIn("on the card, then run it again", said)
self.assertNotIn("--platform", said)
self.assertNotIn("another drive", said)
def test_line_mode_names_the_way_to_another_folder(self):
hint = install.Invocation()
with mock.patch.object(install, "_folder_writable", return_value=False), \
mock.patch.object(install, "_on_read_only_mount", return_value=False):
lines = install.folder_warnings("retroarch", self.root, [], hint, True, False)
self.assertIn("You cannot write to this folder: run it again with another --dest.",
lines)
self.assertNotIn("choose another one", " ".join(lines))
with mock.patch.object(install, "_folder_writable", return_value=False), \
mock.patch.object(install, "_on_read_only_mount", return_value=True):
lines = install.folder_warnings("retroarch", self.root, [], hint, True, True)
self.assertEqual(lines, [install.read_only_words(self.root)])
def test_a_card_plan_offers_one_remedy_first(self):
for facts, locked in (({"free": 50 * 1024 ** 3, "writable": False, "cores": []}, True),
({"free": 10, "writable": True, "cores": []}, False)):
with self.subTest(locked=locked):
state = checked_state(facts)
with mock.patch.object(install, "_on_its_card", return_value=True), \
mock.patch.object(install, "_on_read_only_mount", return_value=locked):
rows = install.tui_rows(state)
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
effects = install.tui_update(state, Key("enter"))
self.assertEqual(rows[0].action, "recheck")
self.assertEqual("install" in [row.action for row in rows], not locked)
self.assertNotIn("choose another one", text)
self.assertEqual(effects, [("run", "check")])
def test_nothing_installable_focuses_the_copy(self):
"""tui_audit_11 P3-8: offline with nothing to copy, the plan focused
Choose systems, whose systems need the internet too."""
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
state = checked_state()
rows = install.tui_rows(state)
self.assertEqual(rows[state.focus.get("plan", 0)].action, "fromcard")
def test_the_card_marker_table_is_written_from_the_rules(self):
"""code_audit_18 P3-6, tui_audit_11 P2-2: the table left out the
SHARE drives. It is generated: every card rule and SHARE drive file