diff --git a/tests/proxmox/README.md b/tests/proxmox/README.md new file mode 100644 index 0000000..b787f17 --- /dev/null +++ b/tests/proxmox/README.md @@ -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@ bash /home/darkforge/run-in-vm.sh +``` + +### 3. Collect the report + +```bash +scp darkforge@:/home/darkforge/darkforge/tests/report.json ./ +scp darkforge@:/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 diff --git a/tests/proxmox/create-vm.sh b/tests/proxmox/create-vm.sh new file mode 100755 index 0000000..fb4e9b5 --- /dev/null +++ b/tests/proxmox/create-vm.sh @@ -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 "═══════════════════════════════════════════════" diff --git a/tests/proxmox/run-in-vm.sh b/tests/proxmox/run-in-vm.sh new file mode 100755 index 0000000..b865b38 --- /dev/null +++ b/tests/proxmox/run-in-vm.sh @@ -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