Files
aic8800d80/tests/installer/test_install.py
Shen Mintaoandrtk94 bee40068b9 fix(installer): test-build before replacing the active installation
Adapt the preflight protection from #128 without its firmware backup or
other installer policy changes. Build a clean private copy against the
running kernel before firmware replacement or DKMS removal. Treat log
initialization failure as an error and add mocked main-flow regressions.

Co-authored-by: rtk94 <richardtknepp@gmail.com>
2026-11-02 14:31:50 +08:00

262 lines
11 KiB
Python

#!/usr/bin/env python3
"""Run installer regressions with mocked system commands and temporary firmware.
Usage: python3 tests/installer/test_install.py [path/to/install.sh]
Requires Bash; does not install packages, load modules, or need root.
"""
from pathlib import Path
import re
import shlex
import subprocess
import sys
import tempfile
import unittest
INSTALLER = Path(sys.argv.pop(1)) if len(sys.argv) > 1 else Path(__file__).resolve().parents[2] / "install.sh"
SOURCE = INSTALLER.read_text(encoding="utf-8")
SHELL_OPTIONS = next(line for line in SOURCE.splitlines() if line.startswith("set -"))
class InstallerTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="aic-installer-test-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.firmware = self.root / "firmware"
self.firmware.mkdir()
def run_installer_function(self, name, stubs="", after=""):
# Extract only the functions under test; never execute main().
names = ["run_logged", "cleanup_on_error", name]
if name == "main" and re.search(r"(?m)^preflight_build\(\)", SOURCE):
names.insert(-1, "preflight_build")
functions = []
for function in names:
match = re.search(r"(?ms)^" + function + r"\(\) \{.*?^\}", SOURCE)
self.assertIsNotNone(match, function)
functions.append(match.group())
script = "\n".join(functions)
script = script.replace("/lib/firmware", str(self.firmware))
script = script.replace("/sys/firmware/efi", str(self.root / "efi"))
setup = f"""
{SHELL_OPTIONS}
SCRIPT_DIR={shlex.quote(str(self.root))}
LOG_FILE="$SCRIPT_DIR/install.log"
SRC_DIR="$SCRIPT_DIR/installed-source"
TMPDIR="$SCRIPT_DIR/tmp"
export TMPDIR
mkdir -p "$TMPDIR"
DRV_NAME=aic8800
DRV_VERSION=1.0.0
MODULE_NAME=aic8800_fdrv
print_step() {{ echo "$*"; }}
print_info() {{ echo "$*"; }}
print_success() {{ echo "$*"; }}
print_warning() {{ echo "$*"; }}
print_error() {{ echo "$*"; }}
log_message() {{ :; }}
dkms() {{ echo 'aic8800/1.0.0: installed'; }}
lsmod() {{ :; }}
aic_zlp_target_present() {{ return 1; }}
iwconfig() {{ :; }}
bluetoothctl() {{ :; }}
kernel_build_tree_available() {{ return 0; }}
is_volumio() {{ return 1; }}
cleanup_legacy_bluetooth_config() {{ echo LEGACY_CLEANUP; }}
"""
return subprocess.run(
["bash", "-c", setup + script + "\ntrap cleanup_on_error ERR\n" + stubs + "\n" + name + after],
input="", text=True, capture_output=True, timeout=15,
)
def test_verification_reaches_final_instructions(self):
for count in (0, 1, 3):
with self.subTest(firmware_count=count):
for index in range(count):
(self.firmware / f"aic8800-{index}").mkdir(exist_ok=True)
result = self.run_installer_function("verify_installation", after="\necho FINAL_INSTRUCTIONS")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("FINAL_INSTRUCTIONS", result.stdout)
if count:
self.assertIn(f"Firmware installed for {count} chip variant(s)", result.stdout)
def test_pacman_uses_existing_database(self):
result = self.run_installer_function(
"install_dependencies", 'pacman() { echo "PACMAN $*"; }', " pacman",
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
calls = [line for line in result.stdout.splitlines() if line.startswith("PACMAN ")]
self.assertEqual(calls, ["PACMAN -S --needed --noconfirm dkms base-devel mokutil util-linux usb_modeswitch"])
def test_pacman_failure_stops_with_upgrade_guidance(self):
result = self.run_installer_function(
"install_dependencies", 'pacman() { return 7; }', " pacman\necho UNEXPECTED_SUCCESS",
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("sudo pacman -Syu", result.stdout)
self.assertNotIn("UNEXPECTED_SUCCESS", result.stdout)
self.assertIn("Installation failed", result.stdout)
def test_secure_boot_eof_cancels_cleanly(self):
result = self.run_installer_function(
"check_secure_boot", 'mokutil() { echo "SecureBoot enabled"; }', "\necho UNEXPECTED_CONTINUE",
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Installation cancelled", result.stdout)
self.assertNotIn("UNEXPECTED_CONTINUE", result.stdout)
def check_invalid_firmware(self):
old = self.firmware / "aic8800D80" / "old.bin"
old.parent.mkdir()
old.write_bytes(b"installed firmware")
result = self.run_installer_function("install_firmware")
self.assertNotEqual(result.returncode, 0)
self.assertEqual(old.read_bytes(), b"installed firmware")
self.assertNotIn("LEGACY_CLEANUP", result.stdout)
def test_missing_firmware_preserves_installation(self):
self.check_invalid_firmware()
def test_empty_firmware_tree_preserves_installation(self):
(self.root / "fw").mkdir()
self.check_invalid_firmware()
def test_empty_variant_preserves_installation(self):
(self.root / "fw" / "aic8800D80").mkdir(parents=True)
self.check_invalid_firmware()
def test_valid_firmware_replaces_old_variant(self):
old = self.firmware / "aic8800D80" / "old.bin"
old.parent.mkdir()
old.write_bytes(b"old")
new = self.root / "fw" / "aic8800D80" / "new.bin"
new.parent.mkdir(parents=True)
new.write_bytes(b"new")
result = self.run_installer_function("install_firmware")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertFalse(old.exists())
self.assertEqual((old.parent / "new.bin").read_bytes(), b"new")
def prepare_preflight(self):
source = self.root / "drivers" / "aic8800" / "source.c"
source.parent.mkdir(parents=True)
source.write_bytes(b"source")
old = self.firmware / "aic8800D80" / "old.bin"
old.parent.mkdir()
old.write_bytes(b"installed firmware")
installed = self.root / "installed-source" / "revision"
installed.parent.mkdir()
installed.write_bytes(b"installed source")
(self.root / "dkms-state").write_bytes(b"installed modules")
return r'''
uname() { echo test-kernel; }
check_root() { :; }
check_secure_boot() { :; }
detect_package_manager() { DETECTED_PKG_MANAGER=apt; }
install_dependencies() { :; }
check_kernel_build_tree() { :; }
create_dkms_conf() { :; }
refresh_initramfs() { :; }
load_module() { :; }
verify_installation() { :; }
show_final_instructions() { echo FINAL_INSTRUCTIONS; }
make() {
echo "MAKE $*" >> "$SCRIPT_DIR/events"
[ "$(cat "$2/source.c")" = source ] || return 99
case "$*" in
*' clean') return "${MAKE_CLEAN_STATUS:-0}" ;;
esac
echo built > "$2/source.c"
return "${MAKE_BUILD_STATUS:-0}"
}
install_firmware() {
echo FIRMWARE >> "$SCRIPT_DIR/events"
echo changed > "$SCRIPT_DIR/firmware/aic8800D80/old.bin"
}
install_via_dkms() {
echo DKMS >> "$SCRIPT_DIR/events"
echo changed > "$SRC_DIR/revision"
echo changed > "$SCRIPT_DIR/dkms-state"
}
'''
def assert_installation_preserved(self, result):
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual((self.firmware / "aic8800D80/old.bin").read_bytes(), b"installed firmware")
self.assertEqual((self.root / "installed-source/revision").read_bytes(), b"installed source")
self.assertEqual((self.root / "dkms-state").read_bytes(), b"installed modules")
events = self.root / "events"
if events.exists():
self.assertNotIn("FIRMWARE", events.read_text())
self.assertNotIn("DKMS", events.read_text())
self.assertNotIn("FINAL_INSTRUCTIONS", result.stdout)
self.assertEqual(list((self.root / "tmp").iterdir()), [])
def test_failed_preflight_preserves_firmware_source_and_dkms(self):
stubs = self.prepare_preflight() + "\nMAKE_BUILD_STATUS=17\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertIn("Test build failed", result.stdout)
self.assertIn("MAKE", (self.root / "events").read_text())
self.assertEqual((self.root / "drivers/aic8800/source.c").read_bytes(), b"source")
def test_preflight_copy_failure_preserves_installation(self):
stubs = self.prepare_preflight() + "\ncp() { return 18; }\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertFalse((self.root / "events").exists())
def test_preflight_clean_failure_preserves_installation(self):
stubs = self.prepare_preflight() + "\nMAKE_CLEAN_STATUS=19\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertEqual(len((self.root / "events").read_text().splitlines()), 1)
def test_preflight_success_precedes_installation_and_cleans_temp(self):
result = self.run_installer_function("main", self.prepare_preflight())
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
events = (self.root / "events").read_text().splitlines()
self.assertEqual(len(events), 4)
self.assertTrue(events[0].endswith(" clean"), events)
self.assertTrue(events[1].startswith("MAKE "), events)
self.assertEqual(events[2:], ["FIRMWARE", "DKMS"])
for line in events[:2]:
self.assertIn(str(self.root / "tmp"), line)
self.assertIn("KVER=test-kernel KDIR=/lib/modules/test-kernel/build", line)
self.assertEqual((self.root / "drivers/aic8800/source.c").read_bytes(), b"source")
self.assertEqual(list((self.root / "tmp").iterdir()), [])
self.assertIn("FINAL_INSTRUCTIONS", result.stdout)
def test_log_replaces_symlink_without_overwriting_target(self):
stubs = self.prepare_preflight()
target = self.root / "keep.log"
target.write_bytes(b"keep this file")
(self.root / "install.log").symlink_to(target)
result = self.run_installer_function("main", stubs)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(target.read_bytes(), b"keep this file")
log = self.root / "install.log"
self.assertFalse(log.is_symlink())
self.assertEqual(log.stat().st_mode & 0o777, 0o644)
def test_root_check_precedes_log_changes(self):
stubs = self.prepare_preflight() + "\ncheck_root() { return 20; }\n"
log = self.root / "install.log"
log.write_bytes(b"previous log")
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertEqual(log.read_bytes(), b"previous log")
self.assertFalse((self.root / "events").exists())
def test_log_creation_failure_stops_before_preflight(self):
stubs = self.prepare_preflight() + "\ninstall() { return 21; }\n"
result = self.run_installer_function("main", stubs)
self.assert_installation_preserved(result)
self.assertFalse((self.root / "events").exists())
if __name__ == "__main__":
unittest.main(verbosity=2)