1128 lines
42 KiB
Bash
Executable File
1128 lines
42 KiB
Bash
Executable File
#!/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()
|
|
# re.DOTALL needed because run/build arrays can span multiple lines
|
|
for m in re.finditer(r'(?:run|build)\s*=\s*\[(.*?)\]', content, re.DOTALL):
|
|
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
|
|
|
|
# 32-bit multilib support check — required for Steam/Wine
|
|
GCC_TOML="${PROJECT_ROOT}/src/repos/core/gcc/gcc.toml"
|
|
if [ -f "$GCC_TOML" ]; then
|
|
if grep -q 'enable-multilib' "$GCC_TOML"; then
|
|
record_test "repos.gcc_multilib" "pass"
|
|
else
|
|
record_test "repos.gcc_multilib" "fail" "GCC built with --disable-multilib — Steam/Wine 32-bit support broken"
|
|
fi
|
|
fi
|
|
|
|
# Check that essential lib32 packages exist
|
|
for lib32pkg in lib32-glibc lib32-zlib lib32-openssl lib32-mesa lib32-nvidia lib32-alsa-lib lib32-libx11; do
|
|
if [ -d "${PROJECT_ROOT}/src/repos/gaming/${lib32pkg}" ]; then
|
|
record_test "repos.${lib32pkg}" "pass"
|
|
else
|
|
record_test "repos.${lib32pkg}" "fail" "Missing — Steam/Wine needs 32-bit ${lib32pkg#lib32-}"
|
|
fi
|
|
done
|
|
|
|
# ============================================================================
|
|
# 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
|
|
|
|
# Verify dhcpcd auto-detects network interface if configured one is missing
|
|
DHCPCD_SCRIPT="${PROJECT_ROOT}/configs/rc.d/dhcpcd"
|
|
if [ -f "$DHCPCD_SCRIPT" ]; then
|
|
if grep -q 'sys/class/net' "$DHCPCD_SCRIPT"; then
|
|
record_test "chain.dhcpcd_auto_detect" "pass"
|
|
else
|
|
record_test "chain.dhcpcd_auto_detect" "fail" "dhcpcd doesn't auto-detect interface — network may fail on different hardware"
|
|
fi
|
|
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
|