fix: a card is told by what its system writes

A drive is a handheld's games part only when it holds the folders ArkOS,
AmberELEC or EmuELEC write there, and never a disk Windows or Linux calls
fixed. A data drive or a stick with a bios folder is listed with a note,
never taken under Enter. Two cards are named in the plural, and the
settle of a new screen runs from when it is drawn.
This commit is contained in:
Abdessamad Derraz committed 2026-10-28 13:28:01 +01:00
1 parent 9dd770f4bf
commit 9911321227
6 files changed
+416 -50

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 = "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7"
$defaultInstallSha256 = "478537cc1ede1d3aa9e7b579a178bd4552de2b40c512fe8c6c7b3d3b277799aa"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+145 -27
View File
@@ -7249,37 +7249,137 @@ def drive_targets(plat: str) -> list[tuple[str, Path]]:
return sorted(targets, key=lambda target: target[1] not in cards)
# The folders a handheld card's games part holds, one per console: the
# names people type for a system (SYSTEM_SHORT_NAMES) and the ArkOS and
# AmberELEC roms folders those miss.
_CONSOLE_FOLDERS = frozenset({*SYSTEM_SHORT_NAMES, "arcade", "atari2600", "atarilynx",
"fbneo", "gamegear", "mame", "mastersystem", "megadrive",
"neogeo", "pcengine", "psp", "segacd", "wonderswan"})
# What a handheld system writes on the games part a computer mounts, beside
# its bios folder: a drive holding these is that card, and a data drive or
# a stick with an emulation library holds none of them (code_audit_22 P2-1).
# Each set is (system, folders and files that must all be there).
_GAMES_PART_MARKERS = (
# ArkOS and dArkOS mount EASYROMS as /roms. dArkOS daecf488 creates every
# folder of game_systems.txt on it (finishing_touches.sh:420-430),
# themes (game_systems.txt:108) and launchimages (game_systems.txt:54),
# and copies loading.jpg there (finishing_touches.sh:471-472). ArkOS
# dedf4278 updates both in place (Update-RG351MP.sh:1003,1443).
("ArkOS", ("themes", "launchimages")),
# AmberELEC 5a3882cb fills GAMES at every boot: homebrew/ from its ports
# with gamelist.xml (packages/amberelec/sources/autostart.sh:93-94,
# 110-113) and gamedata/ for RetroArch, PPSSPP, DOSBox and Hatari
# (autostart.sh:123-145).
("AmberELEC", ("gamedata", "homebrew")),
# EmuELEC d4c7b2f1 copies its default bezels to EEROMS at every boot
# (packages/sx05re/emuelec/bin/emuelec_autostart.sh:78-81).
("EmuELEC", ("bezels/default.cfg",)),
)
# GetDriveTypeW: DRIVE_FIXED, DRIVE_REMOTE, DRIVE_CDROM and DRIVE_RAMDISK,
# none of them a card in a reader.
_NOT_REMOVABLE_TYPES = (3, 4, 5, 6)
def _drive_not_removable(mount: Path) -> bool:
"""Whether the system says mount is a drive no card sits in: a fixed
disk, a network drive or a disc on Windows (GetDriveTypeW), a whole
partition of a disk the kernel calls fixed elsewhere (_fixed_partition).
False where it says nothing."""
if os.name == "nt":
try:
import ctypes # noqa: PLC0415
kind = ctypes.windll.kernel32.GetDriveTypeW(str(mount)) # type: ignore[attr-defined]
except (AttributeError, OSError):
return False
return kind in _NOT_REMOVABLE_TYPES
try:
text = Path("/proc/self/mountinfo").read_text(encoding="utf-8", errors="replace")
except OSError:
return False
return _fixed_partition(mount, text, Path("/sys"))
def _fixed_partition(mount: Path, mountinfo: str, sys_root: Path) -> bool:
"""Whether mountinfo mounts a whole partition at mount whose disk sysfs
calls fixed: removable at 0, neither on USB nor an MMC card. False for
a folder bound in place, which is no whole drive, and for WSL's
/mnt/d, a Windows drive Linux cannot classify."""
for line in mountinfo.splitlines():
fields = line.split(" ")
if len(fields) < 5:
continue
point = re.sub(r"\\([0-7]{3})", lambda m: chr(int(m.group(1), 8)), fields[4])
if point != str(mount):
continue
# Field 4 is the folder of the filesystem mounted there: "/" for a
# whole partition, a folder for a bind mount (proc(5)).
device = sys_root / "dev" / "block" / fields[2]
if fields[3] != "/" or not _exists(device):
return False
try:
real = device.resolve()
disk = real.parent if _exists(real / "partition") else real
if "/usb" in real.as_posix() or disk.name.startswith("mmcblk"):
return False
return (disk / "removable").read_text(encoding="ascii").strip() == "0"
except OSError:
return False
return False
def _games_part_system(mount: Path) -> str:
"""The handheld system whose games part mount is, as the files it
writes there say, "" for none (_GAMES_PART_MARKERS)."""
if not _is_dir(mount / "bios"):
return ""
return next((name for name, markers in _GAMES_PART_MARKERS
if all(_exists(mount / marker) for marker in markers)), "")
def _unnamed_candidates() -> list[Path]:
"""The mounted drives no file names, no SHARE either, holding a bios
folder: a handheld's card, or any drive someone keeps an emulation
library on."""
return [mount for mount in _mounted_cards()
if card_system(mount) is None and not share_doubt(mount)[0]
and _is_dir(mount / "bios")]
def unnamed_cards() -> list[Path]:
"""The mounted drives no file names whose root holds a bios folder and
console folders: ArkOS's EASYROMS, AmberELEC's GAMES. Which system runs
them cannot be told (their own files are on a part a computer does not
open), but they are a handheld's card: "Nothing was found here" over
one, and two presses of Enter filled this computer's folder for a
RetroArch that was not there (tui_audit_14 P2-3)."""
cards = []
for mount in _mounted_cards():
if (card_system(mount) is not None or share_doubt(mount)[0]
or not _is_dir(mount / "bios")):
continue
if any(name.casefold() in _CONSOLE_FOLDERS and _is_dir(mount / name)
for name in _listed(mount)):
cards.append(mount)
return cards
"""The mounted drives no file names that are a handheld's games part:
ArkOS's EASYROMS, AmberELEC's GAMES, EmuELEC's EEROMS, each told by the
folders its system writes there (_GAMES_PART_MARKERS), on a drive the
system does not call fixed. Which of the systems that read RetroArch's
list runs them cannot be told, but they are a handheld's card:
"Nothing was found here" over one, and two presses of Enter filled this
computer's folder for a RetroArch that was not there (tui_audit_14
P2-3). A bios folder and a console folder were the rule, and a data
drive or WSL's D: disk with bios/, snes/ and Music/ took the list
(code_audit_22 P2-1)."""
return [mount for mount in _unnamed_candidates()
if _games_part_system(mount) and not _drive_not_removable(mount)]
def unproven_drives() -> list[Path]:
"""The drives with a bios folder nothing shows to be a handheld's card:
offered at the folder question, never its default."""
cards = unnamed_cards()
return [mount for mount in _unnamed_candidates() if mount not in cards]
def unproven_words() -> str:
"""The plain note on those drives, "" for none."""
drives = unproven_drives()
if not drives:
return ""
names = ", ".join(_mount_name(mount) for mount in drives)
has, it = ("has", "it") if len(drives) == 1 else ("have", "them")
return (f"{names} {has} a bios folder, and nothing on {it} shows a handheld's card: "
"listed, not chosen.")
def unnamed_card_words(cards: list[Path], tail: str = ".") -> str:
"""What the first screen and line mode say of those cards."""
"""What the first screen and line mode say of those cards: one card,
its system, two cards, their systems (code_audit_22 P3-2)."""
names = ", ".join(_mount_name(mount) for mount in cards)
what = "A card is" if len(cards) == 1 else "Cards are"
return f"{what} in the reader ({names}): choose its system{tail}"
if len(cards) == 1:
return f"A card is in the reader ({names}): choose its system{tail}"
return f"Cards are in the reader ({names}): choose their systems{tail}"
def say_unnamed_cards() -> None:
@@ -7351,8 +7451,10 @@ def ask_platform_folder(os_type: str, plat: str, ask: bool = True,
print(f" {share_words(plat)}")
elif cards:
default = cards[0] / "bios"
print(f" The card in the reader keeps them in its bios folder: "
f"{shown_path(default)}.")
# Two cards: the one Enter takes is named, "The card" was one of
# two (code_audit_22 P3-2).
which = "The card in the reader" if len(cards) == 1 else _mount_name(cards[0])
print(f" {which} keeps them in its bios folder: {shown_path(default)}.")
elif found is not None:
print(f" {name} reads its BIOS files from: {shown_path(found)}.")
elif default is not None:
@@ -7381,6 +7483,9 @@ def ask_platform_folder(os_type: str, plat: str, ask: bool = True,
left_out = "" if shared else drives_left_out(plat)
if left_out:
print(f" {left_out}")
unproven = "" if shared else unproven_words()
if unproven:
print(f" {unproven}")
numbers = "" if not targets else (" or 1" if len(targets) == 1
else f" or 1 to {len(targets)}")
why = ("" if default is not None or shared
@@ -18012,7 +18117,11 @@ def _view_folder(state: TuiState, width: int, height: int) -> tuple:
before = [lead, str(state.folder_default)]
if (state.folder_kind == "usual" and tui_rows(state)
and tui_rows(state)[0].value in {mount / "bios" for mount in state.unnamed}):
before = ["The card in the reader keeps them in its bios folder, first below. "
cards = [mount for mount in state.unnamed
if mount / "bios" == tui_rows(state)[0].value]
which = ("The card in the reader" if len(state.unnamed) == 1
else _mount_name(cards[0]))
before = [f"{which} keeps them in its bios folder, first below. "
f"{lead}", str(state.folder_default)]
elif state.folder_kind == "closed":
title = "Which folder?"
@@ -18023,6 +18132,8 @@ def _view_folder(state: TuiState, width: int, height: int) -> tuple:
if not bound and state.folder_kind != "share":
left_out = drives_left_out(state.platform)
before += [left_out] if left_out else []
unproven = unproven_words()
before += [unproven] if unproven else []
return (title, _compose(state, width, height, before, tui_rows(state), _status_after(state),
pinned=state.refused),
("A/Enter Choose", "B/Esc Back", "? Help"))
@@ -20155,10 +20266,17 @@ def run_tui(args: argparse.Namespace) -> int:
state.now = time.monotonic()
lines = tui_view(state, cols, rows)
# A click is read against the frame it was made on.
before = state.hits
state.hits = tui_hits(state, lines)
# Before the frame: a frame on screen takes clicks as it shows.
terminal.set_mouse(tui_wants_mouse(state, lines))
terminal.draw(lines, cols)
if before is None or state.hits.shown != before.shown:
# A new screen's settle runs from when it is on the screen,
# after its layout and its write: the second click of a
# double click waited out a slow layout in the input and
# acted on the screen it had never seen (code_audit_22 P3-1).
state.hits = state.hits._replace(shown=time.monotonic())
for key in terminal.keys(TUI_FRAME):
if key.name == "ctrl-z":
# ISIG is off, so Ctrl-Z comes as a key: it stops what
+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="67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7"
DEFAULT_INSTALL_SHA256="478537cc1ede1d3aa9e7b579a178bd4552de2b40c512fe8c6c7b3d3b277799aa"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 939291,
"sha256": "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7",
"size": 944874,
"sha256": "478537cc1ede1d3aa9e7b579a178bd4552de2b40c512fe8c6c7b3d3b277799aa",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 992468,
"sha256": "06761ebb41ef22fa2c4b64c5e27fab5ed5508dbcd63c5a634527d03a7cdaa7d1"
"size": 998051,
"sha256": "d39926472bdc1ef05c1c16b058b135cbd988b336e4a5d222b5f918e6663d9d32"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 939291,
"sha256": "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7",
"size": 944874,
"sha256": "478537cc1ede1d3aa9e7b579a178bd4552de2b40c512fe8c6c7b3d3b277799aa",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 939291,
"sha256": "67cbe64d1de7b2f38edd25cab1b34384c55cac77e13f22f4c302fa2b04a809d7",
"size": 944874,
"sha256": "478537cc1ede1d3aa9e7b579a178bd4552de2b40c512fe8c6c7b3d3b277799aa",
"executable": false
},
{
+249 -6
View File
@@ -6756,6 +6756,21 @@ class AuditSevenScreens(unittest.TestCase):
self.assertIn("1 wrong copy: backed up, replaced.", view)
def games_part(card: Path) -> Path:
"""An ArkOS EASYROMS (dArkOS game_systems.txt: themes, launchimages
with loading.jpg) or an AmberELEC GAMES (autostart.sh: gamedata,
homebrew with gamelist.xml), console folders beside them."""
marks = (("gamedata/retroarch", "homebrew") if card.name.startswith("GAMES")
else ("themes/es-theme-nes-box", "launchimages"))
for folder in ("bios", "nes", "gba", "psx", *marks):
(card / folder).mkdir(parents=True)
if card.name.startswith("GAMES"):
(card / "homebrew" / "gamelist.xml").write_text("<gameList/>\n")
else:
(card / "launchimages" / "loading.jpg").write_bytes(b"\xff\xd8")
return card
class _Decided(Exception):
"""Raised where line mode has chosen its platforms and folders."""
@@ -12248,12 +12263,10 @@ class AuditTwentyOneAndFourteen(unittest.TestCase):
self.assertIn("About 0 files, 0 B, for 1 of 3 systems, 3 need the internet", text)
def easyroms(self, name: str = "EASYROMS") -> Path:
"""An ArkOS games part as a computer mounts it: bios/ and console
folders, no file naming the system."""
card = self.root / "media" / name
for folder in ("bios", "nes", "gba", "psx"):
(card / folder).mkdir(parents=True)
return card
"""A handheld's games part as a computer mounts it, with the folders
its system writes there (install._GAMES_PART_MARKERS): 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
@@ -12557,5 +12570,235 @@ class AuditTwentyOneAndFourteen(unittest.TestCase):
self.assertIn(f"BIOS folder: {card / 'bios'}", text)
class AuditTwentyTwoAndFifteen(unittest.TestCase):
"""code_audit_22 and tui_audit_15: a drive is a handheld's card only by
the files its system writes, one settle from the frame shown, one
vocabulary and one count per run, and the focus after an action."""
setUp = LineModeAndScreenDecideAlike.setUp
line_run = LineModeAndScreenDecideAlike.line_run
listing = LineModeAndScreenDecideAlike.listing
entry = staticmethod(LineModeAndScreenDecideAlike.entry)
receive = staticmethod(AuditTwentyAndThirteen.receive)
game_manifest = AuditTwentyAndThirteen.game_manifest
game_events = AuditTwentyAndThirteen.game_events
game_plan = AuditTwentyAndThirteen.game_plan
game_listing = AuditTwentyOneAndFourteen.game_listing
screen = staticmethod(AuditTwentyOneAndFourteen.screen)
def drive(self, name: str, folders: tuple) -> Path:
root = self.root / "media" / name
for folder in folders:
(root / folder).mkdir(parents=True)
return root
def first_screen(self, mounts: list) -> install.TuiState:
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=[]), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install.Path, "home", return_value=self.root / "home"):
state, _effects = install.tui_start(_args(), "linux")
return state
def folder_screen(self, mounts: list) -> tuple[install.TuiState, str, list]:
"""RetroArch chosen on the first screen: (state, Which folder? at
80x40, its rows)."""
state = self.first_screen(mounts)
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
install.tui_update(state, Key("enter"))
self.assertEqual(state.screen, "folder")
return state, self.screen(state, 80, 40), install.tui_rows(state)
def test_a_data_drive_or_a_stick_is_no_card(self):
"""code_audit_22 P2-1: a data drive holding bios/, snes/ and Music/
was "a card in the reader", and three presses of Enter put the list
on it. A stick with bios/ alone is no card either: both are listed,
never under Enter, and a plain line says why."""
data = self.drive("DATA", ("bios", "snes", "Music"))
stick = self.drive("STICK", ("bios",))
mounts = [data, stick]
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
mock.patch.object(install, "_mount_points", return_value=frozenset()):
self.assertEqual(install.unnamed_cards(), [])
state = self.first_screen(mounts)
self.assertEqual(state.unnamed, [])
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(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("Enter for the default above", asked[-2])
self.assertNotIn(f"BIOS folder: {data / 'bios'}", text)
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."""
arkos = games_part(self.root / "media" / "EASYROMS")
amber = games_part(self.root / "media" / "GAMES")
emuelec = self.drive("EEROMS", ("bios", "nes", "bezels"))
(emuelec / "bezels" / "default.cfg").write_text("")
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],
["ArkOS", "AmberELEC", "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(), [])
def test_a_card_beside_a_data_drive_comes_first(self):
"""code_audit_22 P2-1: the ArkOS card and the data drive mounted,
the data drive sorted first and took Enter. The card comes first in
both modes, the drive listed after it with its note."""
data = self.drive("DATA", ("bios", "snes", "Music"))
card = games_part(self.root / "media" / "EASYROMS")
mounts = [data, card]
state = self.first_screen(mounts)
self.assertEqual(state.unnamed, [card])
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)
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"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]
state = self.first_screen(mounts)
self.assertIn("Cards are in the reader (EASYROMS, GAMES): 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)
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.assertNotIn("The card in the reader", text)
def test_a_fixed_partition_is_told_from_a_card(self):
"""code_audit_22 P2-1: every mount under /media and /mnt, and on
Windows every drive letter, was a card. A whole partition of a disk
sysfs calls fixed is not, a USB or MMC one is, a folder bound in
place says nothing, and Windows answers by GetDriveTypeW."""
sys_root = self.root / "sys"
devices = sys_root / "devices"
for disk, part, removable in (("pci0/nvme0n1", "nvme0n1p2", "0"),
("pci0/usb2/2-1/sdb", "sdb1", "0"),
("mmc0/mmcblk0", "mmcblk0p1", "0"),
("pci0/sdc", "sdc1", "1")):
(devices / disk / part).mkdir(parents=True)
(devices / disk / part / "partition").write_text("1\n")
(devices / disk / "removable").write_text(f"{removable}\n")
block = sys_root / "dev" / "block"
block.mkdir(parents=True)
for number, target in (("259:2", "pci0/nvme0n1/nvme0n1p2"),
("8:17", "pci0/usb2/2-1/sdb/sdb1"),
("179:1", "mmc0/mmcblk0/mmcblk0p1"),
("8:33", "pci0/sdc/sdc1")):
(block / number).symlink_to(devices / target)
mountinfo = "\n".join([
"40 1 259:2 / /media/me/DATA rw - ext4 /dev/nvme0n1p2 rw",
"41 1 8:17 / /media/me/EASYROMS rw - vfat /dev/sdb1 rw",
"42 1 179:1 / /media/me/GAMES rw - exfat /dev/mmcblk0p1 rw",
"43 1 8:33 / /media/me/STICK rw - vfat /dev/sdc1 rw",
"44 1 259:2 /home/me/cards/X /media/me/BOUND rw - ext4 /dev/nvme0n1p2 rw",
"45 1 0:74 / /mnt/d rw - 9p D:\\134 rw"])
told = {name: install._fixed_partition(Path(point), mountinfo, sys_root)
for name, point in (("DATA", "/media/me/DATA"), ("USB", "/media/me/EASYROMS"),
("MMC", "/media/me/GAMES"), ("STICK", "/media/me/STICK"),
("BOUND", "/media/me/BOUND"), ("WSL", "/mnt/d"),
("unmounted", "/media/me/NONE"))}
self.assertEqual(told, {"DATA": True, "USB": False, "MMC": False, "STICK": False,
"BOUND": False, "WSL": False, "unmounted": False})
for kind, fixed in ((2, False), (3, True), (4, True)):
kernel = types.SimpleNamespace(GetDriveTypeW=lambda root, kind=kind: kind)
fake = types.SimpleNamespace(windll=types.SimpleNamespace(kernel32=kernel))
with mock.patch.object(install.os, "name", "nt"), \
mock.patch.dict(sys.modules, {"ctypes": fake}):
self.assertEqual(install._drive_not_removable(PureWindowsPath("D:\\")), fixed)
# install.py whose tui_view takes SLOW seconds on the first frame of each
# screen, as a slower device takes to lay one out (code_audit_22 P3-1).
SLOW_VIEW = """
import os, sys, time
sys.argv[0] = {install!r}
sys.path.insert(0, {repo!r})
import install
slow = float(os.environ["SLOW"])
real, seen = install.tui_view, [None]
def view(state, cols, rows):
key = (state.screen, len(state.stack))
if key != seen[0]:
seen[0] = key
time.sleep(slow)
return real(state, cols, rows)
install.tui_view = view
sys.exit(install.main())
"""
@unittest.skipUnless(pyte and pty and hasattr(pty, "fork"),
"screen tests need pyte (pip install pyte==0.8.2) and a pty")
class TheSettleRunsFromTheFrameShown(unittest.TestCase):
"""code_audit_22 P3-1: the settle was stamped before the layout, so on
a slow one the second click of a double click on Options' Back reached
the plan's Quit."""
repo = EndToEnd.repo
bar_entry = TapsOnAPtySeeFirst.bar_entry
def test_a_double_click_on_back_stays_on_the_plan_after_a_slow_layout(self):
repo = self.repo()
wrapper = repo.root / "slowview.py"
wrapper.write_text(SLOW_VIEW.format(install=str(REPO_ROOT / "install.py"),
repo=str(REPO_ROOT)))
env = {**self.env, "SLOW": "0.5"}
term = Pty([], env, 80, 24, program=[sys.executable, str(wrapper)])
self.addCleanup(lambda: term.status is None and os.kill(term.pid, 9))
term.wait("Where do you play?")
term.send("\r")
term.wait("> Install")
term.settle()
term.send("o")
term.wait("RetroBIOS: Options")
term.settle()
time.sleep(1.0)
col, line = self.bar_entry(term, "Back")
report = sgr(col + 1, line + 1) + sgr(col + 1, line + 1, release=True)
os.write(term.fd, report.encode())
time.sleep(0.2)
os.write(term.fd, report.encode())
time.sleep(1.0)
screen = term.settle(0.6)
self.assertNotIn("Quit?", screen)
self.assertIn("RetroBIOS: RetroArch", screen.splitlines()[0])
self.assertEqual(repo.installed(), [])
term.send("q")
if "Quit?" in term.settle():
term.send("\r")
term.close()
if __name__ == "__main__":
unittest.main()
+12 -7
View File
@@ -1181,13 +1181,18 @@ muOS, ArkOS and the others) opens the setup list instead, filtered to the
setups that read it, RetroArch first: Enter on it asks RetroArch's folder. At
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 drive no file names that holds a `bios`
folder and console folders (`nes`, `gba`, `psx`) is a handheld's card whose
own files sit on a part a computer does not open, ArkOS's `EASYROMS` and
AmberELEC's `GAMES`: with nothing found here the first screen and line mode
say `A card is in the reader (EASYROMS): choose its system`, and its `bios`
folder comes first at the folder question, under Enter, before this
computer's usual folder. Another system's card or SHARE drive, and a
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
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