mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
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:
1 parent
9dd770f4bf
commit
9911321227
6 files changed
+416
-50
No files matched your search
+1
-1
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user