Files
LEDMatrix/test/test_install_reboot_is_last.py
T
ChuckandClaude Opus 5.5 05deb1ee7d feat(install): support Raspberry Pi OS Bookworm (Python 3.11) alongside Trixie (3.13) (#689)
The installer and scripts/check_system_compatibility.sh share one set of OS rules (scripts/install/lib_os.sh): Bookworm (Debian 12, Python 3.11) and Trixie (Debian 13, Python 3.13) are supported, python3 older than 3.11 stops the install before anything changes, and dhcpcd gets a warning with directions. setcap targets /usr/bin/python3, the apt fallback honours the requirement floors, and the desktop check no longer misreads under pipefail. CI runs the unit and plugin-safety suites on 3.11 and 3.13 (tooling jobs on 3.13); mypy targets 3.11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:35:13 -04:00

135 lines
5.5 KiB
Python

"""first_time_install.sh prints its completion summary before it reboots.
With -y (and so with the one-shot `curl | bash` installer, which always
passes -y) the reboot used to be issued ~180 lines before the "Installation
Complete / Web UI Access" summary. `reboot` returns at once and the script
carried on printing while the system went down, so the SSH session usually
dropped before the user saw the web UI address.
first_time_install.sh exits on anything but Raspberry Pi OS Bookworm or Trixie
before it parses its arguments, so the behavioural test runs only the tail of the
script -- from the summary to the end -- with systemctl, nmcli, hostname, ip
and reboot stubbed.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
FIRST_TIME = ROOT / "first_time_install.sh"
SUMMARY_START = 'echo "Installation Complete!"'
def _text():
return FIRST_TIME.read_text(encoding="utf-8").replace("\r\n", "\n")
def test_every_reboot_comes_after_the_summary():
text = _text()
summary = text.index(SUMMARY_START)
lines = text.splitlines()
reboots = [i for i, line in enumerate(lines) if line.strip() == "reboot"]
assert reboots, "no reboot call found"
summary_line = text[:summary].count("\n")
enjoy_line = text[:text.index('echo "Enjoy your LED Matrix display!"')].count("\n")
assert all(i > enjoy_line > summary_line for i in reboots), (
f"reboot at line(s) {[i + 1 for i in reboots]} runs before the summary "
f"(line {summary_line + 1}) has finished printing")
def _tail():
"""The script from the summary header to the end, header rule included."""
text = _text()
start = text.rindex('echo "=========================================="', 0,
text.index(SUMMARY_START))
return text[start:]
_POSIX = pytest.mark.skipif(sys.platform == "win32" or shutil.which("bash") is None,
reason="needs a POSIX bash")
def _run(tmp_path, env_extra, nmcli_active_line=True, stdin="", hostapd_active=False):
stubs = tmp_path / "stubs"
stubs.mkdir()
log = tmp_path / "calls.log"
active = 'echo "yes:HomeNet"' if nmcli_active_line else ":"
hostapd = 'case "$*" in *"is-active --quiet hostapd"*) exit 0 ;; esac\n' if hostapd_active else ""
bodies = {
"reboot": f'#!/bin/sh\necho REBOOT-CALLED\necho reboot >> "{log}"\n',
"systemctl": f"#!/bin/sh\n{hostapd}exit 3\n",
"hostname": '#!/bin/sh\necho "192.168.1.50 fe80::1"\n',
"ip": "#!/bin/sh\nexit 1\n",
# device status -> one connected wifi device; device wifi -> active line
"nmcli": ('#!/bin/sh\ncase "$*" in\n'
' *"device status"*) echo "wlan0:wifi:connected" ;;\n'
f' *"device wifi"*) {active} ;;\n'
"esac\n"),
}
for name, body in bodies.items():
(stubs / name).write_text(body)
(stubs / name).chmod(0o755)
script = "\n".join([
"set -Eeuo pipefail",
"on_error() { echo \"ERR-TRAP line $1\" >&2; exit 1; }",
"trap 'on_error $LINENO' ERR",
"PROJECT_ROOT_DIR=/home/pi/LEDMatrix",
"ASSUME_YES=${ASSUME_YES:-0}",
"SKIP_REBOOT_PROMPT=${SKIP_REBOOT_PROMPT:-0}",
_tail(),
])
env = dict(os.environ, PATH=os.pathsep.join([str(stubs), "/usr/bin", "/bin"]), **env_extra)
result = subprocess.run(["bash", "-c", script], env=env, input=stdin,
capture_output=True, text=True)
calls = log.read_text().splitlines() if log.exists() else []
return result, calls
@_POSIX
@pytest.mark.parametrize("nmcli_active_line", [True, False], ids=["ssid", "no-ssid"])
def test_assume_yes_prints_the_summary_then_reboots(tmp_path, nmcli_active_line):
result, calls = _run(tmp_path, {"ASSUME_YES": "1"}, nmcli_active_line)
out = result.stdout
assert result.returncode == 0, out + result.stderr
assert calls == ["reboot"]
for text in ("Installation Complete!", "Web UI Access:", "http://192.168.1.50:5000",
"Enjoy your LED Matrix display!"):
assert out.index(text) < out.index("REBOOT-CALLED"), text
assert "Password: ledmatrix123" not in out
@_POSIX
def test_setup_access_point_is_described_as_open(tmp_path):
"""wifi_manager creates the setup AP with no security ("No password" on
the panel); the summary used to print a password it does not have."""
result, _ = _run(tmp_path, {"ASSUME_YES": "1"}, hostapd_active=True)
assert result.returncode == 0, result.stdout + result.stderr
assert "AP Mode is ACTIVE" in result.stdout
assert "Open network, no password" in result.stdout
assert "Password:" not in result.stdout
@_POSIX
def test_no_reboot_prompt_prints_the_summary_and_does_not_reboot(tmp_path):
result, calls = _run(tmp_path, {"ASSUME_YES": "1", "SKIP_REBOOT_PROMPT": "1"})
assert result.returncode == 0, result.stdout + result.stderr
assert calls == []
assert "Enjoy your LED Matrix display!" in result.stdout
assert "Skipping reboot prompt" in result.stdout
@_POSIX
@pytest.mark.parametrize("answer,expected", [("y", ["reboot"]), ("n", [])])
def test_interactive_prompt_comes_after_the_summary(tmp_path, answer, expected):
result, calls = _run(tmp_path, {}, stdin=answer)
assert result.returncode == 0, result.stdout + result.stderr
assert calls == expected
out = result.stdout
assert "Enjoy your LED Matrix display!" in out
if expected:
assert out.index("Enjoy your LED Matrix display!") < out.index("REBOOT-CALLED")