fix: a games part is its system's card

Closes every finding of code_audit_23 and tui_audit_16: AmberELEC and
EmuELEC cards found as theirs, ArkOS cards told by bgmusic or
launchimages, counts that agree and read a copy of the right size before
calling it in place, one terminal width, line mode in the screen's order
and words, a tap shown armed, and which-pack naming the first screen.
This commit is contained in:
Abdessamad Derraz committed 2026-10-28 16:34:55 +01:00
1 parent 27b945ba4c
commit a4faf536dd
12 files changed
+1257 -254

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "665534440e8b79c9643e6dc176c4a03da1ec497f151eb33fa0b9d4fb44809511"
$defaultInstallSha256 = "86f11e91316119889fda8a2baa228a9b7be1c1d3fd29780473a59e412e362c7d"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+543 -172
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="665534440e8b79c9643e6dc176c4a03da1ec497f151eb33fa0b9d4fb44809511"
DEFAULT_INSTALL_SHA256="86f11e91316119889fda8a2baa228a9b7be1c1d3fd29780473a59e412e362c7d"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 961225,
"sha256": "665534440e8b79c9643e6dc176c4a03da1ec497f151eb33fa0b9d4fb44809511",
"size": 981280,
"sha256": "86f11e91316119889fda8a2baa228a9b7be1c1d3fd29780473a59e412e362c7d",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1014402,
"sha256": "cdd06d24d30ad6b564d8a1595b906d50ce05d23093121cc7e5d91b5acd8e08ab"
"size": 1034457,
"sha256": "92d78b81459583ea9ac00546e28f3118e815ac302c0f180e882b44a16e5bcb53"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 961225,
"sha256": "665534440e8b79c9643e6dc176c4a03da1ec497f151eb33fa0b9d4fb44809511",
"size": 981280,
"sha256": "86f11e91316119889fda8a2baa228a9b7be1c1d3fd29780473a59e412e362c7d",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 961225,
"sha256": "665534440e8b79c9643e6dc176c4a03da1ec497f151eb33fa0b9d4fb44809511",
"size": 981280,
"sha256": "86f11e91316119889fda8a2baa228a9b7be1c1d3fd29780473a59e412e362c7d",
"executable": false
},
{
+9 -3
View File
@@ -161,8 +161,13 @@ def _marker(rule: _install._OsRule) -> str:
path, pattern = rule.values
line = re.sub(r"\\s\*\$$", "", re.sub(r"^(\^|\\A)", "", pattern)).replace("\\", "")
return f"`{path}` holding the line `{line}`"
return ", ".join(f"`{value}`" if PurePosixPath(value).suffix else f"`{value}/`"
for value in rule.values)
named = ", ".join(f"`{value}`" if PurePosixPath(value).suffix else f"`{value}/`"
for value in rule.values)
if rule.method == "games_part":
# Told on the games part beside its bios folder, on a drive the
# system does not call fixed (install.games_part).
return f"{named} beside `bios/`, on a drive that is not a fixed disk"
return named
def _list_words(plat: str) -> str:
@@ -173,7 +178,8 @@ def _list_words(plat: str) -> str:
def card_marker_rows() -> list[list[str]]:
"""The files that name a card or a SHARE drive, in the order they are
read: install.py's card rules, then its SHARE drive files."""
rows = [[_marker(rule), rule.name, f"`{rule.bios}`, {_list_words(rule.manifest)}"]
rows = [[_marker(rule), rule.name,
f"`{rule.bios or 'bios'}`, {_list_words(rule.manifest)}"]
for rule in _install._card_rules()]
hostname = "system.hostname=BATOCERA"
for plat, marker in _install._SHARE_DRIVES:
+7 -2
View File
@@ -3940,8 +3940,13 @@ def first_screen_words() -> list[str]:
showed a line menu (Enter, i, c, q) nobody saw, whose keys had changed
since."""
installer = _installer()
return [f"The command opens a screen. **{installer.TUI_INSTALL_ROW}** gets every "
f"file, **{installer.TUI_SYSTEMS_ROW}** fewer."]
# The screen it opens first: the setup found, or the list when none
# is. Install and Choose systems come on the next one (tui_audit_16
# P3-10).
return [f"The command asks **{installer.TUI_FOUND_TITLE}** with the setup it found, "
f"or **{installer.TUI_CHOOSE_TITLE}** with a list when it finds none. Then "
f"**{installer.TUI_INSTALL_ROW}** gets every file, "
f"**{installer.TUI_SYSTEMS_ROW}** only the consoles you pick."]
def check_pass_line() -> str:
+10 -6
View File
@@ -90,12 +90,15 @@ class InstallerOutputExample(unittest.TestCase):
intro, run = re.search(r"(?s)\n([^\n]*)\n\n```\n(Fetching file index.*?)```", text).groups()
self.assertRegex(intro, r"\d{4}-\d{2}-\d{2}", "an output example says when it was taken")
manifest = json.loads((ROOT / "install" / "retroarch.json").read_text(encoding="utf-8"))
omitted = re.search(r"Not installable: \d+ files? \(\d+ required, (.*)\)\.", run)
# The line as the installer writes it (not_installable_words), or
# its earlier "(1 required, not found: 37)" form.
omitted = re.search(r"Not installable: \d+ files? (?:\(\d+ required, (.*)\)\.|(?:is|are) "
r"not in the collection|.*Why: (.*)\.)", run)
if manifest.get("omitted_files"):
self.assertIsNotNone(omitted, "the manifest omits files and the run shows no line for it")
if omitted:
for part in omitted.group(1).split(", "):
self.assertIn(part.rsplit(":", 1)[0], install.OMISSION_REASONS.values())
if omitted and (omitted.group(1) or omitted.group(2)):
for part in (omitted.group(1) or omitted.group(2)).split(", "):
self.assertIn(re.split(r":| \(", part)[0], install.OMISSION_REASONS.values())
count, size = re.search(r"\n (\d+) files \(([\d.]+ [KMG]B)\)", run).groups()
self.assertIn(f"0 in place, {count} missing", run)
self.assertSplitAddsUp(run, count, size)
@@ -772,11 +775,12 @@ class FirstInstallWalkthrough(unittest.TestCase):
self.assertIn(f"Done. {count} files installed", step4)
self.assertIn(f"{count} in place, 0 missing", step5)
manifest = json.loads((ROOT / "install" / "batocera.json").read_text(encoding="utf-8"))
omitted = re.search(r"Not installable: (\d+) files \((\d+) required", step3)
omitted = re.search(r"Not installable: (\d+) files (?:\((\d+) required|.*?, (\d+) of them "
r"required)", step3)
if manifest.get("omitted_files"):
self.assertIsNotNone(omitted, "the manifest omits files and the run shows no line for it")
if omitted:
total, required = map(int, omitted.groups())
total, required = int(omitted.group(1)), int(omitted.group(2) or omitted.group(3))
first, needed = install._omission_lines(
total, [{"required": True, "dest": str(i)} for i in range(required)]
)[:2]
+21 -1
View File
@@ -1806,7 +1806,27 @@ class TestDetectedPlatformPrompt(unittest.TestCase):
class TestManualPlatformPrompt(unittest.TestCase):
"""With nothing detected, an interactive run asks instead of exiting."""
"""With nothing detected, an interactive run asks instead of exiting.
Every test runs on no drive and a home of its own: two read the host's
drives and home, and failed with a card in the reader (code_audit_23
P3-7)."""
def setUp(self):
home = Path(_tempdir(self))
for patcher in (unittest.mock.patch.object(install, "_mounted_cards", return_value=[]),
unittest.mock.patch.object(install, "_mount_points",
return_value=frozenset()),
unittest.mock.patch.object(install.Path, "home", return_value=home),
unittest.mock.patch.dict(os.environ, {"HOME": str(home)}),
unittest.mock.patch("sys.stdout", io.StringIO())):
patcher.start()
self.addCleanup(patcher.stop)
def test_the_host_is_not_read(self):
self.assertEqual(install._mounted_cards(), [])
self.assertEqual(install._default_dest("linux", "retroarch"),
Path.home() / ".config" / "retroarch" / "system")
def test_number_picks_the_platform_and_default_directory(self):
answers = iter(["1", ""])
+6 -4
View File
@@ -234,15 +234,17 @@ class CheckNamesWhyAFileIsRefused(Scratch):
self.complete("scph5501.bin", "pcsx/extra.bin")
out = self.check()
here = self.dest.resolve()
self.assertIn(f"opened at {here / 'scph5501.bin'} by beetle_psx, pcsx_rearmed", out)
self.assertIn(f"opened at {here / 'pcsx' / 'extra.bin'} by beetle_psx, pcsx_rearmed",
self.assertIn(f"read by beetle_psx, pcsx_rearmed at {here / 'scph5501.bin'}", out)
self.assertIn(f"read by beetle_psx, pcsx_rearmed at {here / 'pcsx' / 'extra.bin'}",
out)
# Never "opened" of a file that is not there (tui_audit_16 P3-12).
self.assertNotIn("opened at", out)
def test_a_complete_folder_says_nothing_more(self):
self.complete()
proc = self.run_installer("--check", "--manifest", str(self.list))
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertNotIn("opened at", proc.stdout)
self.assertNotIn("read by", proc.stdout)
class ArchivesInTheBiosFolder(Scratch):
@@ -845,7 +847,7 @@ class OfflineLeavesFilesMissing(Scratch):
proc = self.run_installer("--offline", "--manifest", str(self.list), "--no-input")
# Its files are missing, as the BIOS files --offline leaves out are,
# with or without questions.
self.assertIn("bluemsx: 0 already in place, 1 need the internet (--offline)",
self.assertIn("bluemsx: 0 already in place, 1 needs the internet (--offline)",
proc.stdout)
self.assertNotIn("could not read", proc.stderr)
self.assertNotIn("no longer in the source", proc.stdout)
+624 -40
View File
@@ -2537,10 +2537,11 @@ class AuditRoundThree(unittest.TestCase):
state.summary = {"installed": 4, "up_to_date": 0, "failed": [
{"dest": "fc/disksys.rom", "reason": "HTTP 404"}]}
_title, lines = install.tui_outcome(state)
self.assertEqual(lines[1], "2 files copied from OLD, 1 copied from the wrong place "
"in the BIOS folder, 1 downloaded, 1 still missing.")
# One count, the failed file said as failed (tui_audit_16 P3-11).
self.assertEqual(lines[0], "2 files copied from OLD, 1 copied from the wrong place "
"in the BIOS folder, 1 downloaded, 1 failed.")
# The line mode's own sentence, from the same counts.
self.assertEqual(lines[1], install.sources_line([("OLD", 2)], 1, 1, 1))
self.assertEqual(lines[0], install.sources_line([("OLD", 2)], 1, 1, 0, failed=1))
def test_p2_3_a_run_that_copied_nothing_adds_no_line(self):
state = install.TuiState()
@@ -5100,9 +5101,13 @@ class EndToEnd(unittest.TestCase):
term = self.start()
term.wait("Enter for RetroArch")
term.send("\r")
term.wait("Enter) install everything")
term.send("\r")
# Installed by the first run, in place for the second.
if value is None:
# In place since the first run: no menu, nothing asked
# (tui_audit_16 P3-4).
term.wait("every one in place: nothing to install")
else:
term.wait("Enter) install everything")
term.send("\r")
term.wait("Done.")
term.close()
self.assertNotIn(b"\x1b", term.raw)
@@ -6411,7 +6416,11 @@ class AuditSevenOneCardReading(unittest.TestCase):
stick = self.blank(f"{name}STICK")
choice = install.card_choice(plat, plat, stick)
self.assertEqual(choice.folder, stick)
self.assertIn("cannot tell", choice.note)
# Its card is told by what it writes at every boot: a stick
# holding none of it is one not started yet, never "cannot
# tell" (tui_audit_16 P2-1).
self.assertIn(f"says it is {install._a(name)} card yet", choice.note)
self.assertNotIn("cannot tell", choice.note)
def test_install_py_has_two_blank_lines_around_each_definition(self):
"""code_audit_14 P3-12: E305 after _card_mount, and the same slip
@@ -7438,8 +7447,9 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings,
self.assertRaises(SystemExit):
install.ask_platform_folder("linux", "retroarch")
text = out.getvalue()
self.assertIn(f"1) EASYROMS: bios (card or drive) ({stick / 'bios'})", text)
self.assertIn(f"2) ONION: BIOS (Onion SD card) ({onion / 'BIOS'})", text)
# One parenthesis a row (tui_audit_16 P3-9).
self.assertIn(f"1) EASYROMS: bios (drive, {stick / 'bios'})", text)
self.assertIn(f"2) ONION: BIOS (Onion SD card, {onion / 'BIOS'})", text)
self.assertNotIn("3)", text)
self.assertIn("Not offered here, each reads its own system's files: SHARE (Batocera "
f"SHARE drive: --platform batocera --dest {share}), KNULLI (KNULLI SD "
@@ -8259,7 +8269,9 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings,
found here.\""""
text, _asked = self.line_run(_args(declared_only=False), [])
self.assertIn("Looking for setups here...", text)
self.assertIn("Nothing was found here. Which setup?", text)
# Said once, the question alone after it (tui_audit_16 P3-2).
self.assertEqual(text.count("Nothing was found here."), 1)
self.assertIn("\nWhich setup?\n", text)
self.assertNotIn("Detecting platform", text)
self.assertNotIn("No supported platform", text)
state = install.TuiState(os_type="linux", home=HOME)
@@ -11633,7 +11645,10 @@ class AuditTwentyAndThirteen(unittest.TestCase):
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE:
install.OFFLINE_FLAG_VALUE}):
_title, lines = install.tui_outcome(state)
self.assertIn("Still missing: 2 BIOS files and 1 game data file.", lines)
# The wrong copy the check found is left in place: named as one,
# never counted as missing (code_audit_23 P3-3).
self.assertIn("1 file copied from OLD, 2 still missing, 1 wrong copy left.", lines)
self.assertIn("Still missing: 1 BIOS file and 1 game data file.", lines)
# Root cause 6: every other word.
@@ -12278,15 +12293,16 @@ class AuditTwentyOneAndFourteen(unittest.TestCase):
def easyroms(self, name: str = "EASYROMS") -> Path:
"""A handheld's games part as a computer mounts it, with the folders
its system writes there (install._GAMES_PART_MARKERS): ArkOS's
its system writes there (install._GAMES_PART_RULES): ArkOS's
EASYROMS, AmberELEC's GAMES."""
return games_part(self.root / "media" / name)
def test_an_arkos_card_is_found_and_comes_first(self):
"""tui_audit_14 P2-3: an EASYROMS partition mounted, "Nothing was
found here", and two presses of Enter filled this computer's
~/.config/retroarch/system for a RetroArch that was not there."""
for name in ("EASYROMS", "GAMES"):
~/.config/retroarch/system for a RetroArch that was not there. An
AmberELEC GAMES is AmberELEC's own card (tui_audit_16 P2-1)."""
for name in ("EASYROMS",):
with self.subTest(card=name):
card = self.easyroms(name)
home = self.root / "home"
@@ -12562,15 +12578,14 @@ class AuditTwentyOneAndFourteen(unittest.TestCase):
text = out.getvalue()
self.assertNotIn(", created)", text)
self.assertNotIn("(created)", text)
self.assertIn(f"D: bios (card or drive) ({drives[0] / 'bios'}, does not exist yet)", text)
self.assertIn(f"E: bios (card or drive) ({drives[1] / 'bios'})\n", text)
self.assertIn(f"D: bios (drive, {drives[0] / 'bios'}, does not exist yet)", text)
self.assertIn(f"E: bios (drive, {drives[1] / 'bios'})\n", text)
state = fresh_state()
state.platform = "retroarch"
with mock.patch.object(install, "_mounted_cards", return_value=drives):
install._open_folder_screen(state, SYSTEM_DIR)
labels = [row.label for row in install.tui_rows(state) if row.action == "drive"]
self.assertEqual(labels, ["D: bios (card or drive) (does not exist yet)",
"E: bios (card or drive)"])
self.assertEqual(labels, ["D: bios (drive, does not exist yet)", "E: bios (drive)"])
def test_line_mode_puts_the_arkos_card_first(self):
card = self.easyroms()
@@ -12578,9 +12593,9 @@ class AuditTwentyOneAndFourteen(unittest.TestCase):
declared_only=False), ["1", ""], mounts=[card])
self.assertIn("A card is in the reader (EASYROMS): choose its system.", text)
self.assertNotIn("Nothing was found here", text)
self.assertIn(f"The card in the reader keeps them in its bios folder: {card / 'bios'}.",
text)
self.assertIn(f"1) EASYROMS: bios (card or drive) ({card / 'bios'})", text)
self.assertIn(f"The card in the reader keeps BIOS files in its bios folder: "
f"{card / 'bios'}.", text)
self.assertIn(f"1) EASYROMS: bios (SD card, {card / 'bios'})", text)
self.assertIn("Enter for the card's bios folder", asked[-2])
self.assertIn(f"BIOS folder: {card / 'bios'}", text)
@@ -12643,20 +12658,26 @@ class AuditTwentyTwoAndFifteen(unittest.TestCase):
self.assertIn("Nothing was found here", self.screen(state))
state, text, rows = self.folder_screen(mounts)
self.assertNotIn(rows[0].value, (data / "bios", stick / "bios"))
self.assertIn("DATA, STICK have a bios folder, and nothing on them shows a "
"handheld's card: listed, not chosen.", text)
self.assertIn(self.UNPROVEN_TWO, text)
self.assertIn(data / "bios", [row.value for row in rows])
text, asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True,
declared_only=False), ["1", ""], mounts=mounts)
self.assertNotIn("in the reader", text)
self.assertIn("DATA, STICK have a bios folder, and nothing on them shows a "
"handheld's card: listed, not chosen.", text)
self.assertIn(self.UNPROVEN_TWO, text)
self.assertIn("Enter for the default above", asked[-2])
self.assertNotIn(f"BIOS folder: {data / 'bios'}", text)
# The note on drives with a bios folder and no handheld's folder.
UNPROVEN_TWO = ("DATA and STICK have a bios folder but no folder a handheld system "
"writes. Pick one below if your files go there.")
UNPROVEN_ONE = ("DATA has a bios folder but no folder a handheld system writes. Pick "
"it below if your files go there.")
def test_the_layouts_handhelds_write_are_cards(self):
"""ArkOS's EASYROMS, AmberELEC's GAMES and EmuELEC's EEROMS, each
with the folders its own image writes there."""
with the folders its own image writes there. ArkOS's reads
RetroArch's list, the two others are their systems' own cards
(tui_audit_16 P2-1)."""
arkos = games_part(self.root / "media" / "EASYROMS")
amber = games_part(self.root / "media" / "GAMES")
emuelec = self.drive("EEROMS", ("bios", "nes", "bezels"))
@@ -12664,12 +12685,17 @@ class AuditTwentyTwoAndFifteen(unittest.TestCase):
mounts = [arkos, amber, emuelec]
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
self.assertEqual(install.unnamed_cards(), mounts)
self.assertEqual([install._games_part_system(mount) for mount in mounts],
self.assertEqual(install.unnamed_cards(), [arkos])
self.assertEqual([install.games_part(mount).name for mount in mounts],
["ArkOS", "AmberELEC", "EmuELEC"])
self.assertEqual([install.card_system(mount) for mount in mounts],
[None, ("amberelec", amber, "AmberELEC"),
("emuelec", emuelec, "EmuELEC")])
# One of these the system calls fixed is a disk, not a card.
with mock.patch.object(install, "_drive_not_removable", return_value=True):
self.assertEqual(install.unnamed_cards(), [])
self.assertEqual([install.card_system(mount) for mount in mounts],
[None, None, None])
def test_a_card_beside_a_data_drive_comes_first(self):
"""code_audit_22 P2-1: the ArkOS card and the data drive mounted,
@@ -12683,31 +12709,31 @@ class AuditTwentyTwoAndFifteen(unittest.TestCase):
self.assertIn("A card is in the reader (EASYROMS): choose its system", self.screen(state))
state, text, rows = self.folder_screen(mounts)
self.assertEqual(rows[0].value, card / "bios")
self.assertIn("DATA has a bios folder, and nothing on it shows a handheld's card", text)
self.assertIn(self.UNPROVEN_ONE, text)
text, asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True,
declared_only=False), ["1", ""], mounts=mounts)
self.assertIn("A card is in the reader (EASYROMS): choose its system.", text)
self.assertIn(f"The card in the reader keeps them in its bios folder: {card / 'bios'}.",
text)
self.assertIn(f"1) EASYROMS: bios (card or drive) ({card / 'bios'})", text)
self.assertIn("DATA has a bios folder, and nothing on it shows a handheld's card", text)
self.assertIn(f"The card in the reader keeps BIOS files in its bios folder: "
f"{card / 'bios'}.", text)
self.assertIn(f"1) EASYROMS: bios (SD card, {card / 'bios'})", text)
self.assertIn(self.UNPROVEN_ONE, text)
self.assertIn(f"BIOS folder: {card / 'bios'}", text)
def test_two_cards_are_spoken_of_in_the_plural(self):
"""code_audit_22 P3-2: "Cards are in the reader (DATA, EASYROMS):
choose its system", then "The card in the reader" of one of two."""
arkos = games_part(self.root / "media" / "EASYROMS")
amber = games_part(self.root / "media" / "GAMES")
mounts = [arkos, amber]
second = games_part(self.root / "media" / "ROMS2")
mounts = [arkos, second]
state = self.first_screen(mounts)
self.assertIn("Cards are in the reader (EASYROMS, GAMES): choose their systems",
self.assertIn("Cards are in the reader (EASYROMS, ROMS2): choose their systems",
self.screen(state))
state, text, rows = self.folder_screen(mounts)
self.assertIn("EASYROMS keeps them in its bios folder, first below.", text)
self.assertIn("EASYROMS keeps BIOS files in its bios folder, first below.", text)
text, asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True,
declared_only=False), ["1", ""], mounts=mounts)
self.assertIn("Cards are in the reader (EASYROMS, GAMES): choose their systems.", text)
self.assertIn(f"EASYROMS keeps them in its bios folder: {arkos / 'bios'}.", text)
self.assertIn("Cards are in the reader (EASYROMS, ROMS2): choose their systems.", text)
self.assertIn(f"EASYROMS keeps BIOS files in its bios folder: {arkos / 'bios'}.", text)
self.assertNotIn("The card in the reader", text)
def test_a_fixed_partition_is_told_from_a_card(self):
@@ -13280,8 +13306,16 @@ class TheSystemsQuestionFits(unittest.TestCase):
[f for f in files if install._chosen(f, {system})]))
# One line a system, a second for its shared files: the
# 80-column question took 127 lines for 66 systems.
limit = len(systems) + shared + 10 if cols == 80 else 2 * len(systems) + 12
# At 40 columns a shared size takes a line more: its first
# part alone was printed beside a long name, the shared
# files unsaid (tui_audit_16 P3-5).
limit = (len(systems) + shared + 10 if cols == 80
else 2 * len(systems) + shared + 12)
self.assertLessEqual(len(lines), limit)
for line, after in zip(lines, lines[1:] + [""]):
if line.rstrip().endswith(","):
self.assertTrue(after.startswith(" ") and not re.match(
r" +\d+\) ", after), (cols, line, after))
def test_a_row_that_fits_keeps_one_line(self):
lines, _systems, _files = self.question(80)
@@ -13290,5 +13324,555 @@ class TheSystemsQuestionFits(unittest.TestCase):
self.assertTrue(re.match(r" +\d+\) ", lines[row + 1]))
class AuditTwentyThreeAndSixteen(unittest.TestCase):
"""code_audit_23 and tui_audit_16: a card its markers prove is that
system's card everywhere, counts and states that tell the truth, one
width for every layout, line mode's order and words as the screen's,
and a tap that shows it was seen."""
setUp = LineModeAndScreenDecideAlike.setUp
line_run = LineModeAndScreenDecideAlike.line_run
listing = LineModeAndScreenDecideAlike.listing
entry = staticmethod(LineModeAndScreenDecideAlike.entry)
both = LineModeAndScreenDecideAlike.both
drive = AuditTwentyTwoAndFifteen.drive
first_screen = AuditTwentyTwoAndFifteen.first_screen
folder_screen = AuditTwentyTwoAndFifteen.folder_screen
game_listing = AuditTwentyOneAndFourteen.game_listing
game_manifest = AuditTwentyAndThirteen.game_manifest
receive = staticmethod(AuditTwentyAndThirteen.receive)
screen = staticmethod(AuditTwentyOneAndFourteen.screen)
def amberelec(self, name: str = "GAMES") -> Path:
return games_part(self.root / "media" / name)
def emuelec(self, name: str = "EEROMS") -> Path:
card = self.drive(name, ("bios", "nes", "bezels"))
(card / "bezels" / "default.cfg").write_text("")
return card
def line_from(self, args: argparse.Namespace, answers: list,
folders: list[Path] = ()) -> tuple[str, list]:
"""(text, prompts) of line mode at a terminal, the result included,
the folders --from names read first, as main() reads them."""
given, asked = iter(answers), []
def answer(prompt: str) -> str:
asked.append(prompt)
try:
return next(given)
except StopIteration:
raise _Decided(prompt) from None
install.LOCAL.reset()
self.addCleanup(install.LOCAL.reset)
if folders:
setattr(args, "from", [str(folder) for folder in folders])
invocation = install.Invocation()
invocation.os_type = "linux"
invocation.record(args)
stream = io.StringIO()
with mock.patch.object(install, "_INVOCATION", invocation), \
mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
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", side_effect=answer), \
contextlib.redirect_stdout(io.StringIO()) as out, \
contextlib.redirect_stderr(io.StringIO()) as err, \
contextlib.suppress(_Decided, SystemExit):
install._install(args, stream)
return out.getvalue() + err.getvalue() + stream.getvalue(), asked
# Root cause 1: a card its markers prove is that system's card.
def test_a_games_part_is_its_systems_card_in_both_modes(self):
"""tui_audit_16 P2-1: the GAMES card AmberELEC's folders prove, and
the EEROMS card EmuELEC's prove, read "A card is in the reader:
choose its system" with RetroArch focused, and two presses of Enter
put RetroArch's list on them."""
for plat, name, make in (("amberelec", "AmberELEC", self.amberelec),
("emuelec", "EmuELEC", self.emuelec)):
with self.subTest(system=name):
card = make()
state = self.first_screen([card])
text = self.screen(state)
self.assertIn(f"{name} SD card ({card.name})", text)
self.assertNotIn("in the reader", text)
self.assertIn(f"BIOSfolder:{card / 'bios'}", "".join(text.split()))
(line, screen, said) = self.both([card], [])
self.assertEqual(line, (plat, card))
self.assertEqual(screen, (plat, card))
self.assertIn(f"Found {name}", said)
shutil.rmtree(card)
def test_their_own_routes_take_the_card_they_prove(self):
"""tui_audit_16 P2-1: --platform amberelec said it "cannot tell an
AmberELEC card by its files" of the card holding gamedata/ and
homebrew/, and line mode offered no Enter for it."""
for plat, name, make in (("amberelec", "AmberELEC", self.amberelec),
("emuelec", "EmuELEC", self.emuelec)):
with self.subTest(system=name):
card = make()
stick = self.drive(f"{name}STICK", ("bios",))
with mock.patch.object(install, "_mounted_cards", return_value=[card, stick]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
self.assertEqual(install.mounted_cards_of(name, plat), [card])
self.assertEqual(install.blank_cards_of(name, plat), [stick])
choice = install.card_choice(name, plat, card)
self.assertEqual((choice.refused, choice.folder), ("", card))
self.assertNotIn("cannot tell", choice.note)
(line, screen, said) = self.both([card, stick], [], platform=plat)
self.assertEqual(line, (plat, card))
self.assertEqual(screen, (plat, card))
self.assertIn(f"Found {name} at {card / 'bios'}", said)
self.assertNotIn("cannot tell", said)
# Asked for its card, the one its files prove is offered.
with mock.patch.object(install, "_mounted_cards", return_value=[card, stick]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
contextlib.redirect_stdout(io.StringIO()) as out:
dest = install.card_install_dest(name, plat, True, False)
self.assertEqual(dest, card)
self.assertIn(f"{name} card found at {card.name}", out.getvalue())
with mock.patch.object(install, "_mounted_cards", return_value=[card, stick]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
state = screen_keys(_args(platform=plat, declared_only=False), [])
view = " ".join(install.tui_view(state, 80, 24))
self.assertNotIn("cannot tell", view)
shutil.rmtree(card)
shutil.rmtree(stick)
def test_an_arkos_card_from_any_year_is_a_card(self):
"""code_audit_23 P3-1: ArkOS's default trees of 2020 hold no themes/
(9664c869 none of themes and launchimages, 495eb210 and c14afbe5
launchimages only), and the 12262020 update skipped the themes of a
card with under 1 GB free. Every tree holds bgmusic/. A data drive
holding neither stays no card."""
layouts = {
"ARK_9664c869": ("bios", "bgmusic", "ports", "snes", "System Volume Information"),
"ARK_495eb210": ("bios", "bgmusic", "launchimages", "ports", "snes"),
"ARK_c14afbe5": ("bios", "bgmusic", "launchimages", "psx"),
"ARK_f41b9485": ("bios", "bgmusic", "launchimages", "themes", "backup", "nes"),
"ARK_FULL1226": ("bios", "launchimages", "nes"),
}
cards = [self.drive(name, folders) for name, folders in layouts.items()]
data = self.drive("DATA", ("bios", "snes", "Music", "themes"))
music = self.drive("MUSIC", ("bgmusic", "launchimages"))
mounts = [*cards, data, music]
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
self.assertEqual(install.unnamed_cards(), cards)
self.assertEqual([install.games_part(card).name for card in cards],
["ArkOS"] * len(cards))
self.assertIsNone(install.games_part(data))
self.assertIsNone(install.games_part(music))
self.assertEqual(install.unproven_drives(), [data])
with mock.patch.object(install, "_drive_not_removable", return_value=True):
self.assertEqual(install.unnamed_cards(), [])
# Root cause 2: counts and states that tell the truth.
def wrong_copy_run(self, answers: list) -> tuple[str, list, Path]:
"""An offline run whose BIOS folder holds a wrong copy of the right
size of a.bin, a folder named with --from holding b.bin."""
good_a, good_b = b"A" * 64, b"B" * 64
listing = self.listing("retroarch", [self.entry("a.bin", good_a, ["nintendo-gba"]),
self.entry("b.bin", good_b, ["nintendo-gba"])])
dest = self.root / "dest"
dest.mkdir()
(dest / "a.bin").write_bytes(b"Z" * 64)
old = self.root / "old"
old.mkdir()
(old / "b.bin").write_bytes(good_b)
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
text, asked = self.line_from(_args(manifest=listing, dest=dest, platform="retroarch",
declared_only=False), answers, [old])
return text, asked, dest
def test_the_head_never_calls_a_wrong_copy_in_place(self):
"""code_audit_23 P3-2: a wrong copy of the right size was in place
for the head, which said "1 found in old, nothing to download" over
a run that left it."""
text, _asked, dest = self.wrong_copy_run([""])
self.assertIn("RetroArch: 1 found in old, 1 needs the internet.", text)
self.assertNotIn("nothing to download", text)
self.assertEqual((dest / "a.bin").read_bytes(), b"Z" * 64)
def test_a_wrong_copy_left_is_said_as_one(self):
"""code_audit_23 P3-3: the result said "1 still missing" of a file
--check counts as a wrong copy."""
text, _asked, _dest = self.wrong_copy_run([""])
self.assertIn("Finished with files missing.", text)
self.assertIn("1 file copied from", text)
self.assertIn(", 1 wrong copy left.", text)
self.assertNotIn("still missing", text.rsplit("Finished", 1)[1])
def test_a_count_agrees_with_its_verb(self):
"""code_audit_23 P3-4: "1 need the internet" on the plan, and on
line mode's data tree line."""
self.assertEqual(install.agree(1, "needs", "need"), "1 needs")
self.assertEqual(install.agree(2, "needs", "need"), "2 need")
self.assertEqual(install._plural(2, "wrong copy"), "2 wrong copies")
self.assertEqual(install._plural(1, "wrong copy"), "1 wrong copy")
self.assertEqual(install._plural(2, "day"), "2 days")
state = checked_state()
folder = "/media/OLD"
state.from_folders = [Path(folder)]
state.found = [found(dest, folder) for dest in ("scph5500.bin", "pcsx/extra.bin",
"gba_bios.bin")]
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
text = self.screen(state, 100, 30)
self.assertIn("3 found in OLD, 1 needs the internet", text)
source = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
self.assertNotRegex(source, r"\} need the internet")
def test_line_mode_says_what_is_still_missing_before_the_remedy(self):
"""tui_audit_16 P3-3: "They are not in OLD" followed "Installed: 6
BIOS files", where the screen puts "Still missing:" between them."""
bios, game = b"B" * 32, b"G" * 32
listing = self.listing("retroarch", [
self.entry("a.bin", bios, ["nintendo-gba"]),
self.entry("b.bin", b"C" * 32, ["nintendo-gba"]),
self.entry("g.dat", game, ["sega-dreamcast"], category="game_data")],
categorized=True)
dest = self.root / "dest"
dest.mkdir()
old = self.root / "OLD"
old.mkdir()
(old / "a.bin").write_bytes(bios)
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
text, _asked = self.line_from(_args(manifest=listing, dest=dest,
platform="retroarch", declared_only=False),
[""], [old])
end = text.rsplit("Finished with files missing", 1)[-1]
self.assertIn("Still missing: 1 BIOS file and 1 game data file.", end)
self.assertLess(end.index("Still missing:"), end.index(f"They are not in {old}"))
self.assertLess(end.index("Installed:"), end.index("Still missing:"))
def test_a_rerun_with_everything_in_place_offers_nothing(self):
"""tui_audit_16 P3-4: line mode offered "Enter) install what is
found", then said "Everything is in place." twice."""
data = b"A" * 64
listing = self.listing("retroarch", [self.entry("a.bin", data, ["nintendo-gba"])])
dest = self.root / "dest"
dest.mkdir()
(dest / "a.bin").write_bytes(data)
text, asked = self.line_from(_args(manifest=listing, dest=dest, platform="retroarch",
declared_only=False), [])
self.assertEqual(asked, [])
self.assertNotIn("Enter)", text)
self.assertIn("RetroArch: 1 file, every one in place: nothing to install.", text)
self.assertEqual(text.count(install.EVERYTHING_IN_PLACE), 1)
def test_line_mode_names_the_files_it_cannot_supply(self):
"""tui_audit_16 P3-6: "Not installable: 37 files (1 required, not
found: 37)" read as a sum, and line mode never named the file the
screen names, nor its folder."""
listing = self.listing("retroarch", [
self.entry("a.bin", b"A" * 8, ["nintendo-gba"])], categorized=True,
omitted_files=[
{"dest": "jazz2/Source/Anims.j2a", "name": "Anims.j2a",
"system": "jazz-jackrabbit-2", "required": True, "reason": "not_found",
"category": "game_data"},
{"dest": "x.bin", "name": "x.bin", "system": "nintendo-gba",
"required": False, "reason": "not_found"}])
dest = self.root / "dest"
dest.mkdir()
text, _asked = self.line_run(_args(manifest=listing, dest=dest, platform="retroarch",
declared_only=False, dry_run=True), [])
self.assertIn("Not available here: jazz2/Source/Anims.j2a (Jazz Jackrabbit 2), game "
"data its game reads. Copy it from your own copy of the game into "
f"{dest / 'jazz2' / 'Source'}{os.sep}.", text)
self.assertEqual(install.not_installable_words(37, 1, {"not_found": 37}),
"Not installable: 37 files are not in the collection, 1 of them "
"required.")
self.assertEqual(install.not_installable_words(1, 1, {"not_found": 1}),
"Not installable: 1 file is not in the collection, required.")
self.assertIn("Why: no save directory (2), not found (3).",
install.not_installable_words(5, 0, {"not_found": 3,
"no_save_directory": 2}))
def test_a_system_emptied_by_no_game_data_is_named(self):
"""tui_audit_16 P3-7: --system nintendo-ds,sega-dreamcast with
--no-game-data: "For 1 of 8 systems (--system)", Dreamcast unsaid
until after Enter."""
manifest = self.game_manifest()
words = (f"{install.system_title('sega-dreamcast', manifest)} holds only game data: "
"left out (--no-game-data).")
listing = self.game_listing()
text, _asked = self.line_run(_args(manifest=listing, dest=self.here,
declared_only=False, dry_run=True,
system=["nintendo-ds", "sega-dreamcast"],
no_game_data=True), [])
head = text.split("Enter)", 1)[0]
self.assertIn(words, head)
state = install.TuiState(os_type="linux", home=HOME)
state.manifest = manifest
state.no_game_data = True
state.cli_systems = ["nintendo-ds", "sega-dreamcast"]
state.systems = [{"system": "nintendo-ds", "name": "Nintendo DS", "files": 1,
"present": 0, "missing": 1, "wrong_hash": 0, "bytes_needed": 1}]
state.ticked = {"nintendo-ds"}
state.dest = self.here
state.platform = "retroarch"
state.check = {**CHECK, "files": 1, "present": 0, "missing": 1, "wrong_hash": 0}
state.stack = ["plan"]
self.assertIn(words, self.screen(state, 100, 40))
def test_a_run_with_failures_counts_once(self):
"""tui_audit_16 P3-11: "Done. 44 files installed." then "38 files
copied from E:\\, 4 downloaded, 2 still missing." gave two counts,
the two that failed as missing. Each mode says one, failures named."""
out = io.StringIO()
sources = install.sources_line([("/media/OLD", 38)], 0, 4, 0, failed=2)
self.assertEqual(sources, "38 files copied from /media/OLD, 4 downloaded, 2 failed.")
with contextlib.redirect_stdout(out), contextlib.suppress(SystemExit):
install._report_outcome([self.here], 42, 0, 2, 0, False,
failures=[("x.bin", "timed out"), ("y.bin", "timed out")],
sources=sources)
text = out.getvalue()
self.assertIn(f"Finished with errors. {sources}", text)
self.assertNotIn("42 files installed", text)
self.assertNotIn("still missing", text)
state = checked_state()
state.from_folders = [Path("/media/OLD")]
state.local_dests = {"gba_bios.bin": ("/media/OLD", "")}
state.finished = [("gba_bios.bin", True, ""), ("scph5500.bin", False, "timed out")]
state.stack, state.run = ["result"], "install"
state.summary = {"event": "summary", "mode": "install", "installed": 1,
"up_to_date": 0, "pending": 1,
"failed": [{"dest": "scph5500.bin", "reason": "timed out"}],
"skipped": [], "left_out": [], "kept": [], "interrupted": False}
title, lines = install.tui_outcome(state)
self.assertEqual(title, "Finished with errors")
self.assertEqual(lines[0], "1 file copied from OLD, 1 failed.")
self.assertNotIn("1 installed, 1 failed.", lines)
def test_check_never_says_a_missing_file_was_opened(self):
"""tui_audit_16 P3-12: "missing: nintendo-ds_0.bin" then "opened at
..." read as a file that was opened."""
entry = {"dest": "a.bin", "systems": ["nintendo-gba"], "cores": None}
self.assertEqual(install.opened_line(entry, {}, self.here),
f"expected at {self.here / 'a.bin'}")
named = {**entry, "cores": ["mgba"]}
self.assertEqual(install.opened_line(named, {}, self.here),
f"read by mgba at {self.here / 'a.bin'}")
# Root cause 3: one width with a floor.
def test_a_terminal_of_no_size_has_the_fallback(self):
"""code_audit_23 P3-5: Python 3.8 gives a pty of no size as 0 and 0.
The plan printed "0 systems, the largest first:" and the progress
line lost every figure but its percentage."""
with mock.patch.object(install.shutil, "get_terminal_size",
return_value=os.terminal_size((0, 0))):
self.assertEqual(install.terminal_size(), (80, 24))
out = io.StringIO()
term = types.SimpleNamespace(live=True, quiet=False, stream=out,
bold=str, dim=str)
progress = install.Progress(1, 1400, term)
progress.bytes = progress.files = 1400
progress.files = 1
progress._draw()
self.assertIn("(1/1 files)", out.getvalue())
self.assertIn("[####", out.getvalue())
with contextlib.redirect_stdout(io.StringIO()) as plan:
install._print_plan([], self.here, term, copied=[" 1 file copied"])
self.assertNotIn("the largest first", plan.getvalue())
with mock.patch.object(install.shutil, "get_terminal_size",
return_value=os.terminal_size((8, 3))):
self.assertEqual(install.terminal_size(), (install.LINE_MIN_COLS, 3))
def systems_question(self, cols: int) -> list[str]:
manifest = json.loads((REPO_ROOT / "install" / "retroarch.json").read_text())
out = io.StringIO()
with mock.patch.object(install.shutil, "get_terminal_size",
return_value=os.terminal_size((cols, 24))), \
mock.patch.object(install, "_ask", return_value=""), \
contextlib.redirect_stdout(out):
install._choose_narrowing(manifest["files"], [], manifest, None)
text = out.getvalue()
ends = [at for at in (text.find("\nCores:"), text.find("\nThe platform's own"))
if at >= 0]
return text[text.index("Systems:"):min(ends, default=len(text))].splitlines()
def test_the_systems_question_stays_inside_the_terminal(self):
"""tui_audit_16 P3-5: 6 lines of 81 cells at 80 columns, 16 of 41 at
40, "shared" and "platform" alone on a line."""
for cols in (100, 80, 60, 40, 30):
with self.subTest(cols=cols):
lines = self.systems_question(cols)
for line in lines:
self.assertLess(install.cell_width(line), cols, line)
if line.strip() and not line.endswith(":"):
self.assertTrue(len(install._glued_words(line)) > 1
or re.match(r" *\d+\) ", line), (cols, line))
note = " ".join(line.strip() for line in lines[lines.index(
next(line for line in lines if line.startswith("'Other"))):])
self.assertIn("this platform does not list", note)
def test_the_setup_list_never_folds_inside_a_word(self):
"""tui_audit_16 P3-15: at 39 columns "Miy" / "oo Flip"."""
for cols in (80, 39, 30):
with self.subTest(cols=cols):
with mock.patch.object(install.shutil, "get_terminal_size",
return_value=os.terminal_size((cols, 11))):
text, _asked = self.line_run(_args(declared_only=False), [])
block = text.split("Which setup?", 1)[1].split(" q) quit", 1)[0]
lines = [line for line in block.splitlines() if line.strip()]
self.assertTrue(lines)
for line in lines:
self.assertLess(install.cell_width(line), cols, line)
self.assertIn("Miyoo Flip", " ".join(" ".join(lines).split()))
# Root cause 4: line mode in the screen's order and words.
def test_on_wsl_the_card_comes_first_in_both_modes(self):
"""code_audit_23 P3-6: line mode numbered Windows RetroArch 1) and
the card 2), where the screen put the card first."""
card = games_part(self.root / "media" / "EASYROMS")
windows = Path("/mnt/c/RetroArch-Win64/system")
side = [("Windows RetroArch: C:\\RetroArch-Win64\\system", windows)]
with mock.patch.object(install, "_mounted_cards", return_value=[card]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
mock.patch.object(install, "other_side", return_value=side), \
mock.patch.object(install, "_ask", return_value="q"), \
contextlib.redirect_stdout(io.StringIO()) as out, \
contextlib.suppress(SystemExit):
install.ask_platform_folder("wsl", "retroarch")
state = self.first_screen([card])
self.assertRegex(out.getvalue(), r"\n 1\) EASYROMS: bios \(SD card")
self.assertRegex(out.getvalue(), r"\n 2\) Windows RetroArch")
with mock.patch.object(install, "_mounted_cards", return_value=[card]), \
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
mock.patch.object(install, "other_side", return_value=side):
state.os_type = "wsl"
install.tui_update(state, Key("enter"))
rows = [row for row in install.tui_rows(state) if row.action == "drive"]
self.assertEqual([row.value for row in rows], [card / "bios", windows])
def test_nothing_found_is_said_once(self):
"""tui_audit_16 P3-2: "Nothing was found here." twice, and after "A
card is in the reader"."""
text, _asked = self.line_run(_args(declared_only=False), [])
self.assertEqual(text.count("Nothing was found here."), 1)
card = games_part(self.root / "media" / "EASYROMS")
text, _asked = self.line_run(_args(declared_only=False), [], mounts=[card])
self.assertIn("A card is in the reader (EASYROMS): choose its system.", text)
self.assertNotIn("Nothing was found here", text)
def test_the_bios_only_drive_note_is_one_plain_sentence(self):
"""tui_audit_16 P3-9: "listed, not chosen", "(card or drive)" under
a note saying no card, two parentheses side by side, and "keeps
them" after a title."""
data = self.drive("DATA", ("bios", "snes", "Music"))
stick = self.drive("STICK", ("bios",))
card = games_part(self.root / "media" / "EASYROMS")
mounts = [card, data, stick]
state, text, rows = self.folder_screen(mounts)
self.assertIn("DATA and STICK have a bios folder but no folder a handheld system "
"writes. Pick one below if your files go there.", text)
self.assertIn("The card in the reader keeps BIOS files in its bios folder", text)
labels = [row.label for row in rows]
self.assertIn("DATA: bios (drive)", labels)
self.assertIn("EASYROMS: bios (SD card)", labels)
line, _asked = self.line_run(_args(manifest=self.game_listing(), dry_run=True,
declared_only=False), ["1", ""], mounts=mounts)
for said in (text, line):
self.assertNotIn("listed, not chosen", said)
self.assertNotIn("card or drive", said)
self.assertNotIn(") (", said)
self.assertNotIn("keeps them", said)
def test_the_same_choice_is_one_command_in_both_modes(self):
"""tui_audit_16 P3-14: the screen wrote --manifest before --from,
line mode after it."""
hint = install.Invocation()
hint.os_type = "linux"
hint.options = [("--from", "/media/OLD"), ("--offline", None),
("--manifest", "/x/list.json"), ("--system", "a")]
self.assertEqual(hint.command("retroarch", "/dest"),
"python3 install.py --platform retroarch --dest /dest --manifest "
"/x/list.json --system a --from /media/OLD --offline")
state = checked_state()
state.manifest_given, state.manifest_path = True, Path("/x/list.json")
state.from_folders = [Path("/media/OLD")]
args = _args(manifest="/x/list.json", platform="retroarch", dest=SYSTEM_DIR,
declared_only=False, offline=False)
setattr(args, "from", ["/media/OLD"])
line = install.Invocation()
line.os_type = "linux"
line.record(args)
with mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}):
line.options.append(("--offline", None))
self.assertEqual(install.tui_command(state), line.command("retroarch", SYSTEM_DIR))
# Root cause 5: interaction.
def test_a_tap_on_a_focused_row_shows_it_was_seen(self):
"""tui_audit_16 P3-8: one tap on the focused Onion row showed
nothing, a second continued."""
state = fresh_state()
state.stack = ["setup"]
state.mouse = True
lines = drawn(state, 80, 24)
row = next(index for index, line in enumerate(lines)
if str(line).lstrip().startswith("> "))
self.assertEqual(tap(state, 4, row), [])
lines = drawn(state, 80, 24)
self.assertIn(f"A/Enter Continue: {install.TUI_ARMED}", str(lines[-1]))
self.assertIn(f"RetroArch: {install.TUI_ARMED}", str(lines[row]))
self.assertNotEqual(tap(state, 4, row), [])
def test_a_phone_bar_names_the_enter_key(self):
"""tui_audit_16 P3-13: Termux at 40 columns said "A Continue", and
the a key does nothing."""
state = install.TuiState(os_type="android", home=HOME)
state.pad = install.tui_pad_route("android", {})
self.assertEqual(state.pad, "both")
bar = str(install.tui_view(state, 40, 12)[-1])
self.assertIn("Enter", bar)
self.assertNotRegex(bar, r"(^| )A ")
self.assertIn("A/Enter", str(install.tui_view(state, 80, 24)[-1]))
state.pad = "gptokeyb"
self.assertRegex(str(install.tui_view(state, 40, 12)[-1]), r"(^| )A ")
def test_which_folder_keeps_its_folder_at_40_by_12(self):
"""tui_audit_16 P3-1: with two drives holding bios, Which folder?
at 40x12 dropped the folder "Use this folder" stands for."""
data = self.drive("DATA", ("bios", "snes", "Music"))
stick = self.drive("STICK", ("bios",))
for mounts in ([data, stick], [games_part(self.root / "media" / "EASYROMS"), data]):
with self.subTest(mounts=[mount.name for mount in mounts]):
state, _text, _rows = self.folder_screen(mounts)
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
small = " ".join(str(line) for line in install.tui_view(state, 40, 12))
folder = str(state.folder_default)
self.assertIn("reads its BIOS files", small)
self.assertIn(folder[-12:], small)
# Root cause 6: the pages say what the installer shows.
def test_which_pack_names_the_first_screen(self):
"""tui_audit_16 P3-10: which-pack said the command opens a screen
where Install and Choose systems are, the second screen."""
sys.path.insert(0, str(REPO_ROOT / "scripts"))
self.addCleanup(sys.path.remove, str(REPO_ROOT / "scripts"))
import generate_site # noqa: PLC0415
words = " ".join(generate_site.first_screen_words())
found, nothing = fresh_state(), install.TuiState(os_type="linux", home=HOME)
found.stack = ["setup"]
self.assertIn(install.tui_view(found, 80, 24)[0].split(": ", 1)[1].strip(), words)
self.assertIn(install.tui_view(nothing, 80, 24)[0].split(": ", 1)[1].strip(), words)
self.assertLess(words.index(install.TUI_FOUND_TITLE),
words.index(install.TUI_INSTALL_ROW))
self.assertNotIn("opens a screen", words)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -167,7 +167,7 @@ class GetYourFilesOrder(unittest.TestCase):
text = _page("which-pack.md")
order = [text.index(mark) for mark in (
"## Pick your setup", "| Setup | Runs on | BIOS folder | ZIP |", "<FilePicker",
"## Not in the list? One command finds it", "The command opens a screen",
"## Not in the list? One command finds it", "The command asks **",
"<summary>Is it safe?</summary>",
)]
self.assertEqual(order, sorted(order))
+26 -15
View File
@@ -210,7 +210,9 @@ in the names of what is held. On a computer it names the keys (`Enter Install`,
launcher (`A Install`, `B Back`, see `RETROBIOS_PAD` below), and `?` shows
both the buttons and the keys. Where neither is known, in SteamOS game mode or
on a console no launcher named, it gives both spellings (`A/Enter`, `B/Esc`),
and a narrow screen shortens them to `A`, `B` and `X`. On the
and a narrow screen shortens them to `A`, `B` and `X`. In Termux on Android,
with no launcher named, a narrow screen keeps the keys (`Enter`, `Esc`) instead,
a phone having an Enter key and no A button. On the
regions screen, Left and Right move the region under the cursor up and down the
order of preference. During the download, a stop waits for the files in
progress, and a second one stops at once. Select+Start on a handheld ends the
@@ -267,7 +269,8 @@ says why.
A mouse or a touch screen works too, beside the keys and buttons, in a
terminal that reports clicks: those of Linux, macOS, WSL and Termux. A click
or a tap on a box (a system, a core, a region) ticks or unticks it, as Space
does. On any other row it moves the cursor there, and a second one on that
does. On any other row it moves the cursor there and adds `click or tap
again` to the row and the bottom line, and a second one on that
row does what Enter does: one row of a terminal is far smaller than a finger,
and the row a screen opens on (**Install** on Confirm) is never chosen by one
tap. The entries of the bottom line press their key, **Exit** on the result
@@ -712,6 +715,7 @@ RetroArch: 10994 files, 6.7 GB.
3269 BIOS files (4.1 GB), 5089 game data files (2.5 GB) and 2636 data files (38.5 MB).
Game data: files a game or a game engine reads, of no use to the other cores. Data files: the files an emulator reads from a data folder of its own, such as Dolphin's Sys and PPSSPP's assets.
BIOS folder: /home/me/.config/retroarch/system
Not available here: jazz2/Source/Anims.j2a (Jazz Jackrabbit 2), game data its game reads. Copy it from your own copy of the game into /home/me/.config/retroarch/system/jazz2/Source/.
Enter) install everything
c) choose systems, cores and regions
f) copy from a folder or an old card
@@ -1028,6 +1032,8 @@ gives for the device, below the card's root instead of its mount point:
| `.tmp_update/onionVersion/version.txt` | Onion | `BIOS`, RetroArch's list |
| `.allium/` | Allium | `BIOS`, RetroArch's list |
| `Koriki/version.txt` | Koriki | `BIOS`, RetroArch's list |
| `gamedata/`, `homebrew/` beside `bios/`, on a drive that is not a fixed disk | AmberELEC | `bios`, AmberELEC's list |
| `bezels/default.cfg` beside `bios/`, on a drive that is not a fixed disk | EmuELEC | `bios`, EmuELEC's list |
| `system/batocera.conf` holding `system.hostname=BATOCERA` or no hostname line, and no KNULLI file above | Batocera SHARE drive | `bios`, Batocera's list |
| `system/recalbox.conf` | Recalbox SHARE drive | `bios`, Recalbox's list |
| `system/batocera.conf` with any other hostname, and no KNULLI file above, or the files of two SHARE drives | named by neither: the run says which systems it could be | the folder of the system named with `--platform` |
@@ -1166,9 +1172,10 @@ folder inside another system's card (ROCKNIX's `roms/bios` named for KNULLI).
A card whose own files name it is that card whatever its name: an Onion card
mounted as `BIOS` gets `BIOS/BIOS`. One without the system's own files is
refused for the MinUI family, Onion, muOS, spruceOS, CrossMix-OS, Allium and
Koriki. KNULLI and ROCKNIX fill their card when they first start, and no file
on the card names ArkOS, dArkOS, JELOS, KOLOS, UnofficialOS, ArchR, RetroOZ,
plumOS, MiyooCFW, AmberELEC, EmuELEC or the Anbernic firmware: such a folder
Koriki. KNULLI, ROCKNIX, AmberELEC and EmuELEC fill their card when they first
start (AmberELEC's `gamedata` and `homebrew`, EmuELEC's `bezels/default.cfg`),
and no file on the card names ArkOS, dArkOS, JELOS, KOLOS, UnofficialOS, ArchR,
RetroOZ, plumOS, MiyooCFW or the Anbernic firmware: such a folder
gets the system's folder below it, and a note says so, kept on **Create the
folder?** and the plan. No to creating its folders asks for the card again.
Under WSL a drive letter (`D:`, `D:\`) is that Windows drive, typed or given
@@ -1197,16 +1204,20 @@ that question a drive is numbered by the folder its system reads (`ONION:
BIOS`), a drive nothing names by its `bios` folder (`EASYROMS: bios`), never
by its root, which no setup reads. A handheld's card whose own files sit on a
part a computer does not open is told by the folders its system writes beside
`bios` on the games part: `themes` and `launchimages` on ArkOS's `EASYROMS`,
`gamedata` and `homebrew` on AmberELEC's `GAMES`, `bezels/default.cfg` on
EmuELEC's `EEROMS`. A disk Windows or Linux calls fixed is never one. With
nothing found here the first screen and line mode say `A card is in the reader
(EASYROMS): choose its system` (two cards: `choose their systems`), and its
`bios` folder comes first at the folder question, under Enter, before this
computer's usual folder. A drive with a `bios` folder and none of those
folders, a data drive or a stick, is numbered with a note (`DATA has a bios
folder, and nothing on it shows a handheld's card: listed, not chosen.`) and
never taken under Enter. Another system's card or SHARE drive, and a
`bios` on the games part: `bgmusic` or `launchimages` on ArkOS's `EASYROMS`
(every ArkOS card has one of them from its first images, `themes` only since
an update a full card skipped), `gamedata` and `homebrew` on AmberELEC's
`GAMES`, `bezels/default.cfg` on EmuELEC's `EEROMS`. A disk Windows or Linux
calls fixed is never one. AmberELEC's and EmuELEC's card is found as theirs
(`AmberELEC SD card (GAMES)` under **Found on this machine**), in both modes
and on their own setup's card question, and gets their list. An ArkOS card
reads RetroArch's list: with nothing found here the first screen and line mode
say `A card is in the reader (EASYROMS): choose its system` (two cards: `choose
their systems`), and its `bios` folder comes first at the folder question,
under Enter, before this computer's usual folder. A drive with a `bios` folder
and none of those folders, a data drive or a stick, is numbered with a note
(`DATA has a bios folder but no folder a handheld system writes. Pick it below
if your files go there.`) and never taken under Enter. Another system's card or SHARE drive, and a
drive two systems fit, are left out, and a line names them: each is reached
through its own setup. 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