mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
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>
135 lines
5.5 KiB
Python
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")
|