#!/bin/bash # ============================================================================ # DarkForge Linux — Integration Test Runner # ============================================================================ # Purpose: Run automated integration tests on an Arch Linux host. # Generates a machine-readable report (JSON + human-readable summary) # that can be fed back to the development process for fixing issues. # # Requirements: # - Arch Linux (x86_64) host # - Packages: qemu-full edk2-ovmf rust cargo base-devel git wget # sudo pacman -S qemu-full edk2-ovmf rust cargo base-devel git wget # - ~30GB free disk space # - Internet access (for package signing tests) # # Usage: # bash tests/run-tests.sh # run all tests # bash tests/run-tests.sh --quick # skip QEMU + long tests # bash tests/run-tests.sh --report # generate report and exit # # Output: # tests/report.json — machine-readable test results # tests/report.txt — human-readable summary # ============================================================================ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" REPORT_JSON="${SCRIPT_DIR}/report.json" REPORT_TXT="${SCRIPT_DIR}/report.txt" LOG_DIR="${SCRIPT_DIR}/logs" QUICK_MODE=false # Parse args for arg in "$@"; do case "$arg" in --quick) QUICK_MODE=true ;; esac done mkdir -p "${LOG_DIR}" # --- Colors ----------------------------------------------------------------- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # --- Test infrastructure ---------------------------------------------------- PASS=0 FAIL=0 SKIP=0 TESTS=() start_time=$(date +%s) record_test() { local name="$1" local status="$2" # pass, fail, skip local detail="${3:-}" local duration="${4:-0}" # Escape double quotes in detail for valid JSON detail=$(echo "$detail" | sed 's/"/\\"/g' | tr '\n' ' ') TESTS+=("{\"name\":\"${name}\",\"status\":\"${status}\",\"detail\":\"${detail}\",\"duration_s\":${duration}}") case "$status" in pass) ((PASS++)); echo -e " ${GREEN}PASS${NC} ${name}" ;; fail) ((FAIL++)); echo -e " ${RED}FAIL${NC} ${name}: ${detail}" ;; skip) ((SKIP++)); echo -e " ${YELLOW}SKIP${NC} ${name}: ${detail}" ;; esac } timed_test() { # Usage: timed_test "test.name" command args... local name="$1"; shift local t0=$(date +%s) local output output=$("$@" 2>&1) local rc=$? local t1=$(date +%s) local dur=$((t1 - t0)) if [ $rc -eq 0 ]; then record_test "$name" "pass" "" "$dur" else record_test "$name" "fail" "$(echo "$output" | tail -5 | tr '\n' ' ')" "$dur" fi return $rc } # ============================================================================ # TEST SUITE 1: Host Environment # ============================================================================ echo -e "\n${BOLD}=== Test Suite 1: Host Environment ===${NC}\n" # Check we're on Linux if [ "$(uname -s)" = "Linux" ]; then record_test "host.is_linux" "pass" else record_test "host.is_linux" "fail" "Not Linux: $(uname -s)" fi # Check Arch Linux if [ -f /etc/arch-release ]; then record_test "host.is_arch" "pass" else record_test "host.is_arch" "skip" "Not Arch Linux (tests may still work)" fi # Check required tools for tool in gcc g++ make git wget curl cargo rustc qemu-system-x86_64 sha256sum tar xz python3; do if command -v "$tool" >/dev/null 2>&1; then record_test "host.tool.${tool}" "pass" else if [ "$tool" = "qemu-system-x86_64" ] && [ "$QUICK_MODE" = true ]; then record_test "host.tool.${tool}" "skip" "Quick mode" else record_test "host.tool.${tool}" "fail" "Not installed" fi fi done # Check nested virtualization support if grep -qE '(vmx|svm)' /proc/cpuinfo 2>/dev/null; then record_test "host.nested_virt" "pass" else record_test "host.nested_virt" "skip" "No VMX/SVM — QEMU boot tests will be slower" fi # Check OVMF — search all known paths including 4m variant (newer Arch/edk2) OVMF_PATH="" for p in \ /usr/share/edk2/x64/OVMF_CODE.4m.fd \ /usr/share/edk2/x64/OVMF_CODE.fd \ /usr/share/edk2-ovmf/x64/OVMF_CODE.4m.fd \ /usr/share/edk2-ovmf/x64/OVMF_CODE.fd \ /usr/share/OVMF/OVMF_CODE.4m.fd \ /usr/share/OVMF/OVMF_CODE.fd \ /usr/share/edk2/x64/OVMF.fd \ /usr/share/edk2-ovmf/x64/OVMF.fd \ /usr/share/ovmf/x64/OVMF.fd \ /usr/share/OVMF/OVMF.fd \ /usr/share/ovmf/OVMF.fd; do if [ -f "$p" ]; then OVMF_PATH="$p" break fi done # Last resort: search with find if [ -z "$OVMF_PATH" ]; then OVMF_PATH=$(find /usr/share -name "OVMF_CODE*.fd" -o -name "OVMF.fd" 2>/dev/null | head -1) fi if [ -n "$OVMF_PATH" ]; then record_test "host.ovmf" "pass" "${OVMF_PATH}" elif [ "$QUICK_MODE" = true ]; then record_test "host.ovmf" "skip" "Quick mode" else record_test "host.ovmf" "fail" "OVMF not found — install edk2-ovmf" fi # Check GCC version (need 14+ for znver5) GCC_VER=$(gcc -dumpversion 2>/dev/null | cut -d. -f1) if [ -n "$GCC_VER" ] && [ "$GCC_VER" -ge 14 ]; then record_test "host.gcc_version" "pass" "GCC ${GCC_VER}" elif [ -n "$GCC_VER" ]; then record_test "host.gcc_version" "fail" "GCC ${GCC_VER} — need 14+ for znver5" else record_test "host.gcc_version" "fail" "GCC not found" fi # Check Rust version RUST_VER=$(rustc --version 2>/dev/null | awk '{print $2}') record_test "host.rust_version" "pass" "Rust ${RUST_VER:-unknown}" # ============================================================================ # TEST SUITE 2: dpack Build & Unit Tests # ============================================================================ echo -e "\n${BOLD}=== Test Suite 2: dpack Build ===${NC}\n" cd "${PROJECT_ROOT}/src/dpack" # Build release t_start=$(date +%s) if cargo build --release 2>"${LOG_DIR}/dpack-build.log"; then t_end=$(date +%s) record_test "dpack.build" "pass" "" "$((t_end - t_start))" else t_end=$(date +%s) err=$(tail -5 "${LOG_DIR}/dpack-build.log" | tr '\n' ' ' | tr '"' "'") record_test "dpack.build" "fail" "${err}" "$((t_end - t_start))" fi # Check for warnings (fix: separate the grep exit code from the count) WARNINGS=$(grep -c "^warning" "${LOG_DIR}/dpack-build.log" 2>/dev/null) || WARNINGS=0 if [ "$WARNINGS" -eq 0 ]; then record_test "dpack.no_warnings" "pass" else record_test "dpack.no_warnings" "fail" "${WARNINGS} warning(s)" fi # Unit tests t_start=$(date +%s) if cargo test 2>"${LOG_DIR}/dpack-test.log"; then t_end=$(date +%s) record_test "dpack.unit_tests" "pass" "" "$((t_end - t_start))" else t_end=$(date +%s) err=$(grep "^test result" "${LOG_DIR}/dpack-test.log" | tr '"' "'") record_test "dpack.unit_tests" "fail" "${err}" "$((t_end - t_start))" fi # CLI smoke tests DPACK="${PROJECT_ROOT}/src/dpack/target/release/dpack" if [ -x "$DPACK" ]; then # Basic CLI tests if $DPACK --version >/dev/null 2>&1; then record_test "dpack.cli.version" "pass" else record_test "dpack.cli.version" "fail" "dpack --version failed" fi if $DPACK --help >/dev/null 2>&1; then record_test "dpack.cli.help" "pass" else record_test "dpack.cli.help" "fail" "dpack --help failed" fi # Extended CLI tests with temp config DPACK_TEST_DIR=$(mktemp -d /tmp/dpack-test-XXXXX) mkdir -p "${DPACK_TEST_DIR}"/{db,repos,sources,build} ln -sf "${PROJECT_ROOT}/src/repos/core" "${DPACK_TEST_DIR}/repos/core" ln -sf "${PROJECT_ROOT}/src/repos/extra" "${DPACK_TEST_DIR}/repos/extra" ln -sf "${PROJECT_ROOT}/src/repos/desktop" "${DPACK_TEST_DIR}/repos/desktop" ln -sf "${PROJECT_ROOT}/src/repos/gaming" "${DPACK_TEST_DIR}/repos/gaming" cat > "${DPACK_TEST_DIR}/dpack.conf" << DCONF [paths] db_dir = "${DPACK_TEST_DIR}/db" repo_dir = "${DPACK_TEST_DIR}/repos" source_dir = "${DPACK_TEST_DIR}/sources" build_dir = "${DPACK_TEST_DIR}/build" [[repos]] name = "core" path = "${DPACK_TEST_DIR}/repos/core" priority = 0 [[repos]] name = "extra" path = "${DPACK_TEST_DIR}/repos/extra" priority = 10 [[repos]] name = "desktop" path = "${DPACK_TEST_DIR}/repos/desktop" priority = 20 [[repos]] name = "gaming" path = "${DPACK_TEST_DIR}/repos/gaming" priority = 30 DCONF DPACK_CMD="$DPACK --config ${DPACK_TEST_DIR}/dpack.conf" if $DPACK_CMD list >/dev/null 2>&1; then record_test "dpack.cli.list" "pass" else record_test "dpack.cli.list" "fail" "Exit code $?" fi if $DPACK_CMD check >/dev/null 2>&1; then record_test "dpack.cli.check" "pass" else record_test "dpack.cli.check" "fail" "Exit code $?" fi if $DPACK_CMD search zlib 2>/dev/null | grep -q "zlib"; then record_test "dpack.cli.search" "pass" else record_test "dpack.cli.search" "fail" "zlib not found in search" fi if $DPACK_CMD info zlib 2>/dev/null | grep -qi "compression\|zlib"; then record_test "dpack.cli.info" "pass" else record_test "dpack.cli.info" "fail" "info zlib returned no useful output" fi rm -rf "${DPACK_TEST_DIR}" else record_test "dpack.cli.version" "skip" "Binary not built" record_test "dpack.cli.help" "skip" "Binary not built" record_test "dpack.cli.list" "skip" "Binary not built" record_test "dpack.cli.check" "skip" "Binary not built" record_test "dpack.cli.search" "skip" "Binary not built" record_test "dpack.cli.info" "skip" "Binary not built" fi cd "${PROJECT_ROOT}" # ============================================================================ # TEST SUITE 3: Package Definitions # ============================================================================ echo -e "\n${BOLD}=== Test Suite 3: Package Definitions ===${NC}\n" # Count packages per repo for repo in core extra desktop gaming; do repo_dir="${PROJECT_ROOT}/src/repos/${repo}" if [ -d "$repo_dir" ]; then count=$(find "$repo_dir" -name "*.toml" | wc -l) record_test "repos.${repo}.count" "pass" "${count} packages" else record_test "repos.${repo}.count" "fail" "Directory missing" fi done # Validate TOML syntax (check required sections) TOML_ERRORS=0 for toml in $(find "${PROJECT_ROOT}/src/repos" -name "*.toml" 2>/dev/null); do pkg_name=$(basename "$(dirname "$toml")") for section in '\[package\]' '\[source\]' '\[build\]'; do if ! grep -q "$section" "$toml"; then record_test "repos.validate.${pkg_name}" "fail" "Missing ${section}" ((TOML_ERRORS++)) break fi done done if [ "$TOML_ERRORS" -eq 0 ]; then total=$(find "${PROJECT_ROOT}/src/repos" -name "*.toml" | wc -l) record_test "repos.toml_validation" "pass" "All ${total} valid" fi # Dependency resolution check (all deps must resolve within the repo tree) if command -v python3 >/dev/null 2>&1; then DEP_OUTPUT=$(PROJECT_ROOT="${PROJECT_ROOT}" python3 << 'PYEOF' 2>/dev/null import os, re, sys base = os.environ.get("PROJECT_ROOT", ".") + "/src/repos" known = set() for repo in ['core','extra','desktop','gaming']: d = os.path.join(base, repo) if not os.path.isdir(d): continue for p in os.listdir(d): if os.path.isdir(os.path.join(d,p)) and os.path.exists(os.path.join(d,p,f"{p}.toml")): known.add(p) missing = set() for repo in ['core','extra','desktop','gaming']: d = os.path.join(base, repo) if not os.path.isdir(d): continue for p in os.listdir(d): tf = os.path.join(d,p,f"{p}.toml") if not os.path.exists(tf): continue with open(tf) as f: content = f.read() for m in re.finditer(r'(?:run|build)\s*=\s*\[(.*?)\]', content): for dm in re.finditer(r'"([\w][\w.-]*)"', m.group(1)): if dm.group(1) not in known: missing.add(dm.group(1)) if missing: print(f"MISSING:{','.join(sorted(missing))}") sys.exit(1) else: print(f"OK:{len(known)}") PYEOF ) if [ $? -eq 0 ]; then record_test "repos.deps_resolve" "pass" "All dependencies resolve" else record_test "repos.deps_resolve" "fail" "${DEP_OUTPUT}" fi else record_test "repos.deps_resolve" "skip" "python3 not available" fi # ============================================================================ # TEST SUITE 4: Script Validation # ============================================================================ echo -e "\n${BOLD}=== Test Suite 4: Script Validation ===${NC}\n" # Toolchain scripts — all executable MISSING_SCRIPTS=0 for script in ${PROJECT_ROOT}/toolchain/scripts/*.sh; do if [ ! -x "$script" ]; then record_test "toolchain.exec.$(basename "$script" .sh)" "fail" "Not executable" ((MISSING_SCRIPTS++)) fi done if [ "$MISSING_SCRIPTS" -eq 0 ]; then count=$(ls "${PROJECT_ROOT}/toolchain/scripts/"*.sh | wc -l) record_test "toolchain.all_executable" "pass" "${count} scripts" fi # Toolchain scripts — syntax check SYNTAX_ERRORS=0 for script in ${PROJECT_ROOT}/toolchain/scripts/*.sh; do if ! bash -n "$script" 2>/dev/null; then record_test "toolchain.syntax.$(basename "$script" .sh)" "fail" "Syntax error" ((SYNTAX_ERRORS++)) fi done if [ "$SYNTAX_ERRORS" -eq 0 ]; then record_test "toolchain.bash_syntax" "pass" fi # Init/rc.d scripts — individual syntax checks for script in "${PROJECT_ROOT}"/configs/rc.d/*; do [ -f "$script" ] || continue name=$(basename "$script") if bash -n "$script" 2>/dev/null; then record_test "scripts.init.${name}" "pass" else record_test "scripts.init.${name}" "fail" "Syntax error" fi done # Installer scripts — syntax checks for script in "${PROJECT_ROOT}"/src/install/*.sh "${PROJECT_ROOT}"/src/install/modules/*.sh; do [ -f "$script" ] || continue name=$(basename "$script" .sh) if bash -n "$script" 2>/dev/null; then record_test "scripts.install.${name}" "pass" else record_test "scripts.install.${name}" "fail" "Syntax error" fi done # ISO builder scripts — syntax checks for script in "${PROJECT_ROOT}"/src/iso/*.sh; do [ -f "$script" ] || continue name=$(basename "$script" .sh) if bash -n "$script" 2>/dev/null; then record_test "scripts.iso.${name}" "pass" else record_test "scripts.iso.${name}" "fail" "Syntax error" fi done # ============================================================================ # TEST SUITE 5: Kernel Config # ============================================================================ echo -e "\n${BOLD}=== Test Suite 5: Kernel Config ===${NC}\n" KCONFIG="${PROJECT_ROOT}/kernel/config" if [ -f "$KCONFIG" ]; then record_test "kernel.config_exists" "pass" # Check critical enabled options for opt in CONFIG_EFI_STUB CONFIG_BLK_DEV_NVME CONFIG_PREEMPT CONFIG_R8169 CONFIG_EXT4_FS CONFIG_MODULES CONFIG_SMP CONFIG_AMD_IOMMU; do if grep -q "^${opt}=y" "$KCONFIG"; then record_test "kernel.${opt}" "pass" else record_test "kernel.${opt}" "fail" "Not set to =y" fi done # Check disabled options for opt in CONFIG_BLUETOOTH CONFIG_WIRELESS CONFIG_DRM_NOUVEAU; do if grep -q "^${opt}=n" "$KCONFIG"; then record_test "kernel.disable.${opt}" "pass" else record_test "kernel.disable.${opt}" "fail" "Should be =n" fi done else record_test "kernel.config_exists" "fail" "kernel/config missing" fi # ============================================================================ # TEST SUITE 6: Init System Files # ============================================================================ echo -e "\n${BOLD}=== Test Suite 6: Init System ===${NC}\n" for f in rc.conf inittab fstab.template zprofile; do if [ -f "${PROJECT_ROOT}/configs/${f}" ]; then record_test "init.${f}" "pass" else record_test "init.${f}" "fail" "Missing" fi done for daemon in eudev seatd syslog dbus dhcpcd pipewire; do script="${PROJECT_ROOT}/configs/rc.d/${daemon}" if [ -x "$script" ]; then if bash -n "$script" 2>/dev/null; then record_test "init.daemon.${daemon}" "pass" else record_test "init.daemon.${daemon}" "fail" "Syntax error" fi else record_test "init.daemon.${daemon}" "fail" "Missing or not executable" fi done # ============================================================================ # TEST SUITE 7: Boot Chain Verification # ============================================================================ # Verify that the complete boot-to-desktop chain is correctly wired: # EFISTUB → init → rc.sysinit → rc.multi → agetty --autologin → zsh → dwl # These are static checks — no running system required. echo -e "\n${BOLD}=== Test Suite 7: Boot Chain Verification ===${NC}\n" # 7.1 — Kernel has EFISTUB if [ -f "$KCONFIG" ] && grep -q "^CONFIG_EFI_STUB=y" "$KCONFIG"; then record_test "chain.efistub" "pass" else record_test "chain.efistub" "fail" "CONFIG_EFI_STUB not set — kernel won't boot as EFI binary" fi # 7.2 — inittab has auto-login INITTAB="${PROJECT_ROOT}/configs/inittab" if [ -f "$INITTAB" ] && grep -q -- '--autologin' "$INITTAB"; then AUTOLOGIN_USER=$(grep -- '--autologin' "$INITTAB" | sed 's/.*--autologin \([^ ]*\).*/\1/' | head -1) record_test "chain.autologin" "pass" "User: ${AUTOLOGIN_USER}" else record_test "chain.autologin" "fail" "No --autologin in inittab — user won't be logged in automatically" fi # 7.3 — inittab runs rc.sysinit and rc.multi if [ -f "$INITTAB" ]; then grep -q "rc.sysinit" "$INITTAB" && record_test "chain.inittab_sysinit" "pass" \ || record_test "chain.inittab_sysinit" "fail" "inittab missing rc.sysinit" grep -q "rc.multi" "$INITTAB" && record_test "chain.inittab_multi" "pass" \ || record_test "chain.inittab_multi" "fail" "inittab missing rc.multi" fi # 7.4 — rc.sysinit and rc.multi exist and are executable for rcscript in rc.sysinit rc.multi rc.shutdown; do path="${PROJECT_ROOT}/configs/rc.d/${rcscript}" if [ -x "$path" ]; then record_test "chain.${rcscript}" "pass" else record_test "chain.${rcscript}" "fail" "Missing or not executable" fi done # 7.5 — rc.conf has DAEMONS array with required services # The DAEMONS array is multi-line in rc.conf, so we grep for the service name # anywhere in the file within the DAEMONS block (or just present as a daemon entry) RC_CONF="${PROJECT_ROOT}/configs/rc.conf" if [ -f "$RC_CONF" ]; then for svc in eudev seatd dbus dhcpcd pipewire; do if grep -q "^[[:space:]]*${svc}" "$RC_CONF"; then record_test "chain.daemon_listed.${svc}" "pass" else record_test "chain.daemon_listed.${svc}" "fail" "${svc} not in DAEMONS array — won't start at boot" fi done fi # 7.6 — zprofile contains dwl auto-start on tty1 ZPROFILE="${PROJECT_ROOT}/configs/zprofile" if [ -f "$ZPROFILE" ]; then if grep -q 'exec dwl' "$ZPROFILE"; then record_test "chain.zprofile_dwl" "pass" else record_test "chain.zprofile_dwl" "fail" "zprofile missing 'exec dwl' — desktop won't launch" fi if grep -q '/dev/tty1' "$ZPROFILE"; then record_test "chain.zprofile_tty1_guard" "pass" else record_test "chain.zprofile_tty1_guard" "fail" "zprofile missing tty1 check — dwl might launch on wrong tty" fi if grep -q 'WAYLAND_DISPLAY' "$ZPROFILE"; then record_test "chain.zprofile_wayland_guard" "pass" else record_test "chain.zprofile_wayland_guard" "fail" "zprofile missing WAYLAND_DISPLAY check — might double-launch" fi if grep -q 'pipewire' "$ZPROFILE"; then record_test "chain.zprofile_pipewire" "pass" else record_test "chain.zprofile_pipewire" "fail" "zprofile missing pipewire startup — no audio" fi if grep -q 'GBM_BACKEND' "$ZPROFILE"; then record_test "chain.zprofile_nvidia_env" "pass" else record_test "chain.zprofile_nvidia_env" "fail" "zprofile missing NVIDIA env vars — Wayland may fail on RTX 5090" fi if grep -q 'XDG_RUNTIME_DIR' "$ZPROFILE"; then record_test "chain.zprofile_xdg_runtime" "pass" else record_test "chain.zprofile_xdg_runtime" "fail" "zprofile missing XDG_RUNTIME_DIR setup" fi # D-Bus user session — required by PipeWire and polkit if grep -q 'dbus-launch' "$ZPROFILE"; then record_test "chain.zprofile_dbus_session" "pass" else record_test "chain.zprofile_dbus_session" "fail" "zprofile missing dbus-launch — PipeWire and polkit won't work" fi # polkit authentication agent — needed for GUI password prompts (e.g., Steam) if grep -q 'policykit-agent\|polkit.*agent' "$ZPROFILE"; then record_test "chain.zprofile_polkit_agent" "pass" else record_test "chain.zprofile_polkit_agent" "fail" "zprofile missing polkit agent — no GUI password prompts" fi # LIBSEAT_BACKEND — seatd environment for wlroots/dwl if grep -q 'LIBSEAT_BACKEND' "$ZPROFILE"; then record_test "chain.zprofile_seatd_env" "pass" else record_test "chain.zprofile_seatd_env" "fail" "zprofile missing LIBSEAT_BACKEND — dwl may not get GPU access" fi # pipewire-pulse — PulseAudio compat server needed by Firefox/Steam if grep -q 'pipewire-pulse' "$ZPROFILE"; then record_test "chain.zprofile_pipewire_pulse" "pass" else record_test "chain.zprofile_pipewire_pulse" "fail" "zprofile missing pipewire-pulse — Firefox/Steam audio won't work" fi # wireplumber — session manager for PipeWire if grep -q 'wireplumber' "$ZPROFILE"; then record_test "chain.zprofile_wireplumber" "pass" else record_test "chain.zprofile_wireplumber" "fail" "zprofile missing wireplumber — audio routing won't work" fi else record_test "chain.zprofile_dwl" "fail" "zprofile file missing entirely" fi # 7.11 — dwl config.h exists with keybindings DWL_CONFIG="${PROJECT_ROOT}/configs/dwl/config.h" if [ -f "$DWL_CONFIG" ]; then record_test "chain.dwl_config_exists" "pass" # Check for critical keybindings if grep -q 'XKB_KEY_Return' "$DWL_CONFIG" && grep -q 'termcmd' "$DWL_CONFIG"; then record_test "chain.dwl_config_terminal" "pass" else record_test "chain.dwl_config_terminal" "fail" "dwl config.h missing terminal keybinding" fi if grep -q 'browsercmd\|firefox' "$DWL_CONFIG"; then record_test "chain.dwl_config_browser" "pass" else record_test "chain.dwl_config_browser" "fail" "dwl config.h missing browser keybinding" fi if grep -q 'steamcmd\|steam' "$DWL_CONFIG"; then record_test "chain.dwl_config_steam" "pass" else record_test "chain.dwl_config_steam" "fail" "dwl config.h missing Steam keybinding" fi if grep -q 'XF86Audio' "$DWL_CONFIG"; then record_test "chain.dwl_config_audio_keys" "pass" else record_test "chain.dwl_config_audio_keys" "fail" "dwl config.h missing audio key controls" fi else record_test "chain.dwl_config_exists" "fail" "configs/dwl/config.h missing — dwl will use defaults (no custom keybindings)" fi # 7.12 — Installer deploys rc.d scripts and dwl config to target INSTALLER_PKG="${PROJECT_ROOT}/src/install/modules/packages.sh" if [ -f "$INSTALLER_PKG" ]; then if grep -q 'rc\.d' "$INSTALLER_PKG" && grep -q 'cp.*rc\.d' "$INSTALLER_PKG"; then record_test "chain.installer_deploys_rcd" "pass" else record_test "chain.installer_deploys_rcd" "fail" "Installer doesn't copy rc.d scripts to target" fi if grep -q 'dwl' "$INSTALLER_PKG"; then record_test "chain.installer_deploys_dwl_config" "pass" else record_test "chain.installer_deploys_dwl_config" "fail" "Installer doesn't copy dwl config to target" fi fi # 7.7 — rc.d/pipewire does NOT hardcode a username (should auto-detect) PW_SCRIPT="${PROJECT_ROOT}/configs/rc.d/pipewire" if [ -f "$PW_SCRIPT" ]; then if grep -q 'get_autologin_user\|--autologin' "$PW_SCRIPT"; then record_test "chain.pipewire_dynamic_user" "pass" else # Check if it still hardcodes 'danny' if grep -q 'chown danny' "$PW_SCRIPT"; then record_test "chain.pipewire_dynamic_user" "fail" "rc.d/pipewire hardcodes username 'danny'" else record_test "chain.pipewire_dynamic_user" "pass" fi fi fi # 7.8 — Installer copies zprofile to target user home INSTALLER_USER="${PROJECT_ROOT}/src/install/modules/user.sh" if [ -f "$INSTALLER_USER" ]; then if grep -q 'zprofile' "$INSTALLER_USER"; then record_test "chain.installer_copies_zprofile" "pass" else record_test "chain.installer_copies_zprofile" "fail" "Installer doesn't copy zprofile — target user won't auto-start dwl" fi if grep -q -- '--autologin' "$INSTALLER_USER"; then record_test "chain.installer_updates_inittab" "pass" else record_test "chain.installer_updates_inittab" "fail" "Installer doesn't update inittab autologin user" fi fi # 7.9 — Installer boot config: mkdir before cp, efibootmgr call INSTALLER_DISK="${PROJECT_ROOT}/src/install/modules/disk.sh" if [ -f "$INSTALLER_DISK" ]; then # Check that mkdir comes before cp in configure_boot if python3 -c " import re, sys with open('${INSTALLER_DISK}') as f: content = f.read() # Find configure_boot function m = re.search(r'configure_boot\(\)\s*\{(.*?)\n\}', content, re.DOTALL) if not m: sys.exit(1) body = m.group(1) mkdir_pos = body.find('mkdir -p') cp_pos = body.find('cp.*vmlinuz.*vmlinuz.efi') if 'cp' in body else body.find('cp ') # Just check mkdir exists before the cp of vmlinuz lines = body.split('\n') mkdir_line = cp_line = -1 for i, line in enumerate(lines): if 'mkdir -p' in line and 'EFI/Linux' in line: mkdir_line = i if 'vmlinuz.efi' in line and 'cp ' in line: cp_line = i if mkdir_line >= 0 and cp_line >= 0 and mkdir_line < cp_line: sys.exit(0) else: sys.exit(1) " 2>/dev/null; then record_test "chain.boot_mkdir_before_cp" "pass" else record_test "chain.boot_mkdir_before_cp" "fail" "configure_boot: mkdir must come before cp to EFI/Linux/" fi if grep -q 'efibootmgr' "$INSTALLER_DISK"; then record_test "chain.efibootmgr" "pass" else record_test "chain.efibootmgr" "fail" "Installer doesn't create UEFI boot entry" fi fi # 7.10 — NVIDIA kernel modules in rc.conf MODULES array # MODULES array is multi-line, so grep for nvidia on its own line if [ -f "$RC_CONF" ]; then if grep -q '^[[:space:]]*nvidia' "$RC_CONF"; then record_test "chain.nvidia_modules" "pass" else record_test "chain.nvidia_modules" "fail" "NVIDIA modules not in MODULES array — GPU won't work" fi if grep -q 'nvidia-drm.*modeset=1' "$RC_CONF"; then record_test "chain.nvidia_modeset" "pass" else record_test "chain.nvidia_modeset" "fail" "nvidia-drm modeset=1 not set — Wayland DRM/KMS won't work" fi fi # ============================================================================ # TEST SUITE 8: Package Signing (network test) # ============================================================================ if [ "$QUICK_MODE" = false ] && [ -x "$DPACK" ]; then echo -e "\n${BOLD}=== Test Suite 8: Package Signing ===${NC}\n" ZLIB_TOML="${PROJECT_ROOT}/src/repos/core/zlib/zlib.toml" if [ -f "$ZLIB_TOML" ]; then cp "$ZLIB_TOML" "${ZLIB_TOML}.bak" timed_test "sign.zlib" $DPACK sign zlib || true # Check if it got a real hash (not the placeholder) if grep -q 'sha256 = "aaa' "$ZLIB_TOML" 2>/dev/null; then record_test "sign.zlib_result" "fail" "Checksum still placeholder after signing" else record_test "sign.zlib_result" "pass" fi mv "${ZLIB_TOML}.bak" "$ZLIB_TOML" fi else if [ "$QUICK_MODE" = true ]; then echo -e "\n${BOLD}=== Test Suite 8: Package Signing (SKIPPED) ===${NC}\n" record_test "sign.zlib" "skip" "Quick mode" fi fi # ============================================================================ # TEST SUITE 9: ISO Build # ============================================================================ echo -e "\n${BOLD}=== Test Suite 9: ISO Build ===${NC}\n" ISO="${PROJECT_ROOT}/darkforge-live.iso" # Check ISO build prerequisites ISO_PREREQS_OK=true for tool in mksquashfs xorriso mkfs.fat mcopy; do if command -v "$tool" >/dev/null 2>&1; then record_test "iso.prereq.${tool}" "pass" else record_test "iso.prereq.${tool}" "fail" "Not installed — needed for ISO build" ISO_PREREQS_OK=false fi done if [ "$QUICK_MODE" = false ] && [ "$ISO_PREREQS_OK" = true ]; then # Build the ISO echo -e " ${CYAN}Building ISO (this may take a few minutes)...${NC}" t_start=$(date +%s) if sudo bash "${PROJECT_ROOT}/src/iso/build-iso-arch.sh" > "${LOG_DIR}/iso-build.log" 2>&1; then t_end=$(date +%s) record_test "iso.build" "pass" "" "$((t_end - t_start))" else t_end=$(date +%s) err=$(tail -10 "${LOG_DIR}/iso-build.log" | tr '\n' ' ' | tr '"' "'") record_test "iso.build" "fail" "${err}" "$((t_end - t_start))" fi # Check ISO was produced if [ -f "$ISO" ]; then ISO_SIZE=$(du -sh "$ISO" | cut -f1) record_test "iso.exists" "pass" "Size: ${ISO_SIZE}" # Verify ISO has EFI boot structure if xorriso -indev "$ISO" -find / -name "BOOTX64.EFI" 2>/dev/null | grep -q "BOOTX64"; then record_test "iso.has_efi_binary" "pass" else record_test "iso.has_efi_binary" "fail" "ISO missing EFI/BOOT/BOOTX64.EFI — won't UEFI boot" fi # Verify ISO has squashfs rootfs if xorriso -indev "$ISO" -find / -name "rootfs.img" 2>/dev/null | grep -q "rootfs"; then record_test "iso.has_rootfs" "pass" else record_test "iso.has_rootfs" "fail" "ISO missing LiveOS/rootfs.img" fi # Mount the squashfs and verify critical files are inside SQFS_MNT=$(mktemp -d /tmp/darkforge-sqfs-XXXXX) SQFS_EXTRACTED=false # Extract squashfs from ISO to check contents ISO_MNT=$(mktemp -d /tmp/darkforge-iso-XXXXX) if sudo mount -o loop,ro "$ISO" "$ISO_MNT" 2>/dev/null; then if [ -f "$ISO_MNT/LiveOS/rootfs.img" ]; then if sudo mount -o loop,ro "$ISO_MNT/LiveOS/rootfs.img" "$SQFS_MNT" 2>/dev/null; then SQFS_EXTRACTED=true fi fi fi if [ "$SQFS_EXTRACTED" = true ]; then # Check that the live rootfs has all the critical files for the install chain for check_file in \ "etc/rc.conf:rc.conf in live rootfs" \ "etc/rc.d/rc.sysinit:rc.sysinit in live rootfs" \ "etc/rc.d/rc.multi:rc.multi in live rootfs" \ "etc/rc.d/eudev:eudev daemon in live rootfs" \ "etc/rc.d/dbus:dbus daemon in live rootfs" \ "etc/rc.d/dhcpcd:dhcpcd daemon in live rootfs" \ "etc/rc.d/pipewire:pipewire daemon in live rootfs" \ "install/install.sh:installer script in live rootfs" \ "install/modules/disk.sh:disk module in live rootfs" \ "install/modules/user.sh:user module in live rootfs" \ "install/modules/locale.sh:locale module in live rootfs" \ "install/modules/packages.sh:packages module in live rootfs" \ "install/configs/zprofile:zprofile for target user in live rootfs" \ "etc/rc.d/seatd:seatd daemon in live rootfs" \ "install/configs/dwl/config.h:dwl config.h for target in live rootfs"; do fpath="${check_file%%:*}" fdesc="${check_file##*:}" if sudo test -f "$SQFS_MNT/$fpath"; then record_test "iso.rootfs.${fpath##*/}" "pass" else record_test "iso.rootfs.${fpath##*/}" "fail" "Missing: ${fdesc}" fi done # Check that the zprofile in the ISO has dwl auto-start if sudo test -f "$SQFS_MNT/install/configs/zprofile"; then if sudo grep -q 'exec dwl' "$SQFS_MNT/install/configs/zprofile"; then record_test "iso.rootfs.zprofile_has_dwl" "pass" else record_test "iso.rootfs.zprofile_has_dwl" "fail" "zprofile in ISO missing 'exec dwl'" fi fi # Check that dpack binary is in the ISO if sudo test -f "$SQFS_MNT/usr/bin/dpack"; then record_test "iso.rootfs.dpack_binary" "pass" else record_test "iso.rootfs.dpack_binary" "fail" "dpack binary missing from ISO — installer can't use dpack" fi # Check that package repos are in the ISO (use sudo — squashfs may preserve restrictive perms) # Debug: list what's actually in the repos dir echo " DEBUG repos dir contents:" >&2 sudo ls -laR "$SQFS_MNT/var/lib/dpack/repos/" 2>&1 | head -20 >&2 || echo " DEBUG: repos dir does not exist or empty" >&2 if sudo test -d "$SQFS_MNT/var/lib/dpack/repos/core"; then record_test "iso.rootfs.repos" "pass" else record_test "iso.rootfs.repos" "fail" "Package repos missing from ISO (see debug output above)" fi sudo umount "$SQFS_MNT" 2>/dev/null else record_test "iso.rootfs_mount" "skip" "Could not mount squashfs for inspection" fi sudo umount "$ISO_MNT" 2>/dev/null rmdir "$SQFS_MNT" "$ISO_MNT" 2>/dev/null else record_test "iso.exists" "fail" "ISO not produced by build script" fi else if [ "$QUICK_MODE" = true ]; then record_test "iso.build" "skip" "Quick mode" else record_test "iso.build" "skip" "Missing ISO build prerequisites" fi fi # ============================================================================ # TEST SUITE 10: QEMU Boot Test (skipped in quick mode) # ============================================================================ if [ "$QUICK_MODE" = false ] && [ -n "${OVMF_PATH:-}" ] && [ -f "${ISO}" ]; then echo -e "\n${BOLD}=== Test Suite 10: QEMU Boot Test ===${NC}\n" echo -e " ${CYAN}Testing ISO boot in QEMU (60s timeout)...${NC}" QEMU_DISK=$(mktemp /tmp/darkforge-qemu-XXXXX.qcow2) qemu-img create -f qcow2 "$QEMU_DISK" 20G >/dev/null 2>&1 KVM_FLAG="" [ -c /dev/kvm ] && KVM_FLAG="-enable-kvm" # Build OVMF flags — split CODE/VARS files need -drive, single .fd uses -bios OVMF_FLAGS="" if echo "$OVMF_PATH" | grep -q "OVMF_CODE"; then OVMF_VARS_TEMPLATE="$(dirname "$OVMF_PATH")/OVMF_VARS.fd" # Try 4m variant if regular not found if [ ! -f "$OVMF_VARS_TEMPLATE" ]; then OVMF_VARS_TEMPLATE="$(dirname "$OVMF_PATH")/OVMF_VARS.4m.fd" fi OVMF_VARS_COPY="/tmp/darkforge-ovmf-vars.fd" cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS_COPY" 2>/dev/null || \ dd if=/dev/zero of="$OVMF_VARS_COPY" bs=256K count=1 2>/dev/null OVMF_FLAGS="-drive if=pflash,format=raw,readonly=on,file=${OVMF_PATH} -drive if=pflash,format=raw,file=${OVMF_VARS_COPY}" else OVMF_FLAGS="-bios ${OVMF_PATH}" fi # Check if we have a real kernel for direct boot (more reliable than UEFI ISO in QEMU) HAS_KERNEL=false [ -f "${PROJECT_ROOT}/kernel/vmlinuz" ] && HAS_KERNEL=true # Build QEMU command as an array for correct quoting QEMU_CMD=(qemu-system-x86_64) [ -n "$KVM_FLAG" ] && QEMU_CMD+=($KVM_FLAG) QEMU_CMD+=(-m 2G -smp 2) if [ "$HAS_KERNEL" = true ]; then echo " Using direct kernel boot (kernel + initramfs)..." QEMU_CMD+=(-kernel "${PROJECT_ROOT}/kernel/vmlinuz") if [ -f "${PROJECT_ROOT}/src/iso/initramfs.cpio.gz" ]; then QEMU_CMD+=(-initrd "${PROJECT_ROOT}/src/iso/initramfs.cpio.gz") fi QEMU_CMD+=(-append "console=ttyS0,115200n8 loglevel=7") else echo " Using UEFI ISO boot (no compiled kernel found)..." QEMU_CMD+=(${OVMF_FLAGS}) fi QEMU_CMD+=(-cdrom "$ISO") QEMU_CMD+=(-drive "file=$QEMU_DISK,format=qcow2,if=virtio") QEMU_CMD+=(-nographic -no-reboot) # Debug: show the QEMU command echo " QEMU: ${QEMU_CMD[*]}" >&2 # Run QEMU with timeout, capture serial output directly to file # -nographic routes serial to stdio automatically timeout 30 "${QEMU_CMD[@]}" > "${LOG_DIR}/qemu-output.log" 2>"${LOG_DIR}/qemu-stderr.log" & QEMU_PID=$! # Wait for QEMU to finish (timeout handles the time limit) wait $QEMU_PID 2>/dev/null # Debug: dump log sizes and first/last lines echo " Output log: $(wc -l < "${LOG_DIR}/qemu-output.log" 2>/dev/null || echo 0) lines, $(wc -c < "${LOG_DIR}/qemu-output.log" 2>/dev/null || echo 0) bytes" >&2 echo " Stderr log: $(wc -l < "${LOG_DIR}/qemu-stderr.log" 2>/dev/null || echo 0) lines" >&2 if [ -s "${LOG_DIR}/qemu-output.log" ]; then echo " First 3 lines of output:" >&2 head -3 "${LOG_DIR}/qemu-output.log" >&2 echo " Last 3 lines of output:" >&2 tail -3 "${LOG_DIR}/qemu-output.log" >&2 else echo " WARNING: qemu-output.log is empty!" >&2 echo " Stderr:" >&2 head -10 "${LOG_DIR}/qemu-stderr.log" >&2 fi # Check if we got kernel boot messages if grep -qi "linux version\|darkforge\|kernel" "${LOG_DIR}/qemu-output.log" 2>/dev/null; then record_test "qemu.kernel_boots" "pass" else record_test "qemu.kernel_boots" "fail" "No kernel boot messages in serial output" fi # Check if we got to userspace if grep -qi "login:\|installer\|welcome\|darkforge" "${LOG_DIR}/qemu-output.log" 2>/dev/null; then record_test "qemu.reaches_userspace" "pass" else record_test "qemu.reaches_userspace" "fail" "Did not reach login prompt" fi rm -f "$QEMU_DISK" else echo -e "\n${BOLD}=== Test Suite 10: QEMU Boot Test (SKIPPED) ===${NC}\n" if [ "$QUICK_MODE" = true ]; then record_test "qemu.kernel_boots" "skip" "Quick mode" elif [ -z "${OVMF_PATH:-}" ]; then record_test "qemu.kernel_boots" "skip" "No OVMF firmware" else record_test "qemu.kernel_boots" "skip" "No ISO built" fi fi # ============================================================================ # Generate Report # ============================================================================ end_time=$(date +%s) total_duration=$((end_time - start_time)) TOTAL=$((PASS + FAIL + SKIP)) # JSON report cat > "$REPORT_JSON" << JSONEOF { "project": "DarkForge Linux", "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "host": "$(uname -n) $(uname -r) $(uname -m)", "duration_s": ${total_duration}, "summary": { "total": ${TOTAL}, "pass": ${PASS}, "fail": ${FAIL}, "skip": ${SKIP} }, "tests": [ $(IFS=,; echo "${TESTS[*]}") ] } JSONEOF # Human-readable report cat > "$REPORT_TXT" << TXTEOF ================================================================================ DarkForge Linux — Integration Test Report ================================================================================ Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") Host: $(uname -n) $(uname -r) $(uname -m) Duration: ${total_duration}s RESULTS: ${PASS} pass, ${FAIL} fail, ${SKIP} skip (${TOTAL} total) ================================================================================ TXTEOF # Append failures to the text report if [ "$FAIL" -gt 0 ]; then echo "FAILURES:" >> "$REPORT_TXT" echo "" >> "$REPORT_TXT" for t in "${TESTS[@]}"; do if echo "$t" | grep -q '"status":"fail"'; then name=$(echo "$t" | sed 's/.*"name":"\([^"]*\)".*/\1/') detail=$(echo "$t" | sed 's/.*"detail":"\([^"]*\)".*/\1/') echo " FAIL: ${name}" >> "$REPORT_TXT" echo " ${detail}" >> "$REPORT_TXT" echo "" >> "$REPORT_TXT" fi done fi echo "" >> "$REPORT_TXT" echo "Full results in: ${REPORT_JSON}" >> "$REPORT_TXT" # Print summary echo "" echo -e "${BOLD}═══════════════════════════════════════════════${NC}" echo -e " ${BOLD}Results:${NC} ${GREEN}${PASS} pass${NC}, ${RED}${FAIL} fail${NC}, ${YELLOW}${SKIP} skip${NC} (${TOTAL} total)" echo -e " ${BOLD}Duration:${NC} ${total_duration}s" echo -e " ${BOLD}Report:${NC} ${REPORT_TXT}" echo -e " ${BOLD}JSON:${NC} ${REPORT_JSON}" echo -e "${BOLD}═══════════════════════════════════════════════${NC}" # Exit with failure code if any tests failed [ "$FAIL" -eq 0 ] && exit 0 || exit 1