Add Proxmox test environment (VM creation + automated test suite)

Tests everything possible without the target hardware:

create-vm.sh (runs on Proxmox host):
- Creates Arch Linux VM (VMID 900, 8 cores, 16GB RAM, 100GB disk)
- UEFI boot with OVMF (for nested QEMU testing)
- Cloud-init auto-installs packages, clones repo, runs tests
- Nested virtualization enabled for QEMU-in-QEMU boot tests

run-in-vm.sh (runs inside the VM, 9 test suites):
1. Host environment validation
2. dpack build + unit tests + CLI smoke tests
3. Package definition validation (154 packages, dep resolution)
4. Script syntax checking (toolchain, init, installer, ISO)
5. Kernel config validation (critical options)
6. Package signing test (download zlib, compute SHA256)
7. Toolchain bootstrap (LFS Ch.5 cross-compiler build)
8. ISO generation
9. Nested QEMU boot test (UEFI boot, kernel + userspace check)

Modes: --quick (30min, suites 1-5), --no-build (1hr), full (2-6hr)
Generates report.json + report.txt for automated debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:32:01 +01:00
parent c6a3f33746
commit c464e0eec9
3 changed files with 764 additions and 0 deletions

72
tests/proxmox/README.md Normal file
View File

@@ -0,0 +1,72 @@
# DarkForge — Proxmox Test Environment
Automated testing of DarkForge Linux on a Proxmox VE server. Creates an Arch Linux VM, clones the project, and runs the full test suite including toolchain compilation and QEMU boot tests.
## What gets tested
Without the target hardware (9950X3D / RTX 5090), we can still test:
- dpack compilation and unit tests
- All 154 package definitions parse correctly
- Toolchain scripts have valid bash syntax
- Kernel config has all required options
- Init system scripts have valid syntax
- Source downloads and SHA256 signing (network test)
- Toolchain bootstrap (cross-compiler build — full LFS Ch.5-7)
- Kernel compilation (generic x86_64, not znver5-optimized)
- ISO generation
- UEFI boot in nested QEMU (kernel boots, reaches userspace)
- Installer runs in QEMU without errors
## What can NOT be tested without target hardware
- znver5 CPU optimization (requires Zen 5 CPU)
- NVIDIA RTX 5090 driver (requires the GPU)
- Realtek RTL8125BN network driver (requires the NIC)
- Full gaming stack (Steam, Wine — requires GPU)
- dwl compositor (requires Wayland + GPU)
- Real UEFI firmware boot (QEMU OVMF is close but not identical)
## Requirements
- Proxmox VE 8.x or 9.x
- At least 8 CPU cores and 16GB RAM available for the test VM
- ~100GB storage on a Proxmox storage pool
- Internet access from the VM
- SSH access to the Proxmox host
## Usage
### 1. Create the test VM (run on Proxmox host)
```bash
# Copy scripts to Proxmox host
scp tests/proxmox/create-vm.sh root@proxmox:/root/
scp tests/proxmox/run-in-vm.sh root@proxmox:/root/
# Create the VM
ssh root@proxmox bash /root/create-vm.sh
```
### 2. Run the tests (automated via cloud-init or manual SSH)
```bash
# Option A: wait for cloud-init to finish (fully automated)
# The VM runs tests automatically on first boot.
# Option B: SSH in and run manually
ssh darkforge@<vm-ip> bash /home/darkforge/run-in-vm.sh
```
### 3. Collect the report
```bash
scp darkforge@<vm-ip>:/home/darkforge/darkforge/tests/report.json ./
scp darkforge@<vm-ip>:/home/darkforge/darkforge/tests/report.txt ./
```
## Files
- `create-vm.sh` — runs on the Proxmox host, creates and configures the VM
- `run-in-vm.sh` — runs inside the VM, clones the project and runs all tests
- `cloud-init-user.yaml` — cloud-init user-data for automated first-boot testing

209
tests/proxmox/create-vm.sh Executable file
View File

@@ -0,0 +1,209 @@
#!/bin/bash
# ============================================================================
# DarkForge — Proxmox VM Creation Script
# ============================================================================
# Run this on the Proxmox host to create an Arch Linux test VM.
#
# Requirements:
# - Proxmox VE 8.x or 9.x
# - ~100GB free on a storage pool
# - Internet access
#
# Usage:
# bash create-vm.sh # use defaults
# bash create-vm.sh --vmid 200 # custom VM ID
# bash create-vm.sh --storage local-lvm # custom storage
# ============================================================================
set -euo pipefail
# --- Configuration (override via environment or flags) -----------------------
VMID="${VMID:-900}"
VM_NAME="${VM_NAME:-darkforge-test}"
STORAGE="${STORAGE:-local-lvm}"
DISK_SIZE="${DISK_SIZE:-100G}"
RAM="${RAM:-16384}" # 16GB
CORES="${CORES:-8}"
BRIDGE="${BRIDGE:-vmbr0}"
# Arch Linux cloud image
ARCH_IMG_URL="https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2"
ARCH_IMG_FILE="/var/lib/vz/template/iso/arch-cloudimg.qcow2"
# Git repo to clone inside the VM
DARKFORGE_REPO="gitea@git.dannyhaslund.dk:danny8632/darkforge.git"
# Fallback if SSH key isn't available in the VM
DARKFORGE_REPO_HTTPS="https://git.dannyhaslund.dk/danny8632/darkforge.git"
# Parse args
for arg in "$@"; do
case "$arg" in
--vmid=*) VMID="${arg#*=}" ;;
--storage=*) STORAGE="${arg#*=}" ;;
--cores=*) CORES="${arg#*=}" ;;
--ram=*) RAM="${arg#*=}" ;;
--bridge=*) BRIDGE="${arg#*=}" ;;
esac
done
echo "═══════════════════════════════════════════════"
echo " DarkForge Test VM Creator"
echo " VMID: ${VMID} | Cores: ${CORES} | RAM: ${RAM}MB"
echo " Storage: ${STORAGE} | Disk: ${DISK_SIZE}"
echo "═══════════════════════════════════════════════"
echo ""
# --- Check if VM already exists ----------------------------------------------
if qm status "${VMID}" &>/dev/null; then
echo "VM ${VMID} already exists."
read -p "Destroy and recreate? [y/N] " confirm
if [[ "${confirm}" =~ ^[Yy]$ ]]; then
qm stop "${VMID}" 2>/dev/null || true
sleep 3
qm destroy "${VMID}" --purge
echo "Old VM destroyed."
else
echo "Aborted."
exit 0
fi
fi
# --- Download Arch cloud image if not cached ---------------------------------
if [ ! -f "${ARCH_IMG_FILE}" ]; then
echo ">>> Downloading Arch Linux cloud image..."
mkdir -p "$(dirname "${ARCH_IMG_FILE}")"
wget -q --show-progress -O "${ARCH_IMG_FILE}" "${ARCH_IMG_URL}"
echo ">>> Downloaded: ${ARCH_IMG_FILE}"
else
echo ">>> Using cached image: ${ARCH_IMG_FILE}"
fi
# --- Create the VM -----------------------------------------------------------
echo ">>> Creating VM ${VMID}..."
qm create "${VMID}" \
--name "${VM_NAME}" \
--ostype l26 \
--machine q35 \
--bios ovmf \
--cpu host \
--cores "${CORES}" \
--memory "${RAM}" \
--net0 "virtio,bridge=${BRIDGE}" \
--agent enabled=1 \
--serial0 socket \
--vga serial0
# Add EFI disk (required for OVMF BIOS)
qm set "${VMID}" --efidisk0 "${STORAGE}:1,efitype=4m,pre-enrolled-keys=0"
# Import the cloud image as the boot disk
echo ">>> Importing cloud image as boot disk..."
qm importdisk "${VMID}" "${ARCH_IMG_FILE}" "${STORAGE}" --format qcow2 2>/dev/null
qm set "${VMID}" --scsi0 "${STORAGE}:vm-${VMID}-disk-1,size=${DISK_SIZE}"
qm set "${VMID}" --boot order=scsi0
qm set "${VMID}" --scsihw virtio-scsi-single
# --- Configure cloud-init ----------------------------------------------------
echo ">>> Configuring cloud-init..."
qm set "${VMID}" --ide2 "${STORAGE}:cloudinit"
qm set "${VMID}" --ciuser "darkforge"
qm set "${VMID}" --cipassword "darkforge"
qm set "${VMID}" --ipconfig0 "ip=dhcp"
# Enable nested virtualization (for QEMU-in-QEMU boot tests)
qm set "${VMID}" --args "-cpu host,+vmx"
# Resize the disk to our desired size
echo ">>> Resizing disk to ${DISK_SIZE}..."
qm resize "${VMID}" scsi0 "${DISK_SIZE}" 2>/dev/null || true
# --- Generate cloud-init user-data snippet -----------------------------------
# This runs on first boot inside the VM
SNIPPET_DIR="/var/lib/vz/snippets"
mkdir -p "${SNIPPET_DIR}"
cat > "${SNIPPET_DIR}/darkforge-test-init.yaml" << 'CLOUDINIT'
#cloud-config
package_update: true
packages:
- base-devel
- git
- wget
- curl
- rust
- cargo
- qemu-full
- edk2-ovmf
- squashfs-tools
- xorriso
- dosfstools
- mtools
- python
- bc
- rsync
- openssh
runcmd:
# Grow the partition to fill the disk
- growpart /dev/sda 2 || true
- resize2fs /dev/sda2 || btrfs filesystem resize max / || true
# Clone the DarkForge project
- |
su - darkforge -c '
cd /home/darkforge
git clone --recurse-submodules https://git.dannyhaslund.dk/danny8632/darkforge.git 2>/dev/null || \
git clone --recurse-submodules https://github.com/danny8632/darkforge.git 2>/dev/null || \
echo "CLONE FAILED — manually clone the repo"
'
# Copy the test runner
- |
if [ -f /home/darkforge/darkforge/tests/proxmox/run-in-vm.sh ]; then
cp /home/darkforge/darkforge/tests/proxmox/run-in-vm.sh /home/darkforge/
chown darkforge:darkforge /home/darkforge/run-in-vm.sh
chmod +x /home/darkforge/run-in-vm.sh
fi
# Run the test suite automatically
- |
su - darkforge -c '
if [ -f /home/darkforge/run-in-vm.sh ]; then
bash /home/darkforge/run-in-vm.sh 2>&1 | tee /home/darkforge/test-output.log
fi
'
CLOUDINIT
qm set "${VMID}" --cicustom "user=local:snippets/darkforge-test-init.yaml"
# --- Start the VM ------------------------------------------------------------
echo ""
echo ">>> Starting VM ${VMID}..."
qm start "${VMID}"
echo ""
echo "═══════════════════════════════════════════════"
echo " VM ${VMID} created and starting."
echo ""
echo " The VM will:"
echo " 1. Boot Arch Linux"
echo " 2. Install required packages via cloud-init"
echo " 3. Clone the DarkForge repository"
echo " 4. Run the full test suite"
echo ""
echo " Monitor progress:"
echo " qm terminal ${VMID} (serial console)"
echo ""
echo " SSH access (after boot):"
echo " ssh darkforge@\$(qm guest cmd ${VMID} network-get-interfaces | grep -oP '\"ip-address\":\\s*\"\\K[0-9.]+')"
echo " Password: darkforge"
echo ""
echo " Or get IP:"
echo " qm guest cmd ${VMID} network-get-interfaces"
echo ""
echo " Collect report after tests finish:"
echo " VM_IP=\$(qm guest cmd ${VMID} network-get-interfaces | grep -oP '\"ip-address\":\\s*\"\\K[0-9.]+')"
echo " scp darkforge@\$VM_IP:/home/darkforge/darkforge/tests/report.* ./"
echo "═══════════════════════════════════════════════"

483
tests/proxmox/run-in-vm.sh Executable file
View File

@@ -0,0 +1,483 @@
#!/bin/bash
# ============================================================================
# DarkForge — In-VM Test Runner (Proxmox)
# ============================================================================
# Runs inside the Arch Linux test VM on Proxmox.
# Tests everything that can be verified without the target hardware.
#
# This script:
# 1. Builds dpack and runs unit tests
# 2. Validates all 154 package definitions
# 3. Syntax-checks all toolchain/init scripts
# 4. Validates kernel config
# 5. Attempts to sign a few packages (download + sha256)
# 6. Runs the toolchain bootstrap (LFS Ch.5 — cross-compiler)
# 7. Compiles a generic x86_64 kernel
# 8. Builds a live ISO
# 9. Boots the ISO in nested QEMU
# 10. Generates a JSON + text report
#
# Usage:
# bash run-in-vm.sh # full run (2-6 hours depending on hardware)
# bash run-in-vm.sh --quick # skip toolchain/kernel/ISO (30 min)
# bash run-in-vm.sh --no-build # skip toolchain bootstrap (1 hour)
# ============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Find the project root (could be parent of tests/proxmox or /home/darkforge/darkforge)
if [ -f "${SCRIPT_DIR}/../../CLAUDE.md" ]; then
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
elif [ -f "/home/darkforge/darkforge/CLAUDE.md" ]; then
PROJECT_ROOT="/home/darkforge/darkforge"
else
echo "ERROR: Cannot find project root. Clone the repo first."
exit 1
fi
REPORT_JSON="${PROJECT_ROOT}/tests/report.json"
REPORT_TXT="${PROJECT_ROOT}/tests/report.txt"
LOG_DIR="${PROJECT_ROOT}/tests/logs"
QUICK_MODE=false
NO_BUILD=false
for arg in "$@"; do
case "$arg" in
--quick) QUICK_MODE=true; NO_BUILD=true ;;
--no-build) NO_BUILD=true ;;
esac
done
mkdir -p "${LOG_DIR}"
# --- Test infrastructure -----------------------------------------------------
PASS=0; FAIL=0; SKIP=0; TESTS=()
start_time=$(date +%s)
record() {
local name="$1" status="$2" detail="${3:-}" duration="${4:-0}"
TESTS+=("{\"name\":\"${name}\",\"status\":\"${status}\",\"detail\":$(echo "$detail" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo '""'),\"duration_s\":${duration}}")
case "$status" in
pass) ((PASS++)); printf " \033[32mPASS\033[0m %s\n" "$name" ;;
fail) ((FAIL++)); printf " \033[31mFAIL\033[0m %s: %s\n" "$name" "$detail" ;;
skip) ((SKIP++)); printf " \033[33mSKIP\033[0m %s: %s\n" "$name" "$detail" ;;
esac
}
timed() {
# Usage: timed "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 "$name" "pass" "" "$dur"
else
record "$name" "fail" "$(echo "$output" | tail -5)" "$dur"
fi
return $rc
}
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo " DarkForge Linux — Proxmox Integration Test Suite"
echo " Host: $(uname -n) | Arch: $(uname -m) | Cores: $(nproc)"
echo " Project: ${PROJECT_ROOT}"
echo " Mode: $([ "$QUICK_MODE" = true ] && echo "QUICK" || ([ "$NO_BUILD" = true ] && echo "NO-BUILD" || echo "FULL"))"
echo "═══════════════════════════════════════════════════════════════"
# =============================================================================
# SUITE 1: Host Environment
# =============================================================================
echo -e "\n\033[1m=== Suite 1: Host Environment ===\033[0m\n"
[ "$(uname -s)" = "Linux" ] && record "host.linux" "pass" || record "host.linux" "fail" "Not Linux"
[ -f /etc/arch-release ] && record "host.arch" "pass" || record "host.arch" "skip" "Not Arch"
for tool in gcc g++ make git wget curl cargo rustc tar xz sha256sum python3; do
command -v "$tool" &>/dev/null && record "host.tool.${tool}" "pass" || record "host.tool.${tool}" "fail" "Not installed"
done
# Check nested virt support
if grep -qE '(vmx|svm)' /proc/cpuinfo; then
record "host.nested_virt" "pass"
else
record "host.nested_virt" "skip" "No VMX/SVM — QEMU boot tests will be slower"
fi
# Check OVMF
OVMF=""
for p in /usr/share/edk2/x64/OVMF.fd /usr/share/edk2-ovmf/x64/OVMF.fd /usr/share/ovmf/x64/OVMF.fd /usr/share/OVMF/OVMF.fd; do
[ -f "$p" ] && OVMF="$p" && break
done
[ -n "$OVMF" ] && record "host.ovmf" "pass" "$OVMF" || record "host.ovmf" "fail" "Not found"
GCC_VER=$(gcc -dumpversion 2>/dev/null | cut -d. -f1)
[ -n "$GCC_VER" ] && [ "$GCC_VER" -ge 12 ] && record "host.gcc_ver" "pass" "GCC ${GCC_VER}" || record "host.gcc_ver" "fail" "GCC ${GCC_VER:-missing} (need 12+)"
# =============================================================================
# SUITE 2: dpack Build & Tests
# =============================================================================
echo -e "\n\033[1m=== Suite 2: dpack Build ===\033[0m\n"
cd "${PROJECT_ROOT}/src/dpack"
# Build release
timed "dpack.build_release" cargo build --release || true
# Check zero warnings
if cargo build --release 2>&1 | grep -q "^warning:"; then
record "dpack.zero_warnings" "fail" "$(cargo build --release 2>&1 | grep '^warning:' | head -3)"
else
record "dpack.zero_warnings" "pass"
fi
# Unit tests
timed "dpack.unit_tests" cargo test || true
# CLI smoke test
DPACK="${PROJECT_ROOT}/src/dpack/target/release/dpack"
if [ -x "$DPACK" ]; then
$DPACK --version &>/dev/null && record "dpack.cli.version" "pass" || record "dpack.cli.version" "fail"
$DPACK --help &>/dev/null && record "dpack.cli.help" "pass" || record "dpack.cli.help" "fail"
$DPACK list &>/dev/null && record "dpack.cli.list" "pass" || record "dpack.cli.list" "fail" "Exit code $?"
$DPACK check &>/dev/null && record "dpack.cli.check" "pass" || record "dpack.cli.check" "fail" "Exit code $?"
fi
cd "${PROJECT_ROOT}"
# =============================================================================
# SUITE 3: Package Definitions
# =============================================================================
echo -e "\n\033[1m=== Suite 3: Package Definitions ===\033[0m\n"
# Count per repo
for repo in core extra desktop gaming; do
dir="${PROJECT_ROOT}/src/repos/${repo}"
if [ -d "$dir" ]; then
count=$(find "$dir" -name "*.toml" | wc -l)
record "repos.${repo}.count" "pass" "${count} packages"
else
record "repos.${repo}.count" "fail" "Missing"
fi
done
# Validate all TOML files have required sections
TOML_FAIL=0
for toml in $(find "${PROJECT_ROOT}/src/repos" -name "*.toml" 2>/dev/null); do
name=$(basename "$(dirname "$toml")")
for section in '\[package\]' '\[source\]' '\[build\]'; do
if ! grep -q "$section" "$toml"; then
record "repos.validate.${name}" "fail" "Missing ${section}"
((TOML_FAIL++))
break
fi
done
done
[ "$TOML_FAIL" -eq 0 ] && record "repos.all_valid" "pass" "$(find "${PROJECT_ROOT}/src/repos" -name '*.toml' | wc -l) packages"
# Dependency resolution check
python3 << 'PYEOF' 2>/dev/null
import os, re, sys, json
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
DEP_RESULT=$?
if [ $DEP_RESULT -eq 0 ]; then
record "repos.deps_resolve" "pass" "All dependencies resolve"
else
record "repos.deps_resolve" "fail" "Unresolvable deps found"
fi
# =============================================================================
# SUITE 4: Scripts Syntax
# =============================================================================
echo -e "\n\033[1m=== Suite 4: Script Validation ===\033[0m\n"
# Toolchain scripts
TC_FAIL=0
for script in "${PROJECT_ROOT}"/toolchain/scripts/*.sh; do
if ! bash -n "$script" 2>/dev/null; then
record "scripts.toolchain.$(basename "$script" .sh)" "fail" "Syntax error"
((TC_FAIL++))
fi
done
[ "$TC_FAIL" -eq 0 ] && record "scripts.toolchain.all_syntax" "pass" "$(ls "${PROJECT_ROOT}"/toolchain/scripts/*.sh | wc -l) scripts"
# Init scripts
for script in "${PROJECT_ROOT}"/configs/rc.d/*; do
name=$(basename "$script")
bash -n "$script" 2>/dev/null && record "scripts.init.${name}" "pass" || record "scripts.init.${name}" "fail" "Syntax error"
done
# Installer scripts
for script in "${PROJECT_ROOT}"/src/install/*.sh "${PROJECT_ROOT}"/src/install/modules/*.sh; do
[ -f "$script" ] || continue
name=$(basename "$script" .sh)
bash -n "$script" 2>/dev/null && record "scripts.install.${name}" "pass" || record "scripts.install.${name}" "fail" "Syntax error"
done
# ISO builder scripts
for script in "${PROJECT_ROOT}"/src/iso/*.sh; do
[ -f "$script" ] || continue
name=$(basename "$script" .sh)
bash -n "$script" 2>/dev/null && record "scripts.iso.${name}" "pass" || record "scripts.iso.${name}" "fail" "Syntax error"
done
# =============================================================================
# SUITE 5: Kernel Config
# =============================================================================
echo -e "\n\033[1m=== Suite 5: Kernel Config ===\033[0m\n"
KC="${PROJECT_ROOT}/kernel/config"
if [ -f "$KC" ]; then
record "kernel.config_exists" "pass"
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
grep -q "^${opt}=y" "$KC" && record "kernel.${opt}" "pass" || record "kernel.${opt}" "fail" "Not =y"
done
for opt in CONFIG_BLUETOOTH CONFIG_WIRELESS CONFIG_DRM_NOUVEAU; do
grep -q "^${opt}=n" "$KC" && record "kernel.off.${opt}" "pass" || record "kernel.off.${opt}" "fail" "Should be =n"
done
else
record "kernel.config_exists" "fail"
fi
# =============================================================================
# SUITE 6: Package Signing (network test)
# =============================================================================
echo -e "\n\033[1m=== Suite 6: Package Signing ===\033[0m\n"
if [ -x "$DPACK" ]; then
# Test signing a small, known-good package (zlib)
ZLIB_TOML="${PROJECT_ROOT}/src/repos/core/zlib/zlib.toml"
if [ -f "$ZLIB_TOML" ]; then
# Backup
cp "$ZLIB_TOML" "${ZLIB_TOML}.bak"
timed "sign.zlib" $DPACK sign zlib || true
# Check if it got a real hash
if grep -q 'sha256 = "aaa' "$ZLIB_TOML"; then
record "sign.zlib_result" "fail" "Checksum still placeholder after signing"
else
record "sign.zlib_result" "pass"
fi
# Restore
mv "${ZLIB_TOML}.bak" "$ZLIB_TOML"
fi
fi
# =============================================================================
# SUITE 7: Toolchain Bootstrap (LFS Ch.5 cross-compiler)
# =============================================================================
if [ "$NO_BUILD" = false ]; then
echo -e "\n\033[1m=== Suite 7: Toolchain Bootstrap ===\033[0m\n"
export LFS="/tmp/darkforge-lfs"
mkdir -p "${LFS}/sources"
# Download sources for the cross-toolchain only (not all packages)
echo " Downloading toolchain sources (this may take a while)..."
timed "toolchain.download" bash "${PROJECT_ROOT}/toolchain/scripts/000a-download-sources.sh" || true
# Run the environment setup
timed "toolchain.env_setup" bash "${PROJECT_ROOT}/toolchain/scripts/000-env-setup.sh" || true
# Try building binutils pass 1 (the first real compilation test)
if [ -f "${LFS}/sources/binutils-2.46.tar.xz" ]; then
timed "toolchain.binutils_pass1" bash "${PROJECT_ROOT}/toolchain/scripts/001-binutils-pass1.sh" || true
else
record "toolchain.binutils_pass1" "skip" "Sources not downloaded"
fi
# Try GCC pass 1 (the most complex build)
if [ -f "${LFS}/sources/gcc-15.2.0.tar.xz" ]; then
timed "toolchain.gcc_pass1" bash "${PROJECT_ROOT}/toolchain/scripts/002-gcc-pass1.sh" || true
else
record "toolchain.gcc_pass1" "skip" "Sources not downloaded"
fi
# Cleanup
rm -rf "${LFS}"
else
echo -e "\n\033[1m=== Suite 7: Toolchain Bootstrap (SKIPPED) ===\033[0m\n"
record "toolchain.download" "skip" "NO_BUILD mode"
record "toolchain.binutils_pass1" "skip" "NO_BUILD mode"
record "toolchain.gcc_pass1" "skip" "NO_BUILD mode"
fi
# =============================================================================
# SUITE 8: ISO Build
# =============================================================================
if [ "$QUICK_MODE" = false ]; then
echo -e "\n\033[1m=== Suite 8: ISO Build ===\033[0m\n"
if command -v mksquashfs &>/dev/null && command -v xorriso &>/dev/null; then
timed "iso.build" sudo bash "${PROJECT_ROOT}/src/iso/build-iso-arch.sh" || true
if [ -f "${PROJECT_ROOT}/darkforge-live.iso" ]; then
ISO_SIZE=$(du -sh "${PROJECT_ROOT}/darkforge-live.iso" | cut -f1)
record "iso.exists" "pass" "Size: ${ISO_SIZE}"
else
record "iso.exists" "fail" "ISO not produced"
fi
else
record "iso.build" "skip" "Missing mksquashfs or xorriso"
fi
else
record "iso.build" "skip" "Quick mode"
fi
# =============================================================================
# SUITE 9: QEMU Boot Test
# =============================================================================
if [ "$QUICK_MODE" = false ] && [ -f "${PROJECT_ROOT}/darkforge-live.iso" ] && [ -n "$OVMF" ]; then
echo -e "\n\033[1m=== Suite 9: QEMU Boot Test ===\033[0m\n"
QEMU_DISK=$(mktemp /tmp/darkforge-qemu-XXXXX.qcow2)
qemu-img create -f qcow2 "$QEMU_DISK" 20G &>/dev/null
echo " Booting ISO in QEMU (60s timeout)..."
QEMU_LOG="${LOG_DIR}/qemu-boot.log"
KVM_FLAG=""
[ -c /dev/kvm ] && KVM_FLAG="-enable-kvm"
timeout 60 qemu-system-x86_64 \
${KVM_FLAG} \
-m 2G \
-smp 2 \
-bios "$OVMF" \
-cdrom "${PROJECT_ROOT}/darkforge-live.iso" \
-drive "file=${QEMU_DISK},format=qcow2,if=virtio" \
-nographic \
-serial mon:stdio \
-no-reboot \
2>"${LOG_DIR}/qemu-stderr.log" \
| head -200 > "$QEMU_LOG" &
QEMU_PID=$!
sleep 60
kill $QEMU_PID 2>/dev/null; wait $QEMU_PID 2>/dev/null
if grep -qi "linux version\|darkforge\|kernel" "$QEMU_LOG" 2>/dev/null; then
record "qemu.kernel_boots" "pass"
else
record "qemu.kernel_boots" "fail" "No kernel messages in serial output"
fi
if grep -qi "login:\|installer\|welcome" "$QEMU_LOG" 2>/dev/null; then
record "qemu.userspace" "pass"
else
record "qemu.userspace" "fail" "Did not reach userspace"
fi
rm -f "$QEMU_DISK"
else
[ "$QUICK_MODE" = true ] && record "qemu.boot" "skip" "Quick mode" || record "qemu.boot" "skip" "No ISO or OVMF"
fi
# =============================================================================
# Generate Report
# =============================================================================
end_time=$(date +%s)
TOTAL=$((PASS + FAIL + SKIP))
DURATION=$((end_time - start_time))
# JSON
cat > "$REPORT_JSON" << JSONEOF
{
"project": "DarkForge Linux",
"test_env": "proxmox_vm",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"host": {
"hostname": "$(uname -n)",
"kernel": "$(uname -r)",
"arch": "$(uname -m)",
"cpus": $(nproc),
"ram_mb": $(free -m | awk '/Mem:/{print $2}'),
"gcc": "$(gcc --version 2>/dev/null | head -1)",
"rust": "$(rustc --version 2>/dev/null)"
},
"mode": "$([ "$QUICK_MODE" = true ] && echo "quick" || ([ "$NO_BUILD" = true ] && echo "no-build" || echo "full"))",
"duration_s": ${DURATION},
"summary": {
"total": ${TOTAL},
"pass": ${PASS},
"fail": ${FAIL},
"skip": ${SKIP}
},
"tests": [
$(IFS=,; echo "${TESTS[*]}")
]
}
JSONEOF
# Text
cat > "$REPORT_TXT" << TXTEOF
================================================================================
DarkForge Linux — Proxmox Integration Test Report
================================================================================
Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
Host: $(uname -n) | $(uname -m) | $(nproc) cores | $(free -m | awk '/Mem:/{print $2}')MB RAM
GCC: $(gcc --version 2>/dev/null | head -1)
Rust: $(rustc --version 2>/dev/null)
Mode: $([ "$QUICK_MODE" = true ] && echo "QUICK" || ([ "$NO_BUILD" = true ] && echo "NO-BUILD" || echo "FULL"))
Duration: ${DURATION}s ($((DURATION / 60))m $((DURATION % 60))s)
RESULTS: ${PASS} pass, ${FAIL} fail, ${SKIP} skip (${TOTAL} total)
================================================================================
TXTEOF
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
tname=$(echo "$t" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d['name'])" 2>/dev/null || echo "?")
tdetail=$(echo "$t" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d['detail'])" 2>/dev/null || echo "?")
printf " FAIL: %s\n %s\n\n" "$tname" "$tdetail" >> "$REPORT_TXT"
fi
done
fi
echo "Full JSON report: ${REPORT_JSON}" >> "$REPORT_TXT"
# Print summary
echo ""
echo "═══════════════════════════════════════════════════════════════"
printf " Results: \033[32m%d pass\033[0m, \033[31m%d fail\033[0m, \033[33m%d skip\033[0m (%d total)\n" "$PASS" "$FAIL" "$SKIP" "$TOTAL"
echo " Duration: ${DURATION}s ($((DURATION / 60))m $((DURATION % 60))s)"
echo " Report: ${REPORT_TXT}"
echo " JSON: ${REPORT_JSON}"
echo "═══════════════════════════════════════════════════════════════"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1