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>
484 lines
19 KiB
Bash
Executable File
484 lines
19 KiB
Bash
Executable File
#!/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
|