mirror of
https://github.com/shenmintao/aic8800d80.git
synced 2026-11-10 16:49:17 +00:00
fix: handle installer errors and avoid Arch partial upgrades
Install Arch dependencies using the existing package database and explain how to recover from stale package metadata. Avoid errexit on the first firmware count, handle Secure Boot prompt EOF as cancellation, propagate error traps into functions, and reject empty firmware sources before modifying installed firmware. Validation: eight isolated installer regression tests pass; the original installer fails the affected cases. Bash syntax and ShellCheck error-level checks pass. Fixes #100. Addresses the confirmed findings in #108; other audit suggestions remain separate.
This commit is contained in:
1 parent
eef3d25ca4
commit
18a22341ba
3 files changed
+168
-8
No files matched your search
@@ -0,0 +1,137 @@
|
||||
#!/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]
|
||||
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"
|
||||
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 "$*"; }}
|
||||
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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in new issue
Block a user