fix: refuse only the axis a list lacks

The MinUI family's lists record cores and regions and no system: every
narrowing was refused with a remedy that fetched the same list. Each
option is now refused by the axis missing, with what works, and a pack
names its platform's list only when that list records the axis. Line
mode names what c chooses and skips a question with one answer, and
Options leaves out parallel downloads when nothing is downloaded.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 15:37:24 +01:00
1 parent 5704dd6793
commit 0fa4552dae
10 files changed
+314 -63

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "f7928407478aae79bd9cd7740521e5c83b4fdd26046521c1c49b2f278c3bf22d"
$defaultInstallSha256 = "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+103 -33
View File
@@ -4000,9 +4000,31 @@ def _chosen(entry: dict, wanted: set[str]) -> bool:
return bool(wanted & set(systems))
def recorded_axes(files: list[dict], omitted: list[dict] = ()) -> frozenset:
"""What a file list lets --system, --core and --region narrow by: its
systems, its cores, its regions. A list from an older release records
none. The MinUI family's records cores and regions and no system: all
three were refused on it, by a remedy that fetched the same list
(tui_audit_11 P2-1)."""
entries = [*files, *omitted]
systems = any("systems" in entry for entry in entries)
if not systems and not any("regions" in entry for entry in entries):
# Systems and regions came in one release: a list with neither is
# older, its cores read by nothing that narrows.
return frozenset()
# A current list records the regions of the files a region selects, and
# none when it has no such file: --region then keeps every file, as the
# pack built with it does.
axes = {"regions", *(["systems"] if systems else [])}
if any(copy.get("cores") for entry in files
for copy in [entry, *entry.get("alternatives", []), *entry.get("slot_choices", [])]):
axes.add("cores")
return frozenset(axes)
def records_selection(files: list[dict]) -> bool:
"""Whether a manifest carries what narrowing reads. Older ones do not."""
return any("systems" in entry for entry in files)
"""Whether a manifest records anything narrowing reads. Older ones do not."""
return bool(recorded_axes(files))
def available_choices(files: list[dict], omitted: list[dict]) -> dict[str, list[str]]:
@@ -7235,9 +7257,12 @@ def _prompt_custom_selection(
print(f" Enter) install {whole}")
if installed is not None and current != INSTALLED_ONLY:
print(f" i) only what the installed cores read: {_sized(installed[0])}")
# What c asks, named: "systems, cores and regions" was offered with
# a pack that records systems only (tui_audit_11 P3-10).
axes = narrowing_words(files, omitted) if choosable else ""
choosable = bool(axes)
if choosable:
print(" c) choose systems, cores and regions"
+ (" again" if current is not given else ""))
print(f" c) choose {axes}" + (" again" if current is not given else ""))
# The screen's "Copy the BIOS files from your old card or a folder".
copying = not dry_run
if copying:
@@ -7312,11 +7337,27 @@ def _print_menu_head(
print(f" {line}")
def narrowing_words(files: list[dict], omitted: list[dict]) -> str:
"""What c chooses among for these files, in words, "" for nothing: a
question with one answer is not asked (tui_audit_11 P3-10)."""
axes = recorded_axes(files, omitted)
choices = available_choices(files, omitted)
named = [word for word, axis, enough in (
("systems", "systems", len(listed_systems(files)) > 1),
("cores", "cores", len(choices["cores"]) > 1),
("regions", "regions", bool(choices["regions"]))) if axis in axes and enough]
return f"{', '.join(named[:-1])} and {named[-1]}" if len(named) > 1 else "".join(named)
def _choose_narrowing(files: list[dict], omitted: list[dict], manifest: dict | None,
bios_path: Path | None) -> tuple[list[str], list[str], list[str]]:
"""Systems, cores and regions asked one after the other, the systems
titled and ordered as the screen's Choose systems lists them."""
systems_listed = systems_in_order(listed_systems(files), files, manifest, bios_path)
titled and ordered as the screen's Choose systems lists them. A question
with one answer is skipped: "Cores: 1) azahar" was asked after the 3DS
alone was chosen."""
listed = listed_systems(files) if "systems" in recorded_axes(files, omitted) else []
systems_listed = (systems_in_order(listed, files, manifest, bios_path)
if len(listed) > 1 else [])
if OTHER_SYSTEMS in systems_listed:
print(f"\n'{OTHER_TITLE}' holds what cores load for systems this platform")
print("does not list, such as game engines and extra computers.")
@@ -7332,7 +7373,7 @@ def _choose_narrowing(files: list[dict], omitted: list[dict], manifest: dict | N
cores: list[str] = []
core_names = available_choices(kept, kept_omitted)["cores"]
if core_names:
if len(core_names) > 1:
print("\nThe platform's own files for these systems are always kept.")
print("The extra files each core loads can be narrowed.")
cores = _pick("Cores:", [
@@ -7567,6 +7608,28 @@ def _older_list_advice(plat: str = "") -> str:
"installer again to fetch the current one.")
def _narrowing_advice(plat: str, axis: str, flag: str, recorded: frozenset,
pack_alone: bool) -> str:
"""What to do when a list or a pack records no axis: only a remedy
that changes the outcome. The pack's remedy named install/minui.json,
refused the same way, and the list's fetched the same list again."""
works = [name for name, field in (("--system", "systems"), ("--core", "cores"),
("--region", "regions")) if field in recorded]
rest = f"Run it without {flag}" + (f", or narrow with {' and '.join(works)}"
if works else "") + "."
if pack_alone:
listed = LOCAL.pack.list_records if LOCAL.pack is not None else None
if listed is not None and axis not in listed:
# The pack says its platform's own list lacks the axis too.
return f" {display_name(plat)}'s own file list records no {axis} either. {rest}"
when = "which records them" if listed is not None else f"when that list records {axis}"
return f" Add --manifest FILE, a copy of install/{plat}.json, {when}. {rest}"
if recorded:
# A current list: the platform has no such axis.
return f" {rest}"
return _older_list_advice(plat)
def _narrow_to_emulators(
names: list[str], manifest: dict, plat: str,
files: list[dict], omitted: list[dict],
@@ -7613,25 +7676,26 @@ def _requested_narrowing(
"""The systems, cores and regions named on the command line, checked."""
if not (args.system or args.core or args.region):
return None
if not records_selection(files):
# Without the fields, a region filter would match nothing and install
# everything: refused rather than ignored.
print(
f"Error: the {plat} file list records no systems or regions, so "
"--system, --core and --region cannot narrow it.",
file=sys.stderr,
)
print(_older_list_advice(plat), file=sys.stderr)
sys.exit(1)
if LOCAL.pack is not None and LOCAL.manifest is None:
pack_alone = LOCAL.pack is not None and LOCAL.manifest is None
recorded = recorded_axes(files, omitted)
if pack_alone:
# A pack records the systems of its files, not their cores or
# regions: either filter would keep everything, and is refused.
for flag, given in (("--core", args.core), ("--region", args.region)):
if given:
print(f"Error: {LOCAL.pack.name} records the systems of its files, not "
f"what {flag} narrows by.", file=sys.stderr)
print(_older_list_advice(plat), file=sys.stderr)
sys.exit(1)
recorded = recorded & {"systems"}
asked = [(flag, axis) for flag, given, axis in (
("--system", args.system, "systems"), ("--core", args.core, "cores"),
("--region", args.region, "regions")) if given]
for flag, axis in asked:
if axis in recorded:
continue
# Without the field, a filter would match nothing and install
# everything: refused rather than ignored, by the axis the list
# lacks, with what works on it.
source = LOCAL.pack.name if pack_alone else f"the {plat} file list"
print(f"Error: {source} records no {axis}, so {flag} cannot narrow it.",
file=sys.stderr)
print(_narrowing_advice(plat, axis, flag, recorded, pack_alone), file=sys.stderr)
sys.exit(1)
choices = available_choices(files, omitted)
try:
systems = resolve_choices(
@@ -7780,6 +7844,10 @@ class PackSource:
self.members: dict[str, tuple[zipfile.ZipFile, zipfile.ZipInfo]] = {}
# The system titles and names the pack's list records, if any.
self.names: dict[str, dict] = {}
# What its platform's install list narrows by (systems, cores,
# regions), None for a pack that does not say: the remedy for an
# option the pack cannot apply names that list only when it can.
self.list_records: frozenset | None = None
try:
for path in self.files:
archive = zipfile.ZipFile(path)
@@ -7867,6 +7935,9 @@ class PackSource:
# A pack is a file anyone can hand over: an id or a name holding a
# control character would reach the terminal as an escape, and is
# dropped.
records = data.get("list_records")
if isinstance(records, list) and all(isinstance(axis, str) for axis in records):
self.list_records = frozenset(records) & {"systems", "cores", "regions"}
for key in ("system_titles", "system_names"):
held = data.get(key)
if isinstance(held, dict):
@@ -10260,9 +10331,9 @@ def _read_from_folder(label: str) -> FolderIndex:
return index
def _refuse_closed(dest: Path, named: str, then: str = "") -> None:
def _refuse_closed(dest: Path, named: str) -> None:
"""End the run, with its name, on a folder above dest this user cannot
open. then is a line said after the error, which it continues."""
open."""
closed = closed_folder(dest)
if closed is None:
return
@@ -10271,8 +10342,6 @@ def _refuse_closed(dest: Path, named: str, then: str = "") -> None:
print(f"Error: {screen_text(message)}. Give this user access to it, run as "
f"its owner, or choose another folder than the one {named}.",
file=sys.stderr)
if then:
print(f" {then}", file=sys.stderr)
sys.exit(1)
@@ -11478,9 +11547,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
named = (f"used by default, since {display_name(name)} was not found"
if path in assumed_dests
else f"{found_in[0]} names" if found_in else "detected here")
_refuse_closed(path, f"{named} (name another with --dest)",
"Nothing was installed. Name one platform with --platform "
"to install it alone." if len(platforms) > 1 else "")
# One setup by now: two found without questions stopped above.
_refuse_closed(path, f"{named} (name another with --dest)")
total_downloaded = 0
total_up_to_date = 0
@@ -13221,7 +13289,7 @@ def _plan_rows(state: TuiState) -> list[TuiRow]:
*([TuiRow("install", "Install anyway")] if rows else [])]
# --system given: the choice stays offered, the list widened when it is
# opened. With one system named it was neither shown nor changeable.
if records_selection((state.manifest or {}).get("files", [])) and (
if "systems" in recorded_axes((state.manifest or {}).get("files", [])) and (
len(state.systems) > 1 or state.cli_systems):
rows.append(TuiRow("systems", "Choose systems"))
rows.append(TuiRow("fromcard", "Copy the BIOS files from your old card or a folder"))
@@ -13341,8 +13409,10 @@ def _option_rows(state: TuiState) -> list[TuiRow]:
# The interface said "Name it with --save-dir" and had no row for it.
rows.append(TuiRow("savedir", f"Save folder: {_save_folder_words(state)}",
"--save-dir", style="path"))
if not LOCAL.offline:
# From a pack, or offline, nothing is downloaded (tui_audit_11 P3-16).
rows.append(TuiRow("jobs", f"Parallel downloads: {state.jobs}", "--jobs"))
rows += [
TuiRow("jobs", f"Parallel downloads: {state.jobs}", "--jobs"),
TuiRow("folder", f"BIOS folder: {_short_path(state.dest, state.home)}", "--dest",
style="path"),
TuiRow("from", f"Copy from: {copied}", "--from", style="path"),
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="f7928407478aae79bd9cd7740521e5c83b4fdd26046521c1c49b2f278c3bf22d"
DEFAULT_INSTALL_SHA256="00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 790778,
"sha256": "f7928407478aae79bd9cd7740521e5c83b4fdd26046521c1c49b2f278c3bf22d",
"size": 794767,
"sha256": "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 843327,
"sha256": "edf0948281063d1b9abf271f778a4486adf83b18924965eff68308e1540f25ab"
"size": 847316,
"sha256": "56cac10af6b7afaac0d1b3c0964d54caeabda46e75d35580dcedda264e2daac8"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 790778,
"sha256": "f7928407478aae79bd9cd7740521e5c83b4fdd26046521c1c49b2f278c3bf22d",
"size": 794767,
"sha256": "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 790778,
"sha256": "f7928407478aae79bd9cd7740521e5c83b4fdd26046521c1c49b2f278c3bf22d",
"size": 794767,
"sha256": "00ad7b4c93b5d69e17943e904121485da924ee681c2796a6bbd45aae6052e2b5",
"executable": false
},
{
+6
View File
@@ -43,6 +43,12 @@
"system_names": {
"type": "object",
"additionalProperties": { "type": "array", "items": { "type": "string" } }
},
"list_records": {
"description": "What the platform's install list narrows by: the installer names that list as the remedy for --core or --region given with the pack alone only when it records them.",
"type": "array",
"uniqueItems": true,
"items": { "enum": ["systems", "cores", "regions"] }
}
}
}
+17 -1
View File
@@ -4759,11 +4759,25 @@ def pack_systems(install_manifest: dict) -> dict:
("the file list comes from an older release") and offered no Choose
systems, the route of a handheld without Wi-Fi.
"""
files = install_manifest.get("files", [])
entries = [*files, *install_manifest.get("omitted_files", [])]
# What the install list narrows by, the installer's recorded_axes: with
# the pack alone it names that list as the way to --core and --region
# only when the list records them. It named install/misterfpga.json for
# --core, a list that records no core.
records = {"systems"} if any("systems" in entry for entry in entries) else set()
if any(copy.get("cores") for entry in files
for copy in [entry, *entry.get("alternatives", []),
*entry.get("slot_choices", [])]):
records.add("cores")
if records or any("regions" in entry for entry in entries):
records.add("regions")
return {
"files": {entry["dest"]: list(entry.get("systems") or [])
for entry in install_manifest.get("files", []) if "systems" in entry},
for entry in files if "systems" in entry},
"system_titles": dict(install_manifest.get("system_titles") or {}),
"system_names": dict(install_manifest.get("system_names") or {}),
"list_records": sorted(records),
}
@@ -4779,6 +4793,8 @@ def annotate_pack_systems(manifest: dict, systems: dict | None) -> None:
for key in ("system_titles", "system_names"):
if systems.get(key):
manifest[key] = systems[key]
if "list_records" in systems:
manifest["list_records"] = systems["list_records"]
def inject_manifest(zip_path: str, manifest: dict) -> None:
+25 -13
View File
@@ -370,18 +370,28 @@ class RefusedBeforeAnyInstall(unittest.TestCase):
(tmp / "retroarch.cfg").write_text("")
(tmp / "cores").mkdir()
(tmp / "cores" / "mgba_libretro.so").write_bytes(b"")
with mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=detected), \
mock.patch.object(install, "fetch_manifest") as fetch, \
mock.patch.object(sys, "argv", ["install.py", "--installed-cores", "--no-input"]), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()) as err:
with self.assertRaises(SystemExit) as stop:
install.main()
self.assertEqual(stop.exception.code, 1)
self.assertIn("batocera ships its cores", err.getvalue())
fetch.assert_not_called()
# Two setups found stop a run without questions before either is
# read (code_audit_18 P2-3): the refusal is checked with Batocera
# named, and with both found nothing is fetched either.
for argv, said in (
(["--platform", "batocera", "--dest", str(tmp / "bios")],
"batocera ships its cores"),
([], "2 setups were found here")):
with self.subTest(argv=argv), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=detected), \
mock.patch.object(install, "_mounted_cards", return_value=[]), \
mock.patch.object(install, "fetch_manifest") as fetch, \
mock.patch.object(sys, "argv", ["install.py", "--installed-cores",
"--no-input", *argv]), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()) as err:
with self.assertRaises(SystemExit) as stop:
install.main()
self.assertEqual(stop.exception.code, 1)
self.assertIn(said, err.getvalue())
fetch.assert_not_called()
class AnUnknownTargetStopsTheRunFirst(unittest.TestCase):
@@ -393,12 +403,14 @@ class AnUnknownTargetStopsTheRunFirst(unittest.TestCase):
self.addCleanup(shutil.rmtree, tmp, True)
detected = [("emudeck", tmp / "bios"), ("retroarch", tmp / "system")]
lists = {"emudeck": {"steamos": {"cores": []}}, "retroarch": {"x86_64": {"cores": []}}}
# Listed for both platforms: a listing reads every list found.
with mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.object(install, "detect_frontends", return_value=[]), \
mock.patch.object(install, "detect_platforms", return_value=detected), \
mock.patch.object(install, "fetch_targets", side_effect=lists.get), \
mock.patch.object(install, "fetch_manifest") as fetch, \
mock.patch.object(sys, "argv", ["install.py", "--target", "steamos", "--no-input"]), \
mock.patch.object(sys, "argv", ["install.py", "--target", "steamos",
"--list-systems", "--no-input"]), \
contextlib.redirect_stdout(io.StringIO()), \
contextlib.redirect_stderr(io.StringIO()) as err:
with self.assertRaises(SystemExit) as stop:
+6 -4
View File
@@ -612,7 +612,8 @@ class TheOneLinerNarrows(unittest.TestCase):
with FakeRepository(selection=False) as repo:
proc = repo.run("--region", "us")
self.assertEqual(proc.returncode, 1)
self.assertIn("records no systems or regions", proc.stderr)
self.assertIn("records no regions, so --region cannot narrow it", proc.stderr)
self.assertIn("older release", proc.stderr)
self.assertEqual(repo.installed(), [])
def test_a_pinned_ref_is_named_as_the_way_out(self):
@@ -675,18 +676,19 @@ class TheOneLinerNarrows(unittest.TestCase):
@unittest.skipUnless(shutil.which("script"), "script(1) provides the terminal")
def test_the_custom_mode_at_a_terminal(self):
"""Typed answers: customise, PlayStation only, no core, US first."""
"""Typed answers: customise, PlayStation only, US first. Its one
core is not asked about (tui_audit_11 P3-10)."""
with FakeRepository() as repo:
command = " ".join(shlex.quote(part) for part in repo.command())
proc = subprocess.run(
["script", "-qec", command, "/dev/null"],
input="c\n2\n\n2\n\n", env=repo.env(), capture_output=True,
input="c\n2\n2\n\n", env=repo.env(), capture_output=True,
text=True, timeout=60, check=False,
)
out = proc.stdout
self.assertEqual(proc.returncode, 0, out + proc.stderr)
self.assertIn("choose systems, cores and regions", out)
self.assertIn("pcsx_rearmed", out)
self.assertNotIn("Cores:", out)
self.assertIn("--system sony-playstation --region north-america", out)
self.assertEqual(repo.installed(), ["pcsx/extra.bin", "scph5501.bin"])
+134 -1
View File
@@ -7432,7 +7432,7 @@ class AuditSeventeenAndTen(unittest.TestCase):
with contextlib.redirect_stderr(err), self.assertRaises(SystemExit):
install._requested_narrowing(_args(system=["nintendo-gba"]), "retroarch",
install.LOCAL.pack.manifest("retroarch")["files"], [])
self.assertIn("add --manifest FILE, a copy of install/retroarch.json", err.getvalue())
self.assertIn("Add --manifest FILE, a copy of install/retroarch.json", err.getvalue())
self.assertNotIn("older release", err.getvalue())
def test_a_pack_cannot_write_escapes_through_its_system_names(self):
@@ -8660,6 +8660,139 @@ class AuditEighteenAndEleven(unittest.TestCase):
["Onion SD card (ONION, no BIOS folder)",
"Onion SD card (OLDONION, 6 BIOS files)"])
# tui_audit_11 P2-1: a list or a pack that records some axes only.
def minui(self, plat: str = "minui", systems: bool = False, cores: bool = True,
regions: bool = True) -> tuple[Path, Path, dict]:
"""A list of plat's shape (the MinUI family records cores and regions,
no system), the pack built from it and the files' payloads."""
from test_install_offline import GBA, JP, US # noqa: PLC0415
payloads = {"Bios/MGBA/gba_bios.bin": (GBA, "nintendo-gba", "mgba", None),
"Bios/PS/scph5501.bin": (US, "sony-playstation", "pcsx_rearmed",
"north-america"),
"Bios/PS/scph5500.bin": (JP, "sony-playstation", "pcsx_rearmed", "japan")}
files = []
for dest, (data, system, core, region) in payloads.items():
entry = {"dest": dest, "sha1": hashlib.sha1(data).hexdigest(), "size": len(data),
"repo_path": f"bios/{dest}", "cores": [core] if cores else None}
if systems:
entry["systems"] = [system]
if regions and region:
entry.update(regions=[region], region_groups=["ps"])
files.append(entry)
listing = self.root / f"{plat}.json"
install_list = {"manifest_version": 2, "platform": plat, "files": files}
listing.write_text(json.dumps(install_list))
sys.path.insert(0, str(REPO_ROOT / "scripts"))
self.addCleanup(sys.path.remove, str(REPO_ROOT / "scripts"))
import generate_pack # noqa: PLC0415
pack_list = {"files": [{"path": dest, "sha1": entry["sha1"], "size": entry["size"]}
for dest, entry in zip(payloads, files)]}
generate_pack.annotate_pack_systems(pack_list, generate_pack.pack_systems(install_list))
name = {"minui": "MinUI", "misterfpga": "MiSTer_FPGA"}[plat]
pack = self.root / f"{name}_v1_BIOS_Pack.zip"
with zipfile.ZipFile(pack, "w") as archive:
for dest, (data, *_rest) in payloads.items():
archive.writestr(dest, data)
archive.writestr("manifest.json", json.dumps(pack_list))
return listing, pack, pack_list
def narrowed(self, plat: str, **given) -> tuple[int, str, list]:
"""(exit code, what was said, the files kept) of a --dry-run narrowed
as given."""
dest = self.root / "dest"
if not dest.exists():
make_card(dest, "MinUI")
stream = io.StringIO()
code = 0
with mock.patch.object(install.EVENTS, "stream", stream), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
contextlib.redirect_stdout(io.StringIO()) as out, \
contextlib.redirect_stderr(io.StringIO()) as err:
try:
install._install(_args(platform=plat, dest=dest, dry_run=True, events=True,
no_input=True, declared_only=False, **given),
io.StringIO())
except SystemExit as stop:
code = stop.code
finally:
install.LOCAL.reset()
kept = [json.loads(line)["dest"] for line in stream.getvalue().splitlines()
if '"event": "file"' in line]
return code, out.getvalue() + err.getvalue(), kept
def test_a_list_without_systems_narrows_by_what_it_records(self):
"""tui_audit_11 P2-1: install/minui.json records cores and regions,
no system, and all three options were refused with a remedy that
fetched the same list."""
listing, _pack, _listed = self.minui()
code, text, kept = self.narrowed("minui", manifest=listing, core=["mgba"])
self.assertEqual(code, 0, text)
self.assertEqual(kept, ["Bios/MGBA/gba_bios.bin"])
code, text, kept = self.narrowed("minui", manifest=listing, region=["us"])
self.assertEqual(code, 0, text)
self.assertNotIn("Bios/PS/scph5500.bin", kept)
code, text, _kept = self.narrowed("minui", manifest=listing,
system=["nintendo-gba"])
self.assertEqual(code, 1)
self.assertIn("records no systems, so --system cannot narrow it", text)
self.assertIn("Run it without --system, or narrow with --core and --region", text)
self.assertNotIn("one-line installer", text)
# The remedy followed: what it names works.
code, text, _kept = self.narrowed("minui", manifest=listing, core=["mgba"],
region=["us"])
self.assertEqual(code, 0, text)
def test_a_pack_names_its_list_only_when_that_list_records_it(self):
"""tui_audit_11 P2-1: a MinUI pack sent --core to install/minui.json,
refused the same way, and a MiSTer pack to install/misterfpga.json,
which records no core. The pack now records what its list narrows
by, and each remedy changes the outcome."""
listing, pack, listed = self.minui()
self.assertEqual(listed["list_records"], ["cores", "regions"])
code, text, _kept = self.narrowed("minui", from_pack=[pack], core=["mgba"])
self.assertEqual(code, 1)
self.assertIn("Add --manifest FILE, a copy of install/minui.json, which records them",
text)
code, text, kept = self.narrowed("minui", from_pack=[pack], manifest=listing,
core=["mgba"])
self.assertEqual((code, kept), (0, ["Bios/MGBA/gba_bios.bin"]), text)
mister_list, mister, listed = self.minui("misterfpga", systems=True, cores=False,
regions=False)
self.assertEqual(listed["list_records"], ["regions", "systems"])
code, text, _kept = self.narrowed("misterfpga", from_pack=[mister], core=["x"])
self.assertEqual(code, 1)
self.assertIn("MiSTer FPGA's own file list records no cores either. Run it without "
"--core, or narrow with --system.", text)
self.assertNotIn("--manifest", text)
code, text, kept = self.narrowed("misterfpga", from_pack=[mister],
system=["nintendo-gba"])
self.assertEqual((code, kept), (0, ["Bios/MGBA/gba_bios.bin"]), text)
self.assertTrue(mister_list.exists())
def test_line_mode_offers_what_can_be_chosen(self):
"""tui_audit_11 P3-10: "c) choose systems, cores and regions" with a
pack that records systems only, and "Cores: 1) azahar" asked with
one answer."""
_listing, _pack, listed = self.minui("misterfpga", systems=True, cores=False,
regions=False)
files = [{"dest": f["path"], "sha1": f["sha1"], "size": f["size"],
"repo_path": "x", "systems": f["systems"]} for f in listed["files"]]
self.assertEqual(install.narrowing_words(files, []), "systems")
listing, _pack, _listed = self.minui()
minui = json.loads(listing.read_text())["files"]
self.assertEqual(install.narrowing_words(minui, []), "cores and regions")
one_core = [{**entry, "systems": ["nintendo-3ds"], "cores": ["azahar"]}
for entry in minui[:1]]
with mock.patch.object(install, "_ask", side_effect=["", ""]) as asked, \
contextlib.redirect_stdout(io.StringIO()) as out:
install._choose_narrowing(one_core, [], {}, None)
self.assertNotIn("Cores:", out.getvalue())
self.assertEqual(asked.call_count, 0)
def test_the_card_marker_table_is_written_from_the_rules(self):
"""code_audit_18 P3-6, tui_audit_11 P2-2: the table left out the
SHARE drives. It is generated: every card rule and SHARE drive file
+13 -1
View File
@@ -615,7 +615,9 @@ It keeps what those cores read and drops the systems none of them emulates,
the rule `--installed-cores` applies.
`c` walks through three numbered lists, each answered with numbers such as
`1,4,7-9`, or Enter to keep everything on that axis.
`1,4,7-9`, or Enter to keep everything on that axis. The letter names the
lists it asks (`c) choose systems` with a MiSTer pack), and a list with one
answer is not asked.
- **Systems.** The consoles and computers the platform declares, titled and
ordered as the full screen's Choose systems lists them, the id `--system`
@@ -637,6 +639,16 @@ without questions, every other option of the run in it, for instance
--system sony-playstation --region north-america`. Enter then installs, and
`c` chooses again.
An option is refused by the axis the list does not record, with what works
instead. The MinUI, NextUI and MyMinUI lists record cores and regions and no
system: `--core` and `--region` narrow them, `--system` is refused with
`Run it without --system, or narrow with --core and --region`. A pack records
the systems of its files and what its platform's list narrows by: with the
pack alone, `--core` or `--region` names that list (`--manifest FILE`, a copy
of `install/minui.json`) only when it records them, and MiSTer FPGA's pack
says its list records no cores. A list from an older release records none of
the three, and the one-line installer fetches the current one.
The same choices on the command line:
```bash