Add dpack sign command, test runner, ISO builders, fix build errors
dpack fixes: - Fixed missing SourceInfo fields in CRUX/Gentoo converters (git, branch, tag, commit, update_check fields added to struct initializers) - Added 'sign' command: downloads source tarballs and computes real SHA256 checksums, updating .toml definitions in-place. Replaces placeholder checksums. Usage: dpack sign zlib or dpack sign all Testing: - tests/run-tests.sh: comprehensive integration test runner for Arch Linux host. 7 test suites covering host env, dpack build/tests, package defs, toolchain scripts, kernel config, init system, and QEMU boot. Generates JSON + text reports for automated debugging. Usage: bash tests/run-tests.sh [--quick] ISO builders: - src/iso/build-iso-arch.sh: builds live ISO from Arch Linux host Creates rootfs from pre-built base system or busybox fallback, includes installer + dpack + package repos, UEFI-only boot - src/iso/build-iso-darkforge.sh: builds live ISO from running DarkForge Snapshots the live system via rsync, creates redistributable ISO Package repository (submodule updated): - 14 new self-hosting packages: qemu, edk2-ovmf, squashfs-tools, xorriso, mtools, efibootmgr, efivar, rsync, lz4, nasm, neovim, htop, tmux, libevent - Total: 138 packages across 4 repos Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
434
tests/run-tests.sh
Executable file
434
tests/run-tests.sh
Executable file
@@ -0,0 +1,434 @@
|
||||
#!/bin/bash
|
||||
# ============================================================================
|
||||
# DarkForge Linux — Integration Test Runner
|
||||
# ============================================================================
|
||||
# Purpose: Run automated integration tests on an Arch Linux host with QEMU.
|
||||
# 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 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 downloading sources during sign test)
|
||||
#
|
||||
# Usage:
|
||||
# bash tests/run-tests.sh # run all tests
|
||||
# bash tests/run-tests.sh --quick # skip QEMU tests (dpack only)
|
||||
# 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"
|
||||
QUICK_MODE=false
|
||||
|
||||
# Parse args
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--quick) QUICK_MODE=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- 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}"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 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; 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 OVMF
|
||||
OVMF_PATH=""
|
||||
for p in /usr/share/ovmf/x64/OVMF.fd /usr/share/edk2/x64/OVMF.fd /usr/share/OVMF/OVMF.fd /usr/share/edk2-ovmf/x64/OVMF.fd; do
|
||||
if [ -f "$p" ]; then
|
||||
OVMF_PATH="$p"
|
||||
break
|
||||
fi
|
||||
done
|
||||
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
|
||||
t_start=$(date +%s)
|
||||
if cargo build --release 2>"${SCRIPT_DIR}/dpack-build.log"; then
|
||||
t_end=$(date +%s)
|
||||
record_test "dpack.build" "pass" "" "$((t_end - t_start))"
|
||||
else
|
||||
t_end=$(date +%s)
|
||||
# Extract the error
|
||||
err=$(tail -5 "${SCRIPT_DIR}/dpack-build.log" | tr '\n' ' ' | tr '"' "'")
|
||||
record_test "dpack.build" "fail" "${err}" "$((t_end - t_start))"
|
||||
fi
|
||||
|
||||
# Check for warnings
|
||||
WARNINGS=$(grep -c "^warning" "${SCRIPT_DIR}/dpack-build.log" 2>/dev/null || echo "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>"${SCRIPT_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" "${SCRIPT_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
|
||||
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
|
||||
else
|
||||
record_test "dpack.cli.version" "skip" "Binary not built"
|
||||
record_test "dpack.cli.help" "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 (basic parse check)
|
||||
TOML_ERRORS=0
|
||||
for toml in $(find "${PROJECT_ROOT}/src/repos" -name "*.toml" 2>/dev/null); do
|
||||
# Check required sections exist
|
||||
if ! grep -q '\[package\]' "$toml" || ! grep -q '\[source\]' "$toml" || ! grep -q '\[build\]' "$toml"; then
|
||||
pkg_name=$(basename "$(dirname "$toml")")
|
||||
record_test "repos.toml.${pkg_name}" "fail" "Missing required section"
|
||||
((TOML_ERRORS++))
|
||||
fi
|
||||
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
|
||||
|
||||
# ============================================================================
|
||||
# TEST SUITE 4: Toolchain Scripts
|
||||
# ============================================================================
|
||||
echo -e "\n${BOLD}=== Test Suite 4: Toolchain Scripts ===${NC}\n"
|
||||
|
||||
# Check all scripts exist and are 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
|
||||
|
||||
# Syntax check all bash scripts
|
||||
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
|
||||
|
||||
# ============================================================================
|
||||
# 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 options
|
||||
for opt in CONFIG_EFI_STUB CONFIG_BLK_DEV_NVME CONFIG_PREEMPT CONFIG_R8169 CONFIG_EXT4_FS CONFIG_MODULES; 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
|
||||
# ============================================================================
|
||||
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 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: QEMU Boot Test (skipped in quick mode)
|
||||
# ============================================================================
|
||||
if [ "$QUICK_MODE" = false ] && [ -n "$OVMF_PATH" ]; then
|
||||
echo -e "\n${BOLD}=== Test Suite 7: QEMU Boot Test ===${NC}\n"
|
||||
|
||||
ISO="${PROJECT_ROOT}/darkforge-live.iso"
|
||||
if [ -f "$ISO" ]; then
|
||||
echo " Testing ISO boot in QEMU (30s timeout)..."
|
||||
# Create a temp disk image
|
||||
QEMU_DISK=$(mktemp /tmp/darkforge-qemu-XXXXX.qcow2)
|
||||
qemu-img create -f qcow2 "$QEMU_DISK" 20G >/dev/null 2>&1
|
||||
|
||||
# Boot QEMU with serial console, timeout after 30s
|
||||
timeout 30 qemu-system-x86_64 \
|
||||
-enable-kvm \
|
||||
-m 2G \
|
||||
-bios "$OVMF_PATH" \
|
||||
-cdrom "$ISO" \
|
||||
-drive file="$QEMU_DISK",format=qcow2,if=virtio \
|
||||
-nographic \
|
||||
-serial mon:stdio \
|
||||
-no-reboot \
|
||||
2>"${SCRIPT_DIR}/qemu.log" | head -100 > "${SCRIPT_DIR}/qemu-output.log" &
|
||||
QEMU_PID=$!
|
||||
|
||||
sleep 30
|
||||
kill $QEMU_PID 2>/dev/null
|
||||
wait $QEMU_PID 2>/dev/null
|
||||
|
||||
# Check if we got kernel boot messages
|
||||
if grep -q "Linux version" "${SCRIPT_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 -q "login:" "${SCRIPT_DIR}/qemu-output.log" 2>/dev/null || \
|
||||
grep -q "DarkForge" "${SCRIPT_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
|
||||
record_test "qemu.iso_exists" "fail" "No ISO found — build it first with src/iso/build-iso.sh"
|
||||
record_test "qemu.kernel_boots" "skip" "No ISO"
|
||||
record_test "qemu.reaches_userspace" "skip" "No ISO"
|
||||
fi
|
||||
else
|
||||
echo -e "\n${BOLD}=== Test Suite 7: QEMU Boot Test (SKIPPED) ===${NC}\n"
|
||||
record_test "qemu.kernel_boots" "skip" "Quick mode or no OVMF"
|
||||
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
|
||||
Reference in New Issue
Block a user