diff --git a/install.ps1 b/install.ps1 index b8c036982..50bed9a95 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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 = "5f933d4ab032e6e401341818a35eeee9b4018932fde7ad5111fa789375fb350a" + $defaultInstallSha256 = "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 6ff0322db..13022a7ad 100644 --- a/install.py +++ b/install.py @@ -6122,11 +6122,11 @@ def _report_outcome( # A failure the progress already named on its FAILED line is not named # again: line mode listed each one twice (tui_audit_17 P3-7). unsaid = [failure for failure in failures or [] if failure[0] not in SAID_FAILED] - if unsaid and len(unsaid) > 1 and len(reasons) == 1 and reasons != {""}: + if failures and len(failures) > 1 and len(reasons) == 1 and reasons != {""}: # One cause, said once: a read-only folder listed ten of its files # and "... and 266 more" above the advice that fixes them all. - say(f" failed: {_plural(len(unsaid), 'file')} ({reasons.pop()}), " - f"first {unsaid[0][0]}") + said = f", first {unsaid[0][0]}" if unsaid else "" + say(f" failed: {_plural(len(failures), 'file')} ({reasons.pop()}){said}") else: for dest, reason in unsaid[:10]: say(f" failed: {dest}" + (f" ({reason})" if reason else "")) @@ -15547,7 +15547,7 @@ def core_count_words(cores: list[str], base: list[str]) -> str: def _tui_systems_chosen(state: TuiState) -> list[str]: """The systems the run is narrowed to, [] for every system.""" - return [value for flag, value in _tui_system_flags(state) if value] + return [value for _flag, value in _tui_system_flags(state) if value] def _chosen_cores(state: TuiState) -> set[str] | None: diff --git a/install.sh b/install.sh index d8296c900..c2f6353e9 100755 --- a/install.sh +++ b/install.sh @@ -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="5f933d4ab032e6e401341818a35eeee9b4018932fde7ad5111fa789375fb350a" +DEFAULT_INSTALL_SHA256="5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 31d0df546..14b41d3db 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -27,8 +27,8 @@ { "dest": "roms/ports/retrobios/install.py", "repo_path": "install.py", - "size": 996363, - "sha256": "5f933d4ab032e6e401341818a35eeee9b4018932fde7ad5111fa789375fb350a", + "size": 996397, + "sha256": "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1049540, - "sha256": "a7a9c2f9b7d018d0aca34731957a13d9d8ab22ea747381e6750d45abbfb70390" + "size": 1049574, + "sha256": "30bbe8a0f78cc7b6bb37b9290c69e2244e0b9217339d9ea695f45c0d7815a263" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 996363, - "sha256": "5f933d4ab032e6e401341818a35eeee9b4018932fde7ad5111fa789375fb350a", + "size": 996397, + "sha256": "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 996363, - "sha256": "5f933d4ab032e6e401341818a35eeee9b4018932fde7ad5111fa789375fb350a", + "size": 996397, + "sha256": "5b8bd7b486d236ee1d00439b2bdd4f6fefcf20112abe3b0f1037e38426d80c15", "executable": false }, { diff --git a/schemas/install-events.schema.json b/schemas/install-events.schema.json index 62db8c06b..f43b4ebf8 100644 --- a/schemas/install-events.schema.json +++ b/schemas/install-events.schema.json @@ -17,6 +17,7 @@ "scanned", "version", "choices", + "set_aside", "file", "system", "checking", @@ -58,6 +59,9 @@ { "$ref": "#/$defs/choices" }, + { + "$ref": "#/$defs/set_aside" + }, { "$ref": "#/$defs/file" }, @@ -299,6 +303,29 @@ } } }, + "set_aside": { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "platform", + "dests" + ], + "properties": { + "event": { + "const": "set_aside" + }, + "platform": { + "type": "string" + }, + "dests": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "file": { "type": "object", "additionalProperties": false, diff --git a/tests/netguard/sitecustomize.py b/tests/netguard/sitecustomize.py index 2aa5f732d..d7ab23fb8 100644 --- a/tests/netguard/sitecustomize.py +++ b/tests/netguard/sitecustomize.py @@ -27,7 +27,9 @@ _SAVED: dict = {} def _refuse(host: object) -> None: where = os.environ.get(NETLOG) - if where: + # A name under .invalid never resolves (RFC 2606): a test names one to + # fail on purpose, refused here before its DNS query leaves. + if where and not str(host).endswith(".invalid"): with open(where, "a", encoding="utf-8") as log: log.write(f"{os.getpid()} {host} {' '.join(sys.argv)[:300]}\n") raise OSError(errno.ENETUNREACH, f"a test makes no request to {host}") diff --git a/tests/test_install.py b/tests/test_install.py index 6ddeebd43..23bcb7efd 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -10,6 +10,7 @@ import io import json import os import stat +import select import shlex import re import shutil @@ -4182,17 +4183,35 @@ class AQuestionLeftWithCtrlDOrCtrlCIsAStop(unittest.TestCase): home = Path(_tempdir(self)) master, slave = pty.openpty() + # Its output on the terminal too: piped, nothing is asked, and the + # test skipped on every machine. proc = subprocess.Popen( - [sys.executable, "install.py"], cwd=REPO_ROOT, stdin=slave, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - env={**os.environ, "HOME": str(home), "RETROBIOS_OS": "linux"}, + [sys.executable, "install.py", "--plain"], cwd=REPO_ROOT, stdin=slave, + stdout=slave, stderr=slave, + # No request leaves the machine: the standalone emulators' lists + # come from the network (code_audit_24 P3-13). + env={**os.environ, "HOME": str(home), "RETROBIOS_OS": "linux", + "RETROBIOS_INSTALL_OFFLINE": "1"}, ) os.close(slave) self.addCleanup(os.close, master) - time.sleep(3) + out = "" + deadline = time.monotonic() + 60 + while "Which setup?" not in out and proc.poll() is None \ + and time.monotonic() < deadline: + if select.select([master], [], [], 0.2)[0]: + out += os.read(master, 65536).decode("utf-8", "replace") os.write(master, b"\x04") - out, _ = proc.communicate(timeout=60) - if "Choose the platform" not in out: + proc.wait(timeout=60) + while select.select([master], [], [], 0.2)[0]: + try: + chunk = os.read(master, 65536) + except OSError: + break + if not chunk: + break + out += chunk.decode("utf-8", "replace") + if "Which setup?" not in out: self.skipTest("a platform was detected on this machine") self.assertEqual(proc.returncode, 130, out) self.assertIn("Stopped", out) @@ -5211,6 +5230,8 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree): # The systems Choose systems lists, before the files are read. {"event": "choices", "platform": "retroarch", "systems": ["sys-a", "sys-b", "other"]}, + # Nothing set aside before the choice (code_audit_24 P3-3). + {"event": "set_aside", "platform": "retroarch", "dests": []}, # a.bin and b.bin are in place: the first read and the last. {"event": "checking", "files_done": 1, "files_total": 2}, {"event": "checking", "files_done": 2, "files_total": 2}, diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 802d09079..511bebc7f 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -823,9 +823,16 @@ class Widgets(unittest.TestCase): state = fresh_state() press(state, "up") self.assertEqual(state.focus["setup"], 2) - state = self.systems_state(20) + state = fresh_state() + state.stack = ["setup", "platforms"] press(state, "up") - self.assertEqual(state.focus["systems"], 0) + self.assertEqual(state.focus["platforms"], 0) + # A list of ticks wraps at any length: None and Done, at its end, + # are a few presses up from its first item (tui_audit_17 P3-1). + state = self.systems_state(20) + state.focus["systems"] = 0 + press(state, "up") + self.assertEqual(install.tui_rows(state)[state.focus["systems"]].action, "done") def test_filter_ignores_case_and_accents(self): state = self.systems_state(3) @@ -6777,7 +6784,12 @@ class AuditSevenScreens(unittest.TestCase): with SaveDirectoryRepository() as repo: argv = ["install.py", "--platform", "retroarch", "--dest", str(repo.dest), "--manifest", str(repo.root / "install" / "retroarch.json")] + # The files come from the repository on the loopback: the module + # read its base before the environment named it (code_audit_24 + # P3-13). + served = repo.env()["RETROBIOS_BASE_URL"] + "/{path}" with mock.patch.dict(os.environ, repo.env()), \ + mock.patch.object(install, "RAW_FILE_URL", served), \ mock.patch.object(sys, "argv", argv), \ mock.patch.object(sys.stdin, "isatty", return_value=True), \ mock.patch.object(install, "_is_terminal", return_value=True), \