chore: merge line mode on the screen's decisions

# Conflicts:
#	tests/test_install_tui.py
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 10:20:31 +01:00
commit a9301fbdc0
23 files changed
+1007 -246

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 = "ad35e44d7d16467636197f7e40b34c53670eadf75d24681c32488c9bc21637f7"
$defaultInstallSha256 = "b3363820bb97bec559287f72d9712281e22307387fa752b663f6f7af023a0e33"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+427 -149
View File
File diff suppressed because it is too large. Load diff
+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="ad35e44d7d16467636197f7e40b34c53670eadf75d24681c32488c9bc21637f7"
DEFAULT_INSTALL_SHA256="b3363820bb97bec559287f72d9712281e22307387fa752b663f6f7af023a0e33"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 719855,
"sha256": "ad35e44d7d16467636197f7e40b34c53670eadf75d24681c32488c9bc21637f7",
"size": 734772,
"sha256": "b3363820bb97bec559287f72d9712281e22307387fa752b663f6f7af023a0e33",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 772404,
"sha256": "e3aa2c3b790916d103731c47dc3fbf7dd30cd4416ed5120936796b545251e9cd"
"size": 787321,
"sha256": "45366e0a225d8143f7b359fd04cd6b341a21f43057b7d5a41ad8f734f765f142"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 719855,
"sha256": "ad35e44d7d16467636197f7e40b34c53670eadf75d24681c32488c9bc21637f7",
"size": 734772,
"sha256": "b3363820bb97bec559287f72d9712281e22307387fa752b663f6f7af023a0e33",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 719855,
"sha256": "ad35e44d7d16467636197f7e40b34c53670eadf75d24681c32488c9bc21637f7",
"size": 734772,
"sha256": "b3363820bb97bec559287f72d9712281e22307387fa752b663f6f7af023a0e33",
"executable": false
},
{
+1 -1
View File
@@ -37,4 +37,4 @@
Ctrl-C Quit B/Esc Back
Ctrl-C Quit B/Esc Quit
+1 -1
View File
@@ -9,4 +9,4 @@
Ctrl-C Quit B/Esc Back
Ctrl-C Quit B/Esc Quit
+1 -1
View File
@@ -12,4 +12,4 @@
Ctrl-C Quit B/Esc Back
Ctrl-C Quit B/Esc Quit
+1 -1
View File
@@ -21,4 +21,4 @@
Ctrl-C Quit B/Esc Back
Ctrl-C Quit B/Esc Quit
+1 -1
View File
@@ -37,4 +37,4 @@
A/Enter Create B/Esc Back
A/Enter Create B/Esc Quit
+1 -1
View File
@@ -9,4 +9,4 @@
A/Enter Create B/Esc Back
A/Enter Create B/Esc Quit
+1 -1
View File
@@ -12,4 +12,4 @@
A/Enter Create B/Esc Back
A/Enter Create B/Esc Quit
+1 -1
View File
@@ -21,4 +21,4 @@
A/Enter Create B/Esc Back
A/Enter Create B/Esc Quit
+6 -6
View File
@@ -334,7 +334,7 @@
A Create B Back
A Create B Quit
=== create 40x15
RetroBIOS: Create the folder?
@@ -350,7 +350,7 @@
A Create B Back
A Create B Quit
=== create 80x24
RetroBIOS: Create the folder?
@@ -375,7 +375,7 @@
A Create B Back
A Create B Quit
=== checking 40x12
RetroBIOS: Checking
@@ -388,7 +388,7 @@
B Back
B Quit
=== checking 40x15
RetroBIOS: Checking
@@ -404,7 +404,7 @@
B Back
B Quit
=== checking 80x24
RetroBIOS: Checking
@@ -429,7 +429,7 @@
B Back
B Quit
=== plan 40x12
RetroBIOS: RetroArch
+6 -6
View File
@@ -334,7 +334,7 @@
A Create B Back
A Create B Quit
=== create 40x15
RetroBIOS: Create the folder?
@@ -350,7 +350,7 @@
A Create B Back
A Create B Quit
=== create 80x24
RetroBIOS: Create the folder?
@@ -375,7 +375,7 @@
A Create B Back
A Create B Quit
=== checking 40x12
RetroBIOS: Checking
@@ -388,7 +388,7 @@
Start+Select Quit B Back
Start+Select Quit B Quit
=== checking 40x15
RetroBIOS: Checking
@@ -404,7 +404,7 @@
Start+Select Quit B Back
Start+Select Quit B Quit
=== checking 80x24
RetroBIOS: Checking
@@ -429,7 +429,7 @@
Start+Select Quit B Back
Start+Select Quit B Quit
=== plan 40x12
RetroBIOS: RetroArch
+6 -6
View File
@@ -334,7 +334,7 @@
A Create B Back
A Create B Quit
=== create 40x15
RetroBIOS: Create the folder?
@@ -350,7 +350,7 @@
A Create B Back
A Create B Quit
=== create 80x24
RetroBIOS: Create the folder?
@@ -375,7 +375,7 @@
A Create B Back
A Create B Quit
=== checking 40x12
RetroBIOS: Checking
@@ -388,7 +388,7 @@
Start+Select Quit B Back
Start+Select Quit B Quit
=== checking 40x15
RetroBIOS: Checking
@@ -404,7 +404,7 @@
Start+Select Quit B Back
Start+Select Quit B Quit
=== checking 80x24
RetroBIOS: Checking
@@ -429,7 +429,7 @@
Start+Select Quit B Back
Start+Select Quit B Quit
=== plan 40x12
RetroBIOS: RetroArch
+6 -6
View File
@@ -280,7 +280,7 @@
Enter Create Esc Back
Enter Create Esc Quit
=== create 40x15
RetroBIOS: Create the folder?
@@ -296,7 +296,7 @@
Enter Create Esc Back
Enter Create Esc Quit
=== create 80x24
RetroBIOS: Create the folder?
@@ -321,7 +321,7 @@
Enter Create Esc Back
Enter Create Esc Quit
=== checking 40x12
RetroBIOS: Checking
@@ -334,7 +334,7 @@
Ctrl-C Quit Esc Back
Ctrl-C Quit Esc Quit
=== checking 40x15
RetroBIOS: Checking
@@ -350,7 +350,7 @@
Ctrl-C Quit Esc Back
Ctrl-C Quit Esc Quit
=== checking 80x24
RetroBIOS: Checking
@@ -375,7 +375,7 @@
Ctrl-C Quit Esc Back
Ctrl-C Quit Esc Quit
=== plan 40x12
RetroBIOS: RetroArch
+9 -9
View File
@@ -334,7 +334,7 @@
A Create R2 Back
A Create R2 Quit
=== create 40x15
RetroBIOS: Create the folder?
@@ -350,7 +350,7 @@
A Create R2 Back
A Create R2 Quit
=== create 80x24
RetroBIOS: Create the folder?
@@ -375,7 +375,7 @@
A Create R2 Back
A Create R2 Quit
=== checking 40x12
RetroBIOS: Checking
@@ -388,7 +388,7 @@
R2 Back
R2 Quit
=== checking 40x15
RetroBIOS: Checking
@@ -404,7 +404,7 @@
R2 Back
R2 Quit
=== checking 80x24
RetroBIOS: Checking
@@ -429,7 +429,7 @@
R2 Back
R2 Quit
=== plan 40x12
RetroBIOS: RetroArch
@@ -712,7 +712,7 @@
Start Filter A Tick R2 Back
A Filter R2 Back
=== cores 40x15
RetroBIOS: Cores
@@ -728,7 +728,7 @@
Start Filter A Tick R2 Back
A Filter R2 Back
=== cores 80x24
RetroBIOS: Cores
@@ -753,7 +753,7 @@
Start Filter A Tick R2 Back
A Filter R2 Back
=== preview 40x12
RetroBIOS: Preview
+6 -6
View File
@@ -334,7 +334,7 @@
Start Create B Back
Start Create B Quit
=== create 40x15
RetroBIOS: Create the folder?
@@ -350,7 +350,7 @@
Start Create B Back
Start Create B Quit
=== create 80x24
RetroBIOS: Create the folder?
@@ -375,7 +375,7 @@
Start Create B Back
Start Create B Quit
=== checking 40x12
RetroBIOS: Checking
@@ -388,7 +388,7 @@
Menu Quit B Back
Menu Quit B Quit
=== checking 40x15
RetroBIOS: Checking
@@ -404,7 +404,7 @@
Menu Quit B Back
Menu Quit B Quit
=== checking 80x24
RetroBIOS: Checking
@@ -429,7 +429,7 @@
Menu Quit B Back
Menu Quit B Quit
=== plan 40x12
RetroBIOS: RetroArch
+6 -4
View File
@@ -1794,11 +1794,13 @@ class TestDetectedPlatformPrompt(unittest.TestCase):
def test_a_wrong_answer_names_the_choices(self):
result, out = self._answer("x", "2")
self.assertEqual(result, [self.PLATFORMS[1]])
self.assertIn("Type a number from 1 to 3, or q to quit.", out)
self.assertIn("Type a number from 1 to 2, or q to quit.", out)
def test_all_installs_every_platform(self):
result, _ = self._answer("3")
self.assertEqual(result, self.PLATFORMS)
def test_enter_takes_the_first_and_there_is_no_all(self):
"""tui_audit_9 P3-11: the screen focuses the first and has no All."""
result, out = self._answer("")
self.assertEqual(result, [self.PLATFORMS[0]])
self.assertNotIn("All", out)
class TestManualPlatformPrompt(unittest.TestCase):
+6 -5
View File
@@ -390,7 +390,8 @@ class DecliningTheSizePointsAtTheChoice(unittest.TestCase):
self.assertNotIn("see --help", out)
def test_options_given_keep_the_option_advice(self):
out = self._run(["n"], "--system", "sony-playstation")
# The menu is shown with --system too (tui_audit_9 P3-8): Enter keeps it.
out = self._run(["", "n"], "--system", "sony-playstation")
self.assertIn("--system, --core and --region install less", out)
@@ -593,7 +594,7 @@ class TheOneLinerNarrows(unittest.TestCase):
with FakeRepository() as repo:
proc = repo.run("--system", "sony-playstation", "--region", "us")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("Narrowed 5 -> 2 files", proc.stdout)
self.assertIn("Narrowed 5 to 2 files", proc.stdout)
self.assertEqual(repo.installed(), ["pcsx/extra.bin", "scph5501.bin"])
def test_a_core_choice_keeps_the_platform_list(self):
@@ -1448,13 +1449,13 @@ class ThePlatformsOwnListAlone(unittest.TestCase):
with FakeRepository() as repo:
proc = repo.run("--declared-only")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("5 -> 3 files", proc.stdout)
self.assertIn("5 to 3 files", proc.stdout)
self.assertEqual(repo.installed(),
["gba_bios.bin", "scph5500.bin", "scph5501.bin"])
for mode in ("--check", "--dry-run"):
with self.subTest(mode=mode), FakeRepository() as repo:
proc = repo.run("--declared-only", mode)
self.assertIn("5 -> 3 files", proc.stdout, proc.stderr)
self.assertIn("5 to 3 files", proc.stdout, proc.stderr)
self.assertIn("0/3 present", proc.stdout)
self.assertEqual(repo.installed(), [])
@@ -1476,7 +1477,7 @@ class ThePlatformsOwnListAlone(unittest.TestCase):
proc = repo.run("--declared-only", "--dry-run", "--manifest",
str(repo.root / "install" / "retroarch.json"))
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("5 -> 3 files", proc.stdout)
self.assertIn("5 to 3 files", proc.stdout)
def test_every_mode_that_cannot_apply_it_refuses_it(self):
"""Applied or refused, never ignored: each of these would have run
+483 -17
View File
@@ -2908,8 +2908,10 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
dest = install.card_install_dest("onion", "retroarch", interactive=True)
self.assertEqual(dest, onion / "BIOS")
self.assertEqual(len(asked), 3)
self.assertIn("8 does not exist, or is not a folder. Type the top folder of the Onion "
"card.", out.getvalue())
# No card is listed: 8 is no number of the list, and no folder.
self.assertIn("Type the card's top folder, or q to quit.", out.getvalue())
self.assertIn("NEXT does not exist, or is not a folder. Type the top folder of the "
"Onion card.", out.getvalue())
self.assertFalse((REPO_ROOT / "tmp" / "no-such").exists())
def test_a_typed_root_that_is_no_card_of_that_system_is_asked_again(self):
@@ -2953,7 +2955,7 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
self.assertIn(f"2) {second.name} (KNULLI card, {second})", out.getvalue())
self.assertNotIn(str(muos), out.getvalue())
self.assertNotIn(str(blank), out.getvalue())
self.assertIn("Type 1 to 2, or a folder.", out.getvalue())
self.assertIn("Type 1 to 2, a folder, or q to quit.", out.getvalue())
def test_creating_several_folders_on_the_card_is_asked(self):
"""tui_audit_6 P3-8: N to the question ended the whole run where it
@@ -2969,7 +2971,7 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
dest = install.card_install_dest("rocknix", "rocknix", interactive=True)
self.assertEqual(dest, card / "roms" / "bios")
self.assertIn("[y/N]", asked[1])
self.assertEqual(asked[2], "Card root (Ctrl+C to stop): ")
self.assertEqual(asked[2], "Card root, q to quit: ")
self.assertIn("Not created.", out.getvalue())
self.assertFalse((rocknix / "roms").exists())
@@ -2984,7 +2986,7 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
contextlib.redirect_stdout(io.StringIO()):
chosen = install._prompt_manual_platform("linux")
self.assertEqual(chosen, [("rocknix", card / "roms" / "bios")])
self.assertEqual(asked, ["Card root (Ctrl+C to stop): "])
self.assertEqual(asked, ["Card root, q to quit: "])
def test_a_card_of_another_system_is_not_offered(self):
"""tui_audit_5 P1-1: A on the first mount wrote Onion's BIOS/ on a
@@ -3299,7 +3301,8 @@ class OneSystemInFewPresses(unittest.TestCase):
index = next(i for i, row in enumerate(rows) if row.value == target)
if index == focus:
return count
page = max(1, state.rows - 6)
# A page is the rows drawn (tui_audit_9 P2-2).
page = install._page_step(state)
button = "R1" if paging and index - focus >= page else "down"
self.press_button(state, pad, button)
count += 1
@@ -3339,10 +3342,12 @@ class OneSystemInFewPresses(unittest.TestCase):
# The audit's path, A Down A Up Up A R1 x5 X Start A A. Before the
# fast path it took 17 presses here, 57 on muxterm (None kept the
# focus, and muxterm had no page key).
self.assertEqual(self.presses("gptokeyb", fast=False), 15)
self.assertEqual(self.presses("gptokeyb", fast=True), 12)
# A page is the rows drawn since tui_audit_9 P2-2, ten here where it
# was nine: sys-45 is four pages and five Downs away, not five pages.
self.assertEqual(self.presses("gptokeyb", fast=False), 19)
self.assertEqual(self.presses("gptokeyb", fast=True), 16)
self.assertEqual(self.presses("muxterm", fast=False), 55)
self.assertEqual(self.presses("muxterm", fast=True), 12)
self.assertEqual(self.presses("muxterm", fast=True), 16)
def test_a_on_a_system_with_none_ticked_takes_it_alone(self):
state = self.long_list("keys")
@@ -4878,6 +4883,9 @@ class EndToEnd(unittest.TestCase):
with self.subTest(env=env):
self.env.update(env)
term = self.start()
# The setup found is asked as the screen asks it.
term.wait("Enter for RetroArch")
term.send("\r")
term.wait("Enter) install everything")
term.send("q\r")
term.close()
@@ -4896,6 +4904,8 @@ class EndToEnd(unittest.TestCase):
else:
self.env["TERM"] = value
term = self.start()
term.wait("Enter for RetroArch")
term.send("\r")
term.wait("Enter) install everything")
term.send("\r")
term.wait("installed")
@@ -4911,6 +4921,8 @@ class EndToEnd(unittest.TestCase):
with self.subTest(term=name):
self.env["TERM"] = name
term = self.start()
term.wait("Enter for RetroArch")
term.send("\r")
term.wait("Enter) install everything")
term.send("q\r")
term.close()
@@ -5759,7 +5771,7 @@ class AuditSix(unittest.TestCase):
contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest("onion", "retroarch", True)
self.assertEqual(dest, other / "BIOS")
self.assertEqual(out.getvalue().count("Type 1 to 2, or a folder."), 2)
self.assertEqual(out.getvalue().count("Type 1 to 2, a folder, or q to quit."), 2)
def test_the_articles_read(self):
self.assertEqual([install._a(name) for name in ("Onion", "Allium", "ArkOS", "muOS",
@@ -6362,9 +6374,6 @@ def screen_decision(args: argparse.Namespace) -> tuple[str, Path]:
for _press in range(6):
if any(effect[0] == "fetch" for effect in effects):
return state.platform, state.dest
if state.screen == "create":
# Line mode creates a default folder where the screen asks.
state.focus["create"] = 0
effects = install.tui_update(state, Key("enter"))
finally:
install.LOCAL.reset()
@@ -6400,8 +6409,10 @@ class LineModeAndScreenDecideAlike(unittest.TestCase):
def both(self, mounts: list, found: list, **given) -> tuple:
def answer(prompt: str) -> str:
# The first entry of a numbered list, the default of a folder.
return "1" if prompt.startswith(">") else ""
# The first entry of a numbered list, the default of a folder,
# the first drive of a folder question that has no default.
first = prompt.startswith(">") or ("Enter for" not in prompt and " or 1" in prompt)
return "1" if first else ""
extra = {"declared_only": False, "offline": False}
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
@@ -6421,7 +6432,28 @@ class LineModeAndScreenDecideAlike(unittest.TestCase):
shutil.rmtree(bare / "BIOS")
default = install._default_dest("linux", "retroarch")
pc = [("retroarch", self.here)]
share = self.root / "media" / "SHARE"
(share / "roms").mkdir(parents=True)
batocera = self.root / "batocera.json"
batocera.write_text(json.dumps({"manifest_version": 2, "platform": "batocera",
"files": []}))
blank = self.root / "blank"
blank.mkdir()
scenarios = {
# code_audit_16 P1-1, P2-2, tui_audit_9 P1-1: the drive's root in
# line mode, another system's card first on the screen.
"a Batocera list, its SHARE drive": (
[share], [], {"manifest": batocera}, ("batocera", share / "bios")),
"a Batocera list, a muOS card and its SHARE drive": (
[muos, share], [], {"manifest": batocera}, ("batocera", share / "bios")),
# tui_audit_9 P2-3, P3-11: the setups found asked as the screen
# asks, Enter on the first.
"nothing named, RetroArch here": ([], pc, {}, ("retroarch", self.here)),
"a list, RetroArch here and an Onion card": (
[onion], pc, {"manifest": self.list}, ("retroarch", self.here)),
# code_audit_16 P3-1: --dest with a setup found.
"--dest a blank folder, RetroArch here": (
[onion], pc, {"dest": blank}, ("retroarch", blank)),
"a list, an Onion card in the reader": (
[onion], [], {"manifest": self.list}, ("retroarch", onion / "BIOS")),
"a list, an Onion card without its BIOS folder": (
@@ -6568,8 +6600,9 @@ class OneDeviceRegistry(unittest.TestCase):
line = str(install._row_line(rows[label], False, 38))
self.assertIn(label, line)
self.assertTrue(line.rstrip() != (" " + label).rstrip(), line)
# tui_audit_9 P3-3: its one device, cut, where "1 device" named none.
self.assertTrue(str(install._row_line(rows["CrossMix-OS, SD card"], False, 38))
.rstrip().endswith("1 device"))
.rstrip().endswith("TrimUI Smar..."))
class AuditFifteenAndEightScreens(unittest.TestCase):
@@ -6813,6 +6846,437 @@ class CardBoundaries(unittest.TestCase):
self.assertIn("/", points)
def _screen_text(state: install.TuiState, cols: int = 80, rows: int = 24) -> str:
return "\n".join(line.rstrip() for line in install.tui_view(state, cols, rows))
class AuditSixteenAndNineLineMode(unittest.TestCase):
"""code_audit_16 and tui_audit_9: line mode read other decisions than the
screen (a drive's root for Batocera, no other setup once one was found,
no Enter default, a folder question with no way out)."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
for patcher in (mock.patch.dict(install._DETECTED_OS, {}),
mock.patch.dict(os.environ, {"HOME": str(self.root / "home"),
"XDG_CONFIG_HOME": str(self.root / "cfg"),
"RETROBIOS_OS": "linux"})):
patcher.start()
self.addCleanup(patcher.stop)
def ask(self, *answers: str):
asked = []
given = iter(answers)
def answer(prompt: str) -> str:
asked.append(prompt)
return next(given)
return mock.patch.object(install, "_ask", side_effect=answer), asked
def share_drive(self, name: str = "SHARE") -> Path:
drive = self.root / "media" / name
(drive / "roms").mkdir(parents=True)
return drive
def test_a_numbered_share_drive_is_its_bios_folder(self):
"""code_audit_16 P1-1, tui_audit_9 P1-1: 2) SHARE gave the drive's
root, the screen its bios folder."""
muos = self.root / "media" / "MUOS"
make_card(muos, "muOS")
share = self.share_drive()
patcher, _asked = self.ask("1")
with mock.patch.object(install, "_mounted_cards", return_value=[muos, share]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
patcher, contextlib.redirect_stdout(io.StringIO()) as out:
folder = install.ask_platform_folder("linux", "batocera")
self.assertEqual(folder, share / "bios")
text = out.getvalue()
self.assertIn(f"1) SHARE: bios ({share / 'bios'}, created)", text)
self.assertNotIn("MUOS", text)
def test_the_folder_question_quits_on_q_and_says_what_to_type(self):
"""code_audit_16 P2-3: q became the folder ./q, filled, and Enter
with no default repeated the prompt with no word."""
patcher, asked = self.ask("", "q")
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit) as stop:
install.ask_platform_folder("linux", "batocera")
self.assertEqual(stop.exception.code, 130)
self.assertIn("q to quit", asked[0])
self.assertIn("Type a folder, or q to quit.", out.getvalue())
self.assertFalse((Path.cwd() / "q").exists())
def test_a_relative_folder_is_named_in_full(self):
"""code_audit_16 P2-3: BIOS folder: q, Created q, Location: q."""
here = self.root / "work"
here.mkdir()
patcher, _asked = self.ask("./q")
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
mock.patch.object(install.Path, "cwd", return_value=here), \
mock.patch.object(install.os, "getcwd", return_value=str(here)), \
contextlib.redirect_stdout(io.StringIO()):
folder = install.ask_platform_folder("linux", "batocera", ask=False)
self.assertTrue(folder.is_absolute())
self.assertEqual(folder, here / "q")
def test_a_card_root_question_quits_on_q(self):
patcher, asked = self.ask("q")
with mock.patch.object(install, "_mounted_cards", return_value=[]), patcher, \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()), \
self.assertRaises(SystemExit) as stop:
install.card_install_dest("rocknix", "rocknix", interactive=True)
self.assertEqual(stop.exception.code, 130)
self.assertEqual(asked, ["Card root, q to quit: "])
def test_install_for_takes_enter_and_offers_another_setup(self):
"""tui_audit_9 P2-3, P3-11: no Enter default, an All the screen does
not have, and no other setup once one was found."""
here = self.root / "system"
found = [("retroarch", here)]
patcher, asked = self.ask("")
with patcher, contextlib.redirect_stdout(io.StringIO()) as out:
chosen = install._prompt_platform_choice(
found, labels={("retroarch", here): "RetroArch"}, other=lambda: [])
self.assertEqual(chosen, found)
self.assertEqual(asked, ["> [Enter for RetroArch] "])
self.assertIn("o) another setup or folder", out.getvalue())
self.assertNotIn("All", out.getvalue())
other = [("batocera", self.root / "bios")]
patcher, _asked = self.ask("o")
with patcher, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(install._prompt_platform_choice(found, other=lambda: other), other)
def test_the_setups_for_a_dest_come_in_the_screens_order(self):
"""code_audit_16 P3-1: with RetroArch found, --plain --dest put Onion
at 19, the screen at 29."""
dest = self.root / "blank"
dest.mkdir()
here = self.root / "system"
listed = {}
def choose(title, default="", setups=None, mounted=None):
listed["rows"] = [name for name, _label in install._line_setups(
setups, mounted, "linux")]
listed["default"] = default
raise _Decided(None)
with mock.patch.object(install, "detect_platforms",
return_value=[("retroarch", here)]), \
mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "_choose_platform", side_effect=choose), \
contextlib.redirect_stdout(io.StringIO()), self.assertRaises(_Decided):
install._platform_for_dest("linux", dest, True)
with mock.patch.object(install, "detect_platforms",
return_value=[("retroarch", here)]), \
mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
contextlib.redirect_stdout(io.StringIO()):
state, _effects = install.tui_start(_args(dest=dest), "linux")
screen = [install._row_name(row) for row in install.tui_rows(state)
if row.action != "filter"]
self.assertEqual(listed["rows"], screen)
rows = install.tui_rows(state)
self.assertEqual(install._row_name(rows[state.focus["platforms"]]), listed["default"])
def test_system_named_on_the_command_line_keeps_the_menu(self):
"""tui_audit_9 P3-8: --system skipped the menu and its folder line."""
files = [{"dest": "a.bin", "size": 1, "systems": ["nintendo-gba"], "cores": None},
{"dest": "b.bin", "size": 1, "systems": ["sega-saturn"], "cores": None}]
given = (["nintendo-gba"], [], [])
patcher, _asked = self.ask("")
with patcher, contextlib.redirect_stdout(io.StringIO()) as out:
chosen = install._prompt_custom_selection(
"retroarch", files, [], bios_path=self.root, given=given)
self.assertEqual(chosen, given)
text = out.getvalue()
self.assertIn("RetroArch: 1 file, 1 B, for 1 of 2 systems (--system).", text)
self.assertIn("Enter) install these", text)
self.assertIn(f"BIOS folder: {self.root}", text)
def test_narrowing_is_said_without_an_arrow(self):
source = Path(install.__file__).read_text(encoding="utf-8")
self.assertNotIn("} -> {len(files)}", source)
def test_android_typed_names_retroarch(self):
"""tui_audit_9 P3-5: android matched nothing, and nothing more."""
patcher, _asked = self.ask("android", "q")
with patcher, mock.patch.object(install, "detect_os", return_value="linux"), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._choose_platform("Which setup?")
self.assertIn("On Android, choose RetroArch", out.getvalue())
def test_batocera_named_on_a_computer_without_questions_is_refused(self):
"""code_audit_16 P3-3: /userdata/bios taken, Cannot create, end 2
with no error event."""
listing = self.root / "batocera.json"
listing.write_text(json.dumps({"manifest_version": 2, "platform": "batocera",
"files": []}))
events = io.StringIO()
with mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install.EVENTS, "stream", events), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()) as err, \
self.assertRaises(SystemExit) as stop:
install._install(_args(platform="batocera", no_input=True, manifest=listing),
io.StringIO())
self.assertEqual(stop.exception.code, 1)
self.assertNotIn("/userdata/bios", err.getvalue())
self.assertIn("--dest /media/$USER/SHARE/bios", err.getvalue())
kinds = [json.loads(line)["event"] for line in events.getvalue().splitlines()]
self.assertEqual(kinds[-1], "error")
class AuditSixteenAndNineScreens(unittest.TestCase):
"""The screen points of code_audit_16 and tui_audit_9."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
patcher = mock.patch.dict(install._DETECTED_OS, {})
patcher.start()
self.addCleanup(patcher.stop)
def start(self, mounts=(), found=(), **given) -> tuple:
with mock.patch.object(install, "_mounted_cards", return_value=list(mounts)), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
mock.patch.object(install, "detect_platforms", return_value=list(found)), \
mock.patch.object(install, "detect_frontends", return_value=[]):
return install.tui_start(_args(**given), "linux")
def test_a_folder_holding_a_mounted_muos_card_is_no_muos_card(self):
"""code_audit_16 P2-2: /media/me held the MUOS mount point and read as
a muOS card, and _own_card_root walked above the mount."""
parent = self.root / "media"
muos = parent / "MUOS"
make_card(muos, "muOS")
points = frozenset({str(muos)})
with mock.patch.object(install, "_mount_points", return_value=points), \
mock.patch.object(install, "_mounted_cards", return_value=[muos]):
self.assertIsNone(install.card_system(parent))
self.assertEqual(install.card_system(muos)[1], muos / "MUOS" / "bios")
self.assertIsNone(install._own_card_root(muos / "bios"))
self.assertEqual(install._own_card_root(muos / "MUOS" / "bios"), muos)
def test_batocera_share_rows_leave_out_another_systems_card(self):
"""code_audit_16 P2-2: the muOS card was the first share row."""
muos = self.root / "MUOS"
make_card(muos, "muOS")
share = self.root / "SHARE"
share.mkdir()
state, _effects = self.start([muos, share], platform="batocera")
with mock.patch.object(install, "_mounted_cards", return_value=[muos, share]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
rows = [(row.action, row.label) for row in install.tui_rows(state)]
self.assertIn(("share", "SHARE: bios"), rows)
self.assertNotIn(("share", "MUOS: bios"), rows)
def test_a_proposed_folder_is_created_without_the_guard(self):
"""tui_audit_9 P2-1: Use this folder, then Create the folder? focused
on Choose another folder, and Enter, Enter looped."""
state = install.TuiState(os_type="linux", home=self.root)
usual = self.root / ".config" / "retroarch" / "system"
state.stack, state.platform = ["platforms"], "retroarch"
install._open_folder_screen(state, usual)
effects = install.tui_update(state, Key("enter"))
self.assertEqual(effects, [("fetch", "retroarch")])
self.assertNotIn("create", state.stack)
self.assertIn("system is created in", state.card_note)
typed = self.root / "a" / "b" / "c"
state = install.TuiState(os_type="linux", home=self.root)
state.stack, state.platform = ["folder"], "retroarch"
install._choose(state, "retroarch", typed)
self.assertEqual(state.screen, "create")
self.assertEqual(install.tui_rows(state)[state.focus["create"]].action, "other")
def test_a_page_moves_by_the_rows_drawn(self):
"""tui_audit_9 P2-2: at 40x12 PgDn moved six rows over four drawn,
and muOS and ROCKNIX were never shown."""
for cols, rows in ((40, 12), (40, 15), (80, 24)):
with self.subTest(size=f"{cols}x{rows}"):
state, _effects = self.start()
state.cols, state.rows = cols, rows
seen = set()
for _press in range(40):
text = _screen_text(state, cols, rows)
seen |= {line.strip("|> ").split(",")[0].strip()
for line in text.splitlines()}
install.tui_update(state, Key("pgdn"))
for name in ("muOS, SD card", "ROCKNIX, SD card", "JELOS, SD card",
"EmuELEC, SD card", "RetroOZ, SD card"):
self.assertTrue(any(line.startswith(name.split(",")[0]) for line in seen),
name)
def test_the_first_screen_says_quit_and_a_filter_says_clear(self):
"""tui_audit_9 P3-1, P3-2."""
state, _effects = self.start(platform="onion")
self.assertEqual(state.stack, ["folder"])
bar = install.tui_view(state, 80, 24)[-1]
self.assertIn("Esc Quit", bar)
self.assertNotIn("Esc Back", bar)
state, _effects = self.start()
press(state, "/", "o", "n")
self.assertIn("Esc Clear", install.tui_view(state, 80, 24)[-1])
press(state, "enter")
self.assertIn("Esc Clear", install.tui_view(state, 80, 24)[-1])
def test_rows_at_40_columns_name_a_device(self):
"""tui_audit_9 P3-3: "1 device" for CrossMix-OS."""
state, _effects = self.start()
text = _screen_text(state, 40, 60)
self.assertNotIn("1 device", text)
crossmix = next(line for line in text.splitlines() if "CrossMix-OS" in line)
self.assertIn("TrimUI", crossmix)
def test_a_model_finds_the_systems_that_list_its_family(self):
"""code_audit_16 P2-1, tui_audit_9 P3-4: RG353V missed ROCKNIX, RG351P
missed AmberELEC and ROCKNIX."""
for system, devices in install._SETUP_DEVICES.items():
for device in devices:
for model in (device, *install._DEVICE_FAMILIES.get(device, ())):
rows = install.setup_list([], set(), model, "android")
names = {install._row_name(row) for row in rows}
with self.subTest(system=system, model=model):
self.assertIn(system, names)
def test_the_filter_reads_system_names_as_line_mode_does(self):
"""code_audit_16 P3-2."""
for typed, system in (("spruce os", "spruceOS"), ("unofficial os", "UnofficialOS"),
("cross mix", "CrossMix-OS")):
with self.subTest(typed=typed):
rows = install.setup_list([], set(), typed, "linux")
self.assertIn(system, {install._row_name(row) for row in rows})
def test_android_on_the_filter_names_retroarch(self):
state, _effects = self.start()
state.filters["platforms"] = "android"
self.assertIn("On Android, choose RetroArch", " ".join(_screen_text(state).split()))
def test_a_wrapped_path_has_no_space_inserted(self):
"""tui_audit_9 P3-6: retroarch/ system."""
path = "/home/moi/retroarch_system/tmp/tui_audit9/sb/nd/home/.config/retroarch/system"
for width in range(30, 100):
lines = install._wrap(f"{path} does not exist.", width)
# Each line after the first is indented by two spaces.
joined = lines[0] + "".join(line[2:] for line in lines[1:])
with self.subTest(width=width):
self.assertIn(path, joined)
self.assertTrue(all("/ " not in line for line in lines), lines)
def test_f1_names_what_the_card_lacks(self):
"""tui_audit_9 P3-7: F1 said the refusal again."""
muos = self.root / "MUOS"
make_card(muos, "muOS")
setup = install.card_setup("Onion", "retroarch")
choice = install._not_the_card(setup, muos, muos, "muOS")
self.assertIn(".tmp_update", choice.detail)
def test_help_at_40x12_keeps_quit_and_this_help(self):
"""tui_audit_9 P3-15, and Space for ticking on a keyboard."""
state = checked_state()
press(state, "down", "down", "down", "down", "enter")
state.stack.append("systems")
state.help = True
text = _screen_text(state, 40, 12)
self.assertIn("Quit", text)
self.assertIn("This help", text)
self.assertIn("X, Space, Right/Left", " ".join(text.split()))
def test_the_copy_hint_fits_at_40_columns(self):
"""tui_audit_9 P3-16: cut after "copied,"."""
state = checked_state()
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "_listed", return_value=[]):
install._open_from_browser(state)
text = " ".join(_screen_text(state, 40, 12).split())
self.assertIn("Choose the old card, a backup folder or a ZIP.", text)
def test_the_miyoo_bar_says_what_a_does(self):
"""tui_audit_9 P3-17: A Tick on All and None, where A unticks."""
state = checked_state()
state.pad = "miyoo"
state.stack.append("systems")
state.focus["systems"] = 1
bar = install.tui_view(state, 40, 12)[-1]
self.assertNotIn("A Tick", bar)
state.focus["systems"] = 3
self.assertIn("A Tick", install.tui_view(state, 40, 12)[-1])
def test_system_given_is_said_and_widened(self):
"""tui_audit_9 P3-8: one system named, no word on the plan and no
way to widen it."""
state = checked_state()
state.cli_systems = ["gba"]
state.systems = [row for row in state.systems if row["system"] == "nintendo-gba"]
state.ticked = {"nintendo-gba"}
self.assertIn("For 1 of 3 systems (--system).", " ".join(_screen_text(state).split()))
row = next(row for row in install.tui_rows(state) if row.action == "systems")
effects = install._activate(state, row)
self.assertEqual(effects, [("run", "check")])
self.assertNotIn(("--system", "gba"), install.tui_flags(state))
self.assertTrue(state.widen)
def test_a_local_copy_draws_no_speed(self):
"""tui_audit_9 P3-9: 0 B/s 0:00 left."""
out = io.StringIO()
term = install.Terminal(out)
term.live = True
progress = install.Progress(2, 100, term)
progress.add(50, resumed=True, network=False)
progress._draw()
self.assertNotIn("B/s", out.getvalue())
self.assertNotIn("left", out.getvalue())
def test_the_offline_remedy_follows_the_files_it_is_about(self):
"""tui_audit_9 P3-10."""
unplaced = [{"reason": "no_save_directory", "frontend": "RetroArch"}]
with mock.patch.object(install, "_offline_only", return_value=True), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._report_outcome([self.root], 6, 0, 0, 1, False,
omitted_entries=unplaced, offline=16,
sources="6 files copied from OLD, 16 still missing.")
lines = out.getvalue().splitlines()
remedy = next(i for i, line in enumerate(lines) if "Copy them" in line
or "They are not in" in line)
missing = next(i for i, line in enumerate(lines) if "still missing" in line)
save = next(i for i, line in enumerate(lines) if "save directory" in line)
self.assertEqual(remedy, missing + 1)
self.assertLess(remedy, save)
def test_first_install_says_where_to_type(self):
"""code_audit_16 P3-4, tui_audit_9 P3-19."""
page = (REPO_ROOT / "wiki" / "first-install.md").read_text(encoding="utf-8")
self.assertNotIn("There, type its name", page)
self.assertIn("Press / and type the device", page)
def test_a_folder_that_cannot_be_created_ends_on_an_error_and_a_summary(self):
"""code_audit_16 P3-3: Cannot create, then end 2 and nothing else."""
import subprocess # noqa: PLC0415
from test_install_offline import PAYLOAD, _offline_env, _pack
blocker = self.root / "file"
blocker.write_text("not a folder")
pack = _pack(self.root, "RetroArch_v1_BIOS_Pack.zip", PAYLOAD)
proc = subprocess.run(
[sys.executable, str(REPO_ROOT / "install.py"), "--platform", "retroarch",
"--dest", str(blocker / "system"), "--from-pack", str(pack), "--events"],
env=_offline_env(), capture_output=True, text=True, timeout=120, check=False,
stdin=subprocess.DEVNULL)
events = [json.loads(line) for line in proc.stdout.splitlines()]
kinds = [event["event"] for event in events]
self.assertEqual(kinds[-3:], ["error", "summary", "end"], proc.stderr)
self.assertEqual((events[-3]["stage"], events[-3]["reason"]), ("dest", "not_writable"))
self.assertEqual(events[-1]["code"], 2)
class EdgesTheMutantsFound(unittest.TestCase):
"""Changes to the code that every test let through: the survivors of a
mutmut run over the device filter, the card bounds and the WSL paths."""
@@ -6844,7 +7308,9 @@ class EdgesTheMutantsFound(unittest.TestCase):
make_card(root / "card", system)
return root / "card"
def test_a_card_is_looked_for_up_to_the_mount_point_and_no_further(self):
def test_a_card_is_looked_for_up_to_its_mount_point_and_no_further(self):
"""code_audit_15 P3-2 reads every folder up to the mount point:
three at most took MUOS/a/b/c/d for a KNULLI card."""
card = self.card("KNULLI")
setup = install.card_setup("KNULLI", "knulli")
deep = card / "a" / "b" / "c" / "d"
+3 -2
View File
@@ -80,8 +80,9 @@ It opens a menu:
```
- **Right folder?** Press Enter.
- **Wrong folder?** Choose **Another setup or folder**, then browse to yours.
- **A handheld's SD card?** There, type its name (Miyoo Mini).
- **Wrong folder, or a handheld's SD card?** Choose
**Another setup or folder**. Press / and type the device (Miyoo Mini),
or browse.
Arrows move, Enter chooses, Esc goes back, as A and B on a handheld.
+25 -12
View File
@@ -208,6 +208,10 @@ line instead, by themselves. A terminal whose description is not found on the
machine gets the screen. A listing, `--check`, `--dry-run`, `--events`,
`--quiet`, `--no-input`, `--yes` and `--restore` keep the plain output. Options
given on the command line fill the screens in advance and are kept as they are.
`--system` is said on the plan (`For 1 of 12 systems (--system)`), and
**Choose systems** reads every system again with those ticked. The `--plain`
menu shows it the same way, Enter keeping what `--system` named and `c`
choosing anew.
However the screen ends (Enter on the result, `q`, Ctrl+C, Ctrl+D, SIGTERM, a
hangup), the terminal is given back as the screen found it: the cursor shown,
@@ -959,7 +963,7 @@ own in the setup list (**Onion, SD card**), without the filter, and in the
`--plain` menu, beside the devices that run it (Miyoo Mini, Miyoo Mini Plus).
The filter takes a device name, and so does the `--plain` menu, which lists
the setups it matches by number. With nothing found here, the SD card rows
come right after RetroArch: Onion is four presses away with L1 and R1 at
come right after RetroArch: Onion comes into view after five presses of L1 and R1 at
40x12, where it was row 30 of 34. That row, `--platform onion` and the menu
all ask for the card: one card of that system mounted when the run starts is
taken, otherwise
@@ -994,14 +998,22 @@ home folder or above the default, no default is taken: the frontend's own
configuration there may name another folder, so the run asks for `--dest`.
With nothing detected, the setup screen lists the platforms to choose from,
and `--plain` gives the same list numbered, in the same order. Both find the
SD cards mounted here, the screen under **Found on this machine**. With
several found, the setup screen lists each one, and `--plain` asks which. A
platform named, or read from a list or a pack, and found nowhere gets its
folder asked, its usual one under Enter, the mounted drives numbered. On a
computer Batocera and Recalbox have none: the `bios` folder of their SHARE
drive or network share is theirs. A device several systems run lists them
all. Both need a terminal: piped into a script
and `--plain` gives the same list numbered, in the same order, with `--dest`
too. Both find the SD cards mounted here, the screen under **Found on this
machine**. With a setup found, the setup screen lists it with **Another setup
or folder**, and `--plain` asks the same: Enter takes the first, `o` opens the
numbered list and then the folder, `q` quits. A platform named, or read from
a list or a pack, and found nowhere gets its folder asked, its usual one under
Enter, the mounted drives numbered. A folder typed is read from the current
one when it is not a full path, and named in full after. `q` quits there too
(a folder named `q` is typed `./q`), and Enter with no default says what to
type. On a computer Batocera and Recalbox have no default: the drives are
numbered by their `bios` folder (`SHARE: bios`), as the screen lists them,
and a drive whose files name another system is left out. Without questions
the run stops there with an `error` event and a `--dest` example. A device
several systems run lists them all, and a model finds the systems that list
its family (`RG353V` finds ROCKNIX, which lists `RG353`). Both need a
terminal: piped into a script
with no platform to install for, it prints the manual invocation, in the form
the run was started with (the one-line command for a one-liner user), and
exits 1.
@@ -1196,7 +1208,7 @@ gets the column that names both:
| Move | Up/Down, k/j | Up/Down, k/j | D-pad, Up/Down | D-pad, Up/Down | D-pad, Up/Down | D-pad, Up/Down | D-pad, Up/Down |
| Page | PgUp/PgDn | PgUp/PgDn, L1/R1 | L1/R1, PgUp/PgDn | D-pad Left/Right | D-pad Left/Right | L1/R1, PgUp/PgDn | L1/R1, PgUp/PgDn |
| Continue, open, choose | Enter | A/Enter, Start | A, Start, Enter | A, Start, Enter | Start, Enter | A, Start, Enter | A, Start, Enter |
| Tick, untick | Space, Right/Left | X, Right/Left | X, Space, Right/Left | X, Space | Y, Space | X, Space, Right/Left | A, Space, Right/Left |
| Tick, untick | Space, Right/Left | X, Space, Right/Left | X, Space, Right/Left | X, Space | Y, Space | X, Space, Right/Left | A, Space, Right/Left |
| Options | o, Tab | Y, o, Tab | Y, Tab, o | Y, Tab, o | o, Tab | Y, Tab, o | L2, Tab, o |
| Back, and quit on the first screen | Esc, Backspace | B/Esc, Backspace | B, Select, Esc | B, Select, Esc | B, Esc, Backspace | B, Select, Esc | R2, Esc, Backspace |
| Stop an install | Esc, Ctrl-C | B/Esc, Ctrl-C | B, Esc, Ctrl-C | B, Esc, Ctrl-C | B, Esc, Ctrl-C | B, Esc, Ctrl-C | R2, Esc, Ctrl-C |
@@ -1376,7 +1388,8 @@ The tables of options, events, exit codes and keys on this page sit between
from `EVENT_KEYS`, the codes from its `sys.exit` calls, the keys from
`TUI_ROUTES`. The prose cells stay as written. A row the code gains stops the
script until its text is added inside the markers, and `--check`, which a
test runs, fails on a table behind the code.
test runs, fails on a table behind the code. `--page FILE` renders another
copy of the page, which the tests use.
The [handheld](#on-a-handheld-the-retrobios-port) archives each carry
`install.py`, the one the one-line command of the same commit downloads:
@@ -1444,7 +1457,7 @@ other schemas under `api/v1/schemas/`.
| `start` | `schema`, `installer`, `mode` | First line. `schema` is 1, `installer` the identity `--version` prints, `mode` one of `install`, `check`, `dry-run`, `list` |
| `unsearched` | `folder`, `reason`, `message` | Before `detect`, once for each folder the search for setups could not look in, this user being refused it. What it would hold counts as absent, and the other places are still searched: `detect` then lists what they hold, and an empty `platforms` after an `unsearched` line means nothing was found where the installer could look. `reason` says why in words (`Permission denied`), `message` says it in a sentence |
| `detect` | `os`, `os_name`, `platforms` | Once the platforms are known. `os_name` comes from `/etc/os-release`. `platforms` lists `{platform, name, bios_dir}`, and is empty when nothing supported was found |
| `error` | `stage`, `reason`, `platform`, `message` | The file list could not be read (`stage` is `manifest`): `reason` is `network`, `not_found` or `invalid`, `clock` when the server's certificate was refused and the system clock reads a date before the installer was written, `no_ca` when the machine holds no list of trusted certificates (`SSL_CERT_FILE` names one), or `offline` when `--offline` forbids the request. A folder named with `--from` cannot be read (`stage` is `from`, `reason` `not_found`, `not_a_folder` for a file that is no ZIP, `unreadable`, or `system` for the root of the system, `/proc`, `/sys` or `/dev`). The BIOS folder sits under a folder this user cannot open (`stage` is `dest`, `reason` `unreadable`), and the run ends with code 1 before anything is written. Two systems are detected on one machine and no `--dest` names the folder (`stage` is `detect`, `reason` `conflict`). The platform named with `--platform` is found nowhere and no folder can be assumed for it: a card system with no card named, a portable emulator, or no usual folder on this system or one set aside behind a folder this user cannot open (`stage` is `detect`, `reason` `not_found`). A `--dest` refused as the card of the system named carries the same `stage` and `reason`: another system's card, a folder inside a card, a drive with none of the files its card always has, or a folder that does not exist. `message` says it in words, and is the only thing that tells these cases apart |
| `error` | `stage`, `reason`, `platform`, `message` | The file list could not be read (`stage` is `manifest`): `reason` is `network`, `not_found` or `invalid`, `clock` when the server's certificate was refused and the system clock reads a date before the installer was written, `no_ca` when the machine holds no list of trusted certificates (`SSL_CERT_FILE` names one), or `offline` when `--offline` forbids the request. A folder named with `--from` cannot be read (`stage` is `from`, `reason` `not_found`, `not_a_folder` for a file that is no ZIP, `unreadable`, or `system` for the root of the system, `/proc`, `/sys` or `/dev`). The BIOS folder sits under a folder this user cannot open (`stage` is `dest`, `reason` `unreadable`), and the run ends with code 1 before anything is written. The BIOS folder cannot be created (`stage` is `dest`, `reason` `not_writable`): a `summary` follows with what the run installed before it. Two systems are detected on one machine and no `--dest` names the folder (`stage` is `detect`, `reason` `conflict`). The platform named with `--platform` is found nowhere and no folder can be assumed for it: a card system with no card named, a portable emulator, Batocera or Recalbox named on a computer (their folder is the `bios` folder of their SHARE drive), or no usual folder on this system or one set aside behind a folder this user cannot open (`stage` is `detect`, `reason` `not_found`). A `--dest` refused as the card of the system named carries the same `stage` and `reason`: another system's card, a folder inside a card, a drive with none of the files its card always has, or a folder that does not exist. `message` says it in words, and is the only thing that tells these cases apart |
| `scanned` | `folder`, `files`, `truncated`, `other_drives` | Each folder named with `--from`, once read and before `detect`. `files` counts the files found, `truncated` is true when the search stopped at 200,000 of them, and `other_drives` lists the folders below it on another drive, which were not entered |
| `file` | `platform`, `dest`, `systems`, `size`, `state` | Each file of the list after the check. `state` is `present` (hash verified), `wrong_hash` or `missing` |
| `system` | `platform`, `system`, `name`, `files`, `present`, `missing`, `wrong_hash`, `bytes_needed` | Each system, sorted by id, and with `--system` only the systems named. Files with no system count under `other`. `name` is the platform's own name for it |