fix: one setup decision for line mode and screen

Line mode finds mounted cards, shows its menu and names the folder for a
pack, asks the folder instead of taking a default, and lists setups in the
screen's order. Devices have one spelling each, matched without case,
spaces or punctuation, and a device several systems run lists them all.
--tui --dest reads a card as --plain does. Card boundaries read every
parent up to a mount point from mountinfo. --offline with --yes counts the
files it leaves out as pending, never as failed. The P3 points of
code_audit_15 and tui_audit_8 follow.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 08:43:08 +01:00
1 parent 5431a42e23
commit 4fbbd2c760
18 files changed
+1413 -383

No files matched your search

+10 -10
View File
@@ -15,26 +15,26 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, Odroid Go Super, RG351V, +6
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, R35S, RGB10
CrossMix-OS, SD card TrimUI Smart Pro
dArkOS, SD card Odroid Go Advance, RGB10, RG351MP, G350
EmuELEC, SD card Odroid Go
JELOS, SD card RetroArch files
KNULLI, SD card Anbernic RG-Arc-S, RG28XX, RG34XX, RG35XX Original, RG35XX Plus, RG35XX H, +16
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, Powkiddy RK2023, RGB30, RGB10 Max 3
JELOS, SD card RG351M, RG351V, RG353M, RG353P, RG353V, RG353VS, RG503, RG552, R33S, R36S, +24
KNULLI, SD card RG ARC-S, RG28XX, RG34XX, RG35XX, RG35XX Plus, RG35XX H, RG35XX 2024, +15
KOLOS, SD card RG353P, RG353M, RG353V, RG353VS, RG503, RK2023, RGB30, RGB10 Max 3
Koriki, SD card Miyoo Mini
MinUI, SD card Anbernic, Miyoo, TrimUI
MinUI, SD card RG28XX, RG34XX, RG34XX SP, RG35XX, RG35XX Plus, RG35XX H, RG35XX SP, +16
MiyooCFW, SD card BittBoy, Pocket Go, PowKiddy Q90, PowKiddy V90, PowKiddy Q20 Mini, Sup M3, +1
muOS, SD card RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX H, RG40XX H, RG40XX V, +4
MyMinUI, SD card Anbernic, Miyoo
muOS, SD card RG35XX H, RG35XX Plus, RG35XX SP, RG35XX 2024, RG34XX, RG40XX H, RG40XX V, +4
MyMinUI, SD card RG35XX, Miyoo Mini, Miyoo Mini Flip, Miyoo Mini Plus, Miyoo A30, +16
NextUI, SD card TrimUI Brick, TrimUI Smart Pro
Onion, SD card Miyoo Mini, Miyoo Mini Plus
plumOS, SD card MagicX XU20 V32
RetroOZ, SD card OGS, OGA, RGB10Max, RGB10Max2
ROCKNIX, SD card Anbernic RG351, RG353, RG503, RG552, RG 55G1, RG ARC, RG35XX 2024, +38
RetroOZ, SD card Odroid Go Super, Odroid Go Advance, RGB10 Max, RGB10 Max 2
ROCKNIX, SD card RG351, RG353, RG503, RG552, RG 55G1, RG ARC, RG35XX 2024, RG35XX Plus, +37
spruceOS, SD card Miyoo A30, Miyoo Flip, Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip, +5
UnofficialOS, SD card R35S, R36S, RG351M, RG351P, RG351V, RG353M, RG353P, RG353V, RG503, +5
A/Enter Choose B/Esc Back / Filter ? Help
+2 -2
View File
@@ -13,8 +13,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+2 -2
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
A Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
A Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+14 -14
View File
@@ -97,8 +97,8 @@
MiSTer FPGA
RetroPie
Allium, SD card Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip
AmberELEC, SD card Anbernic RG351, RG552
Anbernic stock firmware, SD card RetroArch files
AmberELEC, SD card RG351, RG552
Anbernic stock firmware, SD card RG35XX Plus, RG35XX H, RG35XX SP, RG28XX
ArchR, SD card R36S, R33S, R36S Soysauce, Odroid Go Advance, +8
ArkOS, SD card RG353M, RG353V, RG503, RGB30, RK2023, RG351MP, RG351V, +2
CrossMix-OS, SD card TrimUI Smart Pro
@@ -980,8 +980,8 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
1-8 of 17
Details: /tmp/retrobios-x/install.log
Start Exit Up/Down Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -993,11 +993,11 @@
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py
--restore
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
1-11 of 17
Details: /tmp/retrobios-x/install.log
Start Exit Up/Down Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -1007,11 +1007,6 @@
failed: scph5500.bin (HTTP 404)
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py --restore
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Same choice without questions:
python3 install.py --platform retroarch --dest
/home/user/.config/retroarch/system
Details: /tmp/retrobios-x/install.log
@@ -1023,6 +1018,11 @@
Start Exit Up/Down Scroll
=== error 40x12
RetroBIOS: Something went wrong
+18 -3
View File
@@ -1829,10 +1829,25 @@ class TestManualPlatformPrompt(unittest.TestCase):
self.assertEqual(chosen, [("batocera", Path("~/my bios").expanduser())])
def test_default_directory_comes_from_the_table(self):
answers = iter(["batocera", ""])
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)):
answers = iter(["lakka", ""])
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)), \
unittest.mock.patch.object(install, "_mounted_cards", return_value=[]), \
contextlib.redirect_stdout(io.StringIO()):
chosen = install._prompt_manual_platform("linux")
self.assertEqual(chosen, [("batocera", install.DEFAULT_DESTS["batocera"])])
self.assertEqual(chosen, [("lakka", install.DEFAULT_DESTS["lakka"])])
def test_batocera_on_a_computer_gets_no_default(self):
"""tui_audit_8 P2-4: /userdata/bios, a folder only the Batocera
machine has, was offered on a computer, and Enter took it."""
answers = iter(["batocera", "", "~/share/bios"])
out = io.StringIO()
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)), \
unittest.mock.patch.object(install, "_mounted_cards", return_value=[]), \
contextlib.redirect_stdout(out):
chosen = install._prompt_manual_platform("linux")
self.assertEqual(chosen, [("batocera", Path("~/share/bios").expanduser())])
self.assertIn("SHARE drive", out.getvalue())
self.assertNotIn("/userdata/bios", out.getvalue())
def test_quit_stops_like_ctrl_c(self):
"""`install.sh && retroarch` went on to start RetroArch with no BIOS
+2 -1
View File
@@ -370,7 +370,8 @@ class TheQuestion(unittest.TestCase):
def test_a_console_without_python_says_so_once(self):
asked, put = self.run_finish("y", system="Onion")
self.assertEqual((asked, put), ([], []))
self.assertIn(install.NO_PYTHON_WORDS, self.output)
self.assertIn(install.no_python_words("Onion"), self.output)
self.assertNotIn("This console", self.output)
class TheOption(unittest.TestCase):
+78 -2
View File
@@ -662,7 +662,9 @@ class OfflineMakesNoRequest(Scratch):
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual((self.dest / "scph5501.bin").read_bytes(), US)
self.assertFalse((self.dest / "gba_bios.bin").exists())
self.assertIn("no network request", proc.stdout + proc.stderr)
# Missing, never failed (code_audit_15 P3-1): no FAILED line per file.
self.assertIn("3 not fetched (--offline)", proc.stdout + proc.stderr)
self.assertNotIn("FAILED", proc.stdout + proc.stderr)
self.assertIn("without --offline", proc.stdout + proc.stderr)
self.assertNotIn("trying again", proc.stderr)
@@ -839,7 +841,11 @@ class OfflineLeavesFilesMissing(Scratch):
self.list.write_text(json.dumps(listed))
self.complete()
proc = self.run_installer("--offline", "--manifest", str(self.list), "--no-input")
self.assertIn("bluemsx: could not read", proc.stderr)
# Its files are missing, as the BIOS files --offline leaves out are,
# with or without questions.
self.assertIn("bluemsx: 0 already up to date, 1 need the internet (--offline)",
proc.stdout)
self.assertNotIn("could not read", proc.stderr)
self.assertNotIn("no longer in the source", proc.stdout)
@@ -936,5 +942,75 @@ class OneOfflineVocabulary(Scratch):
self.assertNotIn("install everything", text)
class AuditFifteenOffline(Scratch):
"""code_audit_15 P3-1, P3-5 and tui_audit_8 P3-11: what --offline and a
pack leave out, and what a folder copied from holds, said as such."""
def test_yes_offline_counts_the_rest_missing_never_failed(self):
"""The screen's own install (--yes): each file left out was
"downloading", then FAILED, and pending and failed at once."""
old = self.root / "old"
old.mkdir()
(old / "scph5501.bin").write_bytes(US)
proc = self.run_installer("--offline", "--manifest", str(self.list), "--from",
str(old), "--yes", "--events")
self.assertEqual(proc.returncode, 1, proc.stderr)
events = [json.loads(line) for line in proc.stdout.splitlines()]
summary = next(event for event in events if event["event"] == "summary")
self.assertEqual((summary["installed"], summary["pending"], summary["failed"]),
(1, 3, []))
self.assertNotIn("FAILED", proc.stderr)
self.assertNotIn("downloading", proc.stderr)
self.assertIn("1 file installed, 3 not fetched (--offline)", proc.stderr)
download = next(event for event in events if event["event"] == "download")
self.assertEqual(download["files"], 1)
# 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"}):
title, lines = install.tui_outcome(state)
self.assertEqual(title, "Finished with files missing")
self.assertIn("1 installed, 3 not fetched (--offline).", lines[0])
def test_a_pack_is_copied_not_downloaded(self):
downloads = self.root / "downloads"
downloads.mkdir()
pack = _pack(downloads, "RetroArch_v1_BIOS_Pack.zip", PAYLOAD)
proc = self.run_installer("--from-pack", str(pack), "--dry-run", "--no-input")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("Would copy 4 files", proc.stdout)
self.assertIn("from the pack to", proc.stdout)
self.assertNotIn("download", proc.stdout.lower())
def test_a_folder_holding_none_of_the_files_is_said_and_left(self):
empty = self.root / "empty"
(empty / "photos").mkdir(parents=True)
(empty / "photos" / "a.jpg").write_bytes(b"jpeg")
install.LOCAL.reset()
self.addCleanup(install.LOCAL.reset)
files = manifest()["files"]
answers = iter(["f", f"{self.root}/x/../empty", "q"])
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
unittest.mock.patch.object(install, "_ask", lambda _prompt: next(answers)), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._prompt_custom_selection("retroarch", files, [], bios_path=self.dest)
text = out.getvalue()
self.assertIn("empty holds none of these files.", text)
self.assertNotIn("install what is found", text)
self.assertNotIn("..", text)
self.assertEqual(install.LOCAL.folders, [])
# One that holds some is counted.
(empty / "scph5501.bin").write_bytes(US)
answers = iter(["f", str(empty), "q"])
with unittest.mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
unittest.mock.patch.object(install, "_ask", lambda _prompt: next(answers)), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._prompt_custom_selection("retroarch", files, [], bios_path=self.dest)
self.assertIn("1 of 4 found in empty.", out.getvalue())
self.assertIn("Enter) install what is found in", out.getvalue())
if __name__ == "__main__":
unittest.main()
+3 -1
View File
@@ -704,7 +704,9 @@ class TheOneLinerNarrows(unittest.TestCase):
env = dict(repo.env(), HOME=str(home), USERPROFILE=str(home),
XDG_CONFIG_HOME=str(home), RETROBIOS_OS="linux")
command = f"{shlex.quote(sys.executable)} {shlex.quote(str(REPO_ROOT / 'install.py'))}"
for answer, code in (("n", 2), ("y", 0)):
# No to the folders asks the folder again, as the screen's
# Choose another folder does: the input then ends, Ctrl+D.
for answer, code in (("n", 130), ("y", 0)):
with self.subTest(answer=answer):
proc = subprocess.run(
["script", "-qec", command, "/dev/null"],
+517 -13
View File
@@ -2193,7 +2193,10 @@ class AuditRoundThree(unittest.TestCase):
self.assertEqual(proc.returncode, 1)
self.assertIn(f"{root} is not a muOS SD card: no muOS files on it. muOS keeps "
"MUOS at the root of its card.", proc.stderr)
self.assertIn("--dest <card>/MUOS/bios", proc.stderr)
# muOS always names its card: <card>/MUOS/bios would be refused the
# same (code_audit_15 P3-4), the card's top folder is the remedy.
self.assertIn("Name the top folder of the muOS card with --dest", proc.stderr)
self.assertNotIn("<card>/", proc.stderr)
self.assertEqual(list(root.iterdir()), [root / "roms"])
# Its BIOS folder named outright is its card's, checked as the card
# is: on a folder with no MUOS it is refused, as typed it was.
@@ -2861,7 +2864,8 @@ class ACardSystemNamedOnAComputer(unittest.TestCase):
contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest("onion", "retroarch", interactive=False)
self.assertEqual(dest, onion / "BIOS")
self.assertIn(f"Onion card found at {onion}", out.getvalue())
self.assertIn(f"Onion card found at {onion.name}, BIOS folder {onion / 'BIOS'}",
out.getvalue())
def test_at_a_terminal_the_card_root_is_asked(self):
onion = self.card(self.ONION)
@@ -3124,15 +3128,17 @@ class TheSetupFilterKnowsDevices(unittest.TestCase):
TYPED = {
"steam deck": {"retroarch", "batocera", "emudeck", "retrodeck"},
"r36s": {"retroarch", "rocknix"},
"rg35xx": {"retroarch", "rocknix", "knulli"},
# MinUI and MyMinUI list their devices (code_audit_15 P2-1), and on
# a computer an Android device is no RetroArch of this machine's.
"r36s": {"retroarch", "rocknix", "myminui"},
"rg35xx": {"retroarch", "rocknix", "knulli", "minui", "myminui"},
"trimui": {"retroarch", "knulli", "minui", "nextui"},
"retroid": {"retroarch", "rocknix", "knulli"},
"retroid": {"rocknix", "knulli"},
"miyoo": {"retroarch", "knulli", "minui", "myminui"},
"anbernic": {"retroarch", "rocknix", "amberelec", "knulli", "minui", "myminui"},
"odin": {"retroarch", "rocknix"},
"odin": {"rocknix"},
"ayn": {"retroarch", "rocknix"},
"rp5": {"retroarch", "rocknix", "knulli"},
"rp5": {"rocknix", "knulli"},
"rog ally": {"retroarch", "emudeck", "retrobat", "bizhawk"},
"onion": {"retroarch"},
}
@@ -4387,7 +4393,7 @@ class AppOnTheCard(unittest.TestCase):
self.assertEqual(install.tui_rows(offline), [])
os.environ.pop(install.OFFLINE_VARIABLE)
_title, lines = install.tui_outcome(cases["no launcher"])
self.assertIn(install.NO_PYTHON_WORDS, lines)
self.assertIn(install.no_python_words("Onion"), lines)
def test_put_by_the_install_itself_no_second_offer(self):
root, bios = _app_card(self, "muOS")
@@ -4951,7 +4957,10 @@ 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 4 to install: 1 copied from this "
# The backup line under the count it explains, then the split
# (tui_audit_8 P3-5).
self.assertIn("1 in place, 3 missing, 1 wrong copy The 1 wrong copy is replaced, the "
"old one kept in a backup folder. 4 to install: 1 copied from this "
"machine, 3 downloaded", screen)
@@ -5390,7 +5399,7 @@ class AuditSix(unittest.TestCase):
self.assertEqual(args.dest, Path("/mnt/d"))
card = Path("/mnt/d")
with mock.patch.object(install, "_card_what", return_value="Onion card"):
self.assertEqual(install.card_line(card), "D: (Onion card, /mnt/d)")
self.assertEqual(install.card_line(card), "D: (Onion card)")
self.assertEqual(install._typed_folder("D:"), Path("D:"))
def test_a_dest_refused_on_the_setup_list_names_dest(self):
@@ -5407,8 +5416,10 @@ class AuditSix(unittest.TestCase):
self.assertEqual(install.tui_rows(state)[1].label, "Onion, SD card")
self.assertEqual(press(state, "enter"), [])
self.assertEqual(state.screen, "platforms")
self.assertIn("--dest names it: choose another setup.", state.status)
self.assertNotIn("Choose the card's top folder", state.status)
# --dest is fixed: a stick that is no card is named again in the
# command, another setup would not help (code_audit_15 P3-3).
self.assertIn("Run it again with the card's top folder.", state.status)
self.assertNotIn("choose another setup", state.status)
self.assertEqual((state.platform, state.card_bound), ("", None))
def test_on_the_device_a_folder_typed_is_taken_as_typed(self):
@@ -5780,7 +5791,7 @@ class AuditSevenOneCardReading(unittest.TestCase):
text = out.getvalue()
self.assertIn("Type its number or its top folder:", text)
self.assertNotIn("Mount the card", text)
self.assertIn(f"ONION2: Onion files go to {cards[1] / 'BIOS'}", text)
self.assertIn(f"Onion card ONION2, BIOS folder {cards[1] / 'BIOS'}", text)
self.assertEqual(install.detected_os("retroarch", dest), "Onion")
with mock.patch.object(install, "_ask", return_value=""), \
contextlib.redirect_stdout(io.StringIO()) as out:
@@ -6043,5 +6054,498 @@ class AuditSevenScreens(unittest.TestCase):
self.assertIn("1 wrong copy: backed up, replaced.", view)
class _Decided(Exception):
"""Raised where line mode has chosen its platforms and folders."""
def line_decision(args: argparse.Namespace, answer) -> tuple[list, str]:
"""(platforms, text) line mode reaches at a terminal for args: the run
stops where its platforms are known, before anything is read or written.
answer(prompt) answers each question."""
def stop(_args, platforms):
raise _Decided(platforms)
install.LOCAL.reset()
with mock.patch.object(install, "app_refusal", side_effect=stop), \
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", side_effect=answer), \
mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
contextlib.redirect_stdout(io.StringIO()) as out, \
contextlib.redirect_stderr(io.StringIO()):
try:
install._install(args, io.StringIO())
except _Decided as decided:
return decided.args[0], out.getvalue()
finally:
install.LOCAL.reset()
raise AssertionError("line mode chose nothing:\n" + out.getvalue())
def screen_decision(args: argparse.Namespace) -> tuple[str, Path]:
"""(platform, folder) the full screen reaches for args, A pressed on
whatever it offers first, as Enter is in line mode."""
install.LOCAL.reset()
with mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()):
try:
state, effects = install._tui_startup(args)
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()
raise AssertionError(f"the screen chose nothing: {state.stack}")
class LineModeAndScreenDecideAlike(unittest.TestCase):
"""tui_audit_8 P1-1, P2-1 and code_audit_15 P2-2: line mode looked at
this machine alone, took this computer's default folder with a card in
the reader or nothing found, and the screen put RetroArch under A for a
KNULLI card named with --dest. The same inputs now give the same
platform and folder on both, A or Enter taking what each offers first."""
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)
self.list = self.root / "retroarch.json"
self.list.write_text(json.dumps({"manifest_version": 2, "platform": "retroarch",
"files": []}))
self.here = self.root / "pc" / "system"
self.here.mkdir(parents=True)
def card(self, name: str, system: str) -> Path:
root = self.root / "media" / name
make_card(root, system)
return root
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 ""
extra = {"declared_only": False, "offline": False}
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
mock.patch.object(install, "detect_platforms", return_value=found), \
mock.patch.object(install, "detect_frontends", return_value=[]):
platforms, text = line_decision(_args(**extra, **given), answer)
install._DETECTED_OS.clear()
screen = screen_decision(_args(**extra, **given))
return platforms[0], screen, text
def test_the_same_scenarios_give_the_same_platform_and_folder(self):
onion = self.card("ONION", "Onion")
muos = self.card("MUOS", "muOS")
knulli = self.card("KNULLI", "KNULLI")
bare = self.card("ONIONB", "Onion")
shutil.rmtree(bare / "BIOS")
default = install._default_dest("linux", "retroarch")
pc = [("retroarch", self.here)]
scenarios = {
"a list, an Onion card in the reader": (
[onion], [], {"manifest": self.list}, ("retroarch", onion / "BIOS")),
"a list, an Onion card without its BIOS folder": (
[bare], [], {"manifest": self.list}, ("retroarch", bare / "BIOS")),
"a list, a KNULLI card in the reader": (
[knulli], [], {"manifest": self.list}, ("retroarch", default)),
"a list, nothing here": ([], [], {"manifest": self.list}, ("retroarch", default)),
"nothing named, two cards": (
[muos, onion], [], {}, ("retroarch", muos / "MUOS" / "bios")),
"nothing named, nothing here": ([], [], {}, ("retroarch", default)),
"--dest a KNULLI card, RetroArch here": (
[knulli], pc, {"dest": knulli}, ("knulli", knulli / "bios")),
"--platform onion, its card": (
[onion, muos], pc, {"platform": "onion"}, ("retroarch", onion / "BIOS")),
}
for name, (mounts, found, given, expected) in scenarios.items():
with self.subTest(scenario=name):
line, screen, text = self.both(mounts, found, **given)
self.assertEqual(tuple(line), expected, text)
self.assertEqual(screen, expected)
self.assertNotIn("using default path", text)
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]), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]):
_platforms, text = line_decision(_args(manifest=self.list, declared_only=False),
lambda prompt: "")
self.assertIn(f"Found Onion at {onion / 'BIOS'} (RetroArch files)", text)
self.assertNotIn("No supported platform detected", text)
def test_a_pack_gets_the_menu_and_its_folder_named(self):
"""P1-1: no menu and no folder before the copy, the pack's list
recording no system."""
from test_install_offline import _pack, PAYLOAD
downloads = self.root / "downloads"
downloads.mkdir()
pack = _pack(downloads, "RetroArch_v1_BIOS_Pack.zip", PAYLOAD)
install.LOCAL.reset()
self.addCleanup(install.LOCAL.reset)
menus = []
real = install._prompt_custom_selection
def menu(*given, **named):
with mock.patch.object(install, "_ask", return_value="q"):
try:
real(*given, **named)
except SystemExit as stop:
menus.append(stop.code)
raise
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=[]), \
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=str(self.here)), \
mock.patch.object(install, "_prompt_custom_selection", menu), \
contextlib.redirect_stdout(io.StringIO()) as out, \
self.assertRaises(SystemExit):
install._install(_args(from_pack=[pack], declared_only=False), io.StringIO())
text = out.getvalue()
self.assertEqual(menus, [130])
self.assertIn("RetroArch was not found here.", text)
self.assertIn(f"BIOS folder: {self.here}", text)
self.assertIn("Enter) install everything", text)
self.assertNotIn("c) choose systems", text)
self.assertNotIn("Copying", text)
class OneDeviceRegistry(unittest.TestCase):
"""code_audit_15 P2-1, tui_audit_8 P3-2, P3-3, P3-4: one device spelled
several ways found one system, one press took it, MinUI listed brands."""
def found(self, typed: str, os_type: str = "linux") -> set:
return {row.value[1] if row.action == "cardsystem" else row.value
for row in install.setup_list([], set(), typed, os_type)}
def test_one_spelling_per_device(self):
keys = {}
for name in install._DEVICES:
keys.setdefault(install._device_key(name), []).append(name)
self.assertEqual({key: names for key, names in keys.items() if len(names) > 1}, {})
for system, devices in install._SETUP_DEVICES.items():
with self.subTest(system=system):
self.assertEqual([name for name in devices if name not in install._DEVICES], [])
def test_every_spelling_finds_every_system_that_runs_it(self):
rk2023 = {"ArkOS", "KOLOS", "JELOS", "rocknix"}
cubexx = {"muOS", "knulli", "rocknix", "minui"}
flip2 = {"knulli", "rocknix"}
for typed, systems in (("Powkiddy RK2023", rk2023), ("RK2023", rk2023),
("rk-2023", rk2023), ("RG CubeXX", cubexx),
("RGCUBEXX", cubexx), ("Anbernic RG CubeXX", cubexx),
("Retroid Pocket Flip 2", flip2), ("flip2", flip2)):
with self.subTest(typed=typed):
self.assertEqual(self.found(typed), systems)
self.assertIn("ArkOS", self.found("Anbernic RG353V"))
self.assertTrue({"Onion", "minui", "myminui", "Allium"} <= self.found("Miyoo Mini"))
self.assertTrue({"Onion", "minui"} <= self.found("Miyoo Mini+"))
self.assertIn("minui", self.found("TrimUI Brick"))
self.assertIn("Anbernic stock firmware", self.found("rg35xx"))
self.assertIn("JELOS", self.found("rgb10 max 3"))
def test_android_is_this_machine_only_on_android(self):
self.assertNotIn("retroarch", self.found("android"))
self.assertNotIn("retroarch", self.found("retroid pocket 5"))
self.assertIn("retroarch", self.found("android", "android"))
def test_a_device_several_systems_run_is_never_taken_in_one_press(self):
out = io.StringIO()
with mock.patch.object(install, "_ask",
side_effect=["Powkiddy RK2023", "", "0", "35", "q"]), \
mock.patch.object(install, "detect_os", return_value="linux"), \
contextlib.redirect_stdout(out), self.assertRaises(SystemExit):
install._choose_platform("Which?", "retroarch")
text = out.getvalue()
self.assertIn("4 setups run on 'Powkiddy RK2023':", text)
# Enter after the matches takes none, and 0 and 35 are numbers.
self.assertEqual(text.count("Type a number from 1 to 34"), 3)
self.assertNotIn("run on '0'", text)
self.assertNotIn("run on '35'", text)
def test_the_line_menu_is_the_screen_list_numbered(self):
for setups in ([], [("retroarch", SYSTEM_DIR, "RetroArch")]):
with self.subTest(found=bool(setups)):
rows = install.setup_list(setups, set(), "", "linux")
self.assertEqual([name for name, _label in install._line_setups(setups)],
[install._row_name(row) for row in rows])
labels = dict(install._line_setups())
self.assertIn("RG28XX", labels["minui"])
self.assertNotIn("(Anbernic, Miyoo", labels["minui"])
self.assertIn("RG35XX Plus", labels["Anbernic stock firmware"])
self.assertNotIn("RetroArch files", labels["JELOS"])
def test_forty_columns_keep_the_name_whole_and_a_device_count(self):
rows = {row.label: row for row in install.setup_list([], set(), "", "linux")}
for label in ("KNULLI, SD card", "ROCKNIX, SD card", "CrossMix-OS, SD card",
"AmberELEC, SD card", "JELOS, SD card"):
with self.subTest(row=label):
line = str(install._row_line(rows[label], False, 38))
self.assertIn(label, line)
self.assertTrue(line.rstrip() != (" " + label).rstrip(), line)
self.assertTrue(str(install._row_line(rows["CrossMix-OS, SD card"], False, 38))
.rstrip().endswith("1 device"))
class AuditFifteenAndEightScreens(unittest.TestCase):
"""The P3 points of code_audit_15 and tui_audit_8 the screens show."""
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, "detect_platforms", return_value=list(found)), \
mock.patch.object(install, "detect_frontends", return_value=[]):
return install.tui_start(_args(**given), "linux")
def test_a_card_found_by_its_files_gets_its_folder_without_a_question(self):
"""tui_audit_8 P3-12: Create the folder? for the Onion card's own
BIOS, which line mode created."""
onion = self.root / "ONIONB"
make_card(onion, "Onion")
shutil.rmtree(onion / "BIOS")
state, effects = self.start([onion], platform="onion")
self.assertEqual(effects, [("fetch", "retroarch")])
self.assertEqual(state.dest, onion / "BIOS")
self.assertIn("BIOS is created on ONIONB", state.card_note)
def test_a_refused_dest_says_the_remedy_that_works(self):
"""code_audit_15 P3-3, P3-4."""
rocknix = self.root / "ROCKNIX"
make_card(rocknix, "ROCKNIX")
stick = self.root / "STICK"
(stick / "photos").mkdir(parents=True)
for named, plat, dest, words, never in (
("knulli", "knulli", rocknix / "roms" / "bios", "--platform rocknix", "<card>"),
("onion", "retroarch", stick, "Name the top folder of the Onion card",
"<card>/BIOS")):
with self.subTest(named=named):
with contextlib.redirect_stderr(io.StringIO()) as err, \
self.assertRaises(SystemExit):
install.card_dest(named, plat, dest)
self.assertIn(words, err.getvalue())
self.assertNotIn(never, err.getvalue())
minui = self.root / "MINUI"
make_card(minui, "MinUI")
(minui / "Bios").mkdir()
state = install.TuiState()
install._card_status(state, install.card_choice("minui", "minui", minui / "Bios"),
given=True)
self.assertIn(f"Run it again with --dest {minui}.", state.status)
def test_wsl_names_drives_in_line_mode_messages(self):
"""code_audit_15 P3-7: /mnt/f in a refusal where D:\\ was typed."""
mnt = self.root / "mnt"
make_card(mnt / "f", "muOS")
(mnt / "g" / "photos").mkdir(parents=True)
with mock.patch.dict(install._WSL, {"active": True, "root": str(mnt)}), \
mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
with contextlib.redirect_stderr(io.StringIO()) as err, \
self.assertRaises(SystemExit):
install.card_dest("onion", "retroarch", mnt / "f")
self.assertIn("F:\\ is a muOS card", err.getvalue())
note = install.card_choice("knulli", "knulli", mnt / "g").note
self.assertIn("G:\\bios", note)
self.assertNotIn(str(mnt), note)
self.assertEqual(install._last_name(str(mnt / "g" / "old")), "old")
def test_choose_the_card_below_only_when_one_is_mounted(self):
"""tui_audit_8 P3-6."""
muos = self.root / "MUOS"
make_card(muos, "muOS")
stick = self.root / "USBSTICK"
(stick / "photos").mkdir(parents=True)
with mock.patch.object(install, "_mounted_cards", return_value=[muos, stick]):
state, _effects = self.start([muos, stick], platform="onion")
rows = install.tui_rows(state)
state.focus["folder"] = next(index for index, row in enumerate(rows)
if row.action == "browse")
install.tui_update(state, Key("enter"))
self.assertEqual(state.screen, "browse")
self.assertIn("No Onion card is mounted", state.status)
focused = install.tui_rows(state)[state.focus["browse"]]
self.assertEqual(focused.value, stick)
def test_type_a_path_says_f1_and_f1_says_more(self):
"""tui_audit_8 P2-2."""
muos = self.root / "MUOS"
make_card(muos, "muOS")
with mock.patch.object(install, "_mounted_cards", return_value=[]):
state, _effects = self.start(platform="onion")
stick = self.root / "STICK"
stick.mkdir()
state.stack.append("path")
install._card_status(state, install.card_choice("Onion", "retroarch", stick))
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertIn("F1 says more.", text)
self.assertNotIn("? says more", text)
install.tui_update(state, Key("f1"))
self.assertTrue(state.help)
help_text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertIn("Onion keeps .tmp_update at the root of its card", help_text)
def test_batocera_on_a_computer_offers_its_share(self):
"""tui_audit_8 P2-4."""
stick = self.root / "SHARE"
stick.mkdir()
state, _effects = self.start([stick])
state.stack = ["platforms"]
state.focus["platforms"] = install._platform_focus(state, "batocera")
with mock.patch.object(install, "_mounted_cards", return_value=[stick]):
install.tui_update(state, Key("enter"))
rows = install.tui_rows(state)
text = " ".join(" ".join(install.tui_view(state, 80, 24)).split())
self.assertEqual(state.screen, "folder")
self.assertNotIn("/userdata/bios", text)
self.assertIn("SHARE drive", text)
self.assertEqual([row.action for row in rows], ["share", "browse", "type"])
self.assertEqual(rows[0].value, stick / "bios")
def test_the_first_screen_bar_says_quit(self):
"""tui_audit_8 P3-10."""
state, _effects = self.start()
self.assertEqual(state.stack, ["platforms"])
self.assertIn("B/Esc Quit", install._TUI_VIEWS["platforms"](state, 80, 24)[2])
state.stack = ["setup", "platforms"]
self.assertIn("B/Esc Back", install._TUI_VIEWS["platforms"](state, 80, 24)[2])
def test_a_pad_result_prints_no_command(self):
"""tui_audit_8 P3-9: a shell command on a controller-only screen."""
state = _installed_state(self.root / "bios")
state.summary["kept"] = [{"folder": str(self.root / "kept"), "files": 1}]
state.pad = "muxterm"
_title, lines = install.tui_outcome(state)
text = " ".join(lines)
self.assertNotIn("install.py", text)
self.assertIn("kept in", text)
state.pad = "keys"
self.assertIn("Same choice without questions:", install.tui_outcome(state)[1])
def test_the_checking_screen_is_titled_as_the_plan(self):
"""code_audit_15 P3-9.1."""
state = install.TuiState()
state.platform, state.dest = "retroarch", self.root / "bios"
install._DETECTED_OS[("retroarch", state.dest)] = "ArkOS"
state.stack = ["checking"]
text = " ".join(install.tui_view(state, 80, 24))
self.assertIn("ArkOS (RetroArch files):", text)
def test_the_backup_line_survives_forty_columns(self):
"""tui_audit_8 P3-5: under the pack and copy split it was cut."""
state = checked_state()
state.found = [found("gba_bios.bin", "/media/old")]
lines = [str(line) for line in install._plan_lines(state.check, state.dest, state)]
self.assertTrue(any("copied from this machine" in line for line in lines), lines)
counts = next(index for index, line in enumerate(lines) if " in place, " in line)
self.assertTrue(lines[counts + 1].startswith("The 1 wrong copy is replaced"), lines)
def test_an_empty_folder_is_said_once(self):
"""tui_audit_8 P3-7."""
old = self.root / "USBSTICK"
old.mkdir()
state = checked_state()
state.from_folders = [old]
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
lines = [str(line) for line in install._plan_lines(state.check, state.dest,
state)]
said = [line for line in lines if "USBSTICK" in line]
self.assertEqual(said, ["USBSTICK holds none of these files: run the installer again "
"with a pack (--from-pack) or without --offline."])
def test_amberelec_keeps_its_files_on_the_games_part(self):
"""tui_audit_8 P3-13."""
self.assertEqual(install.card_where("AmberELEC", ""),
"on the games part of its SD card")
files = [{"dest": "bios/a.bin"}, {"dest": "bios/Machines/b.xml"}]
self.assertEqual(install.shown_bios_folder(self.root, files), self.root / "bios")
self.assertEqual(install.shown_bios_folder(self.root, [{"dest": "a.bin"}]),
self.root)
def test_a_save_directory_left_out_has_one_wording(self):
"""code_audit_15 P3-9.3, tui_audit_8 P3-8."""
files = [{"dest": "keys.txt", "root": "save", "sha1": "0" * 40, "size": 1}]
with contextlib.redirect_stdout(io.StringIO()) as out:
install.place_save_entries(files, self.root / "BIOS",
{"platform": "retroarch", "save_directory": {
"folder": "saves", "by_core": False},
"base_destination": "system"},
None, "RetroArch")
self.assertIn("left out: its place is not known. Name it with --save-dir.",
out.getvalue())
class CardBoundaries(unittest.TestCase):
"""code_audit_15 P3-2, P3-9.5: three folders at most, a bind mount not
seen, a folder named MUOS taken for a card, ismount unprobed."""
def setUp(self):
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
def choice(self, folder: Path, points=frozenset()) -> install.CardChoice:
with mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "_mount_points", return_value=points):
return install.card_choice("knulli", "knulli", folder)
def test_every_folder_up_to_the_mount_is_read(self):
muos = self.root / "MUOS"
make_card(muos, "muOS")
deep = muos / "a" / "b" / "c" / "d"
deep.mkdir(parents=True)
self.assertIn("muOS", self.choice(deep).refused)
def test_a_bind_mount_inside_a_card_is_its_own_drive(self):
muos = self.root / "MUOS"
make_card(muos, "muOS")
stick = muos / "x"
stick.mkdir()
self.assertTrue(self.choice(stick).refused)
choice = self.choice(stick, frozenset({str(stick)}))
self.assertEqual((choice.refused, choice.folder), ("", stick / "bios"))
def test_a_folder_named_muos_beside_it_is_no_card(self):
backups = self.root / "stage" / "backups"
(backups / "MUOS").mkdir(parents=True)
(backups / "knulli-new").mkdir()
choice = self.choice(backups / "knulli-new")
self.assertEqual(choice.refused, "")
self.assertEqual(choice.folder, backups / "knulli-new" / "bios")
def test_mount_points_are_probed(self):
with mock.patch.object(install, "_probed", wraps=install._probed) as probed, \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
mock.patch.object(install, "_mounted_cards", return_value=[]):
install._card_boundary(self.root)
self.assertTrue(probed.called)
points = install._mount_points()
if Path("/proc/self/mountinfo").exists():
self.assertIn("/", points)
if __name__ == "__main__":
unittest.main()