smol-machines--smolvm
432 行
13 KiB
Bash
可执行文件
432 行
13 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
|
#
|
|
# Common test utilities for smolvm integration tests.
|
|
#
|
|
# Source this file in test scripts:
|
|
# source "$(dirname "$0")/common.sh"
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Test counters
|
|
TESTS_RUN=0
|
|
TESTS_PASSED=0
|
|
TESTS_FAILED=0
|
|
|
|
# Find the script directory and project root
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
# Find smolvm binary
|
|
find_smolvm() {
|
|
if [[ -n "${SMOLVM:-}" ]] && [[ -x "$SMOLVM" ]]; then
|
|
echo "$SMOLVM"
|
|
return
|
|
fi
|
|
|
|
# Prefer cargo build output (latest build) over dist
|
|
local target_release="$PROJECT_ROOT/target/release/smolvm"
|
|
if [[ -x "$target_release" ]]; then
|
|
echo "$target_release"
|
|
return
|
|
fi
|
|
|
|
# Fall back to dist directory
|
|
local dist_dir="$PROJECT_ROOT/dist"
|
|
if [[ -d "$dist_dir" ]]; then
|
|
# Find the extracted distribution directory
|
|
local smolvm_dir=$(find "$dist_dir" -maxdepth 1 -type d \( -name 'smolvm-*-darwin-*' -o -name 'smolvm-*-linux-*' \) 2>/dev/null | head -1)
|
|
if [[ -n "$smolvm_dir" ]] && [[ -x "$smolvm_dir/smolvm" ]]; then
|
|
echo "$smolvm_dir/smolvm"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
# Initialize SMOLVM variable
|
|
init_smolvm() {
|
|
SMOLVM=$(find_smolvm)
|
|
|
|
# Resolve to absolute path (tests cd into temp dirs)
|
|
if [[ -n "$SMOLVM" ]] && [[ "$SMOLVM" != /* ]]; then
|
|
SMOLVM="$(cd "$(dirname "$SMOLVM")" && pwd)/$(basename "$SMOLVM")"
|
|
fi
|
|
|
|
if [[ -z "$SMOLVM" ]]; then
|
|
echo -e "${RED}Error: Could not find smolvm binary${NC}"
|
|
echo "Either:"
|
|
echo " 1. Build and extract the distribution: ./scripts/build-dist.sh"
|
|
echo " 2. Set SMOLVM environment variable to the binary path"
|
|
exit 1
|
|
fi
|
|
|
|
# Set library path to ensure we use bundled libkrun/libkrunfw.
|
|
# This is needed when running from target/release since the system
|
|
# may not have libkrun on its default library search path.
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
local lib_dir="$PROJECT_ROOT/lib"
|
|
if [[ -d "$lib_dir" ]]; then
|
|
export DYLD_LIBRARY_PATH="${lib_dir}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
|
|
fi
|
|
else
|
|
local lib_dir="$PROJECT_ROOT/lib/linux-$(uname -m)"
|
|
if [[ -d "$lib_dir" ]]; then
|
|
export LD_LIBRARY_PATH="${lib_dir}:${LD_LIBRARY_PATH:-}"
|
|
fi
|
|
fi
|
|
|
|
echo "Using smolvm: $SMOLVM"
|
|
}
|
|
|
|
# Resolve a hostname to a single IPv4 address on the host, portably across
|
|
# Linux and macOS. Linux glibc has `getent`; macOS does not, so fall back to
|
|
# dig/python3/host (all present on a stock macOS or with bind tools). Prints the
|
|
# IP on stdout, or nothing if resolution fails.
|
|
resolve_host_ipv4() {
|
|
local host="$1" ip=""
|
|
if command -v getent >/dev/null 2>&1; then
|
|
ip=$(getent ahostsv4 "$host" 2>/dev/null | awk 'NR==1{print $1}')
|
|
fi
|
|
if [[ -z "$ip" ]] && command -v dig >/dev/null 2>&1; then
|
|
ip=$(dig +short "$host" A 2>/dev/null | grep -m1 -E '^[0-9]+(\.[0-9]+){3}$')
|
|
fi
|
|
if [[ -z "$ip" ]] && command -v python3 >/dev/null 2>&1; then
|
|
ip=$(python3 -c 'import socket,sys; print(socket.gethostbyname(sys.argv[1]))' "$host" 2>/dev/null)
|
|
fi
|
|
if [[ -z "$ip" ]] && command -v host >/dev/null 2>&1; then
|
|
ip=$(host -t A "$host" 2>/dev/null | awk '/has address/{print $NF; exit}')
|
|
fi
|
|
printf '%s' "$ip"
|
|
}
|
|
|
|
# Log helpers
|
|
log_test() {
|
|
echo -e "${YELLOW}[TEST]${NC} $1"
|
|
}
|
|
|
|
log_pass() {
|
|
echo -e "${GREEN}[PASS]${NC} $1"
|
|
((TESTS_PASSED++))
|
|
}
|
|
|
|
log_fail() {
|
|
echo -e "${RED}[FAIL]${NC} $1"
|
|
((TESTS_FAILED++))
|
|
}
|
|
|
|
log_skip() {
|
|
echo -e "${BLUE}[SKIP]${NC} $1"
|
|
}
|
|
|
|
log_info() {
|
|
echo -e "${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
# Track failed test names for summary
|
|
FAILED_TESTS=()
|
|
|
|
# Fail-fast mode: stop on first failure.
|
|
# Set FAIL_FAST=1 or use TEST_FILTER with run_tests.sh.
|
|
FAIL_FAST="${FAIL_FAST:-0}"
|
|
|
|
# Single test filter: only run tests whose name contains this string.
|
|
# Usage: TEST_FILTER="port mapping" ./tests/run_tests.sh ports
|
|
TEST_FILTER="${TEST_FILTER:-}"
|
|
|
|
# Skip slow tests (≥25s intentional sleeps) when SMOLVM_SKIP_SLOW=1.
|
|
# Usage inside a test function: skip_if_slow && return 0
|
|
SMOLVM_SKIP_SLOW="${SMOLVM_SKIP_SLOW:-0}"
|
|
|
|
skip_if_slow() {
|
|
if [[ "$SMOLVM_SKIP_SLOW" == "1" ]]; then
|
|
log_skip "slow test skipped (SMOLVM_SKIP_SLOW=1)"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Run a test function, capturing output and showing it on failure.
|
|
run_test() {
|
|
local test_name="$1"
|
|
local test_func="$2"
|
|
|
|
# Skip if filter is set and test name doesn't match
|
|
if [[ -n "$TEST_FILTER" ]] && [[ "$test_name" != *"$TEST_FILTER"* ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Skip remaining tests if fail-fast triggered
|
|
if [[ "$FAIL_FAST" == "1" ]] && [[ $TESTS_FAILED -gt 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
((TESTS_RUN++))
|
|
log_test "$test_name"
|
|
|
|
local output_file
|
|
output_file=$(mktemp)
|
|
|
|
if $test_func 2>&1 | tee "$output_file"; then
|
|
log_pass "$test_name"
|
|
rm -f "$output_file"
|
|
return 0
|
|
else
|
|
log_fail "$test_name"
|
|
FAILED_TESTS+=("$test_name")
|
|
|
|
# Show last 10 lines on failure (may already be visible, but
|
|
# repeating under the FAIL marker makes it easy to find)
|
|
local output
|
|
output=$(tail -10 "$output_file" 2>/dev/null || true)
|
|
if [[ -n "$output" ]]; then
|
|
echo -e " ${RED}Error output:${NC}"
|
|
echo "$output" | sed 's/^/ /'
|
|
fi
|
|
rm -f "$output_file"
|
|
|
|
if [[ "$FAIL_FAST" == "1" ]]; then
|
|
echo -e "\n${RED}Stopping: --fail-fast is set${NC}"
|
|
fi
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Print test summary
|
|
print_summary() {
|
|
local test_suite="${1:-Tests}"
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " $test_suite Summary"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo "Tests run: $TESTS_RUN"
|
|
echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
|
|
echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
|
|
|
|
if [[ $TESTS_FAILED -gt 0 ]] && [[ ${#FAILED_TESTS[@]} -gt 0 ]]; then
|
|
echo ""
|
|
echo -e "${RED}Failed tests:${NC}"
|
|
for test_name in "${FAILED_TESTS[@]}"; do
|
|
echo -e " ${RED}✗${NC} $test_name"
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
|
|
if [[ $TESTS_FAILED -eq 0 ]]; then
|
|
echo -e "${GREEN}All tests passed!${NC}"
|
|
return 0
|
|
else
|
|
echo -e "${RED}Some tests failed.${NC}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Get the data directory for a named machine.
|
|
#
|
|
# Delegates to `smolvm machine data-dir --name <name>` so the test helper never
|
|
# duplicates the hash logic. If Rust changes the on-disk layout, the test
|
|
# suite automatically picks it up via this CLI call.
|
|
vm_data_dir() {
|
|
local name="${1:-default}"
|
|
$SMOLVM machine data-dir --name "$name" 2>/dev/null
|
|
}
|
|
|
|
# Cleanup helper - stop machine and remove named "default" from DB
|
|
# so tests start from a clean slate (no leftover DB records from
|
|
# manual testing or previous test runs).
|
|
cleanup_machine() {
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
}
|
|
|
|
# Verify that a VM's data directory was removed after deletion.
|
|
# Returns non-zero if the directory still exists.
|
|
ensure_data_dir_deleted() {
|
|
local name="${1:?vm name required}"
|
|
local data_dir
|
|
data_dir=$(vm_data_dir "$name")
|
|
if [[ -d "$data_dir" ]]; then
|
|
echo -e "${RED}ERROR: data directory still exists after delete: $data_dir${NC}" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Ensure machine is running and reachable.
|
|
# If net=true, recreate with --net (needed for container image pulls).
|
|
# Handles stale "already running" state from previous tests by verifying
|
|
# connectivity and doing a full cleanup cycle if the VM is unreachable.
|
|
ensure_machine_running() {
|
|
local with_net="${1:-false}"
|
|
if [[ "$with_net" == "true" ]]; then
|
|
# Stop and delete existing default VM, recreate with --net
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
$SMOLVM machine create --name default --net 2>/dev/null || true
|
|
fi
|
|
$SMOLVM machine start 2>/dev/null || true
|
|
|
|
# Verify the VM is actually reachable. If it reports "running" but
|
|
# the process is dead (stale PID), do a full cleanup and restart.
|
|
if ! $SMOLVM machine exec -- true 2>/dev/null; then
|
|
$SMOLVM machine stop 2>/dev/null || true
|
|
$SMOLVM machine delete --name default -f 2>/dev/null || true
|
|
if [[ "$with_net" == "true" ]]; then
|
|
$SMOLVM machine create --name default --net 2>/dev/null || true
|
|
fi
|
|
$SMOLVM machine start 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Poll until a VM responds to exec (i.e., the agent is ready to accept commands).
|
|
# Replaces fixed sleep N readiness waits after machine start.
|
|
#
|
|
# Usage: wait_vm_ready [--name NAME] [TIMEOUT_SECS]
|
|
# --name NAME Named VM (default: unnamed default VM)
|
|
# TIMEOUT_SECS Give up after this many seconds (default: 10)
|
|
#
|
|
# Returns 0 when ready, 1 on timeout.
|
|
wait_vm_ready() {
|
|
local name_flag="" timeout=10
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--name) name_flag="--name $2"; shift 2 ;;
|
|
*) timeout="$1"; shift ;;
|
|
esac
|
|
done
|
|
local i=0
|
|
while [[ $i -lt $((timeout * 2)) ]]; do
|
|
$SMOLVM machine exec $name_flag -- true 2>/dev/null && return 0
|
|
sleep 0.5
|
|
((i++)) || true
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Extract container ID from output
|
|
extract_container_id() {
|
|
local output="$1"
|
|
echo "$output" | grep -oE 'smolvm-[a-f0-9]+' | head -1
|
|
}
|
|
|
|
# Cleanup container by ID
|
|
cleanup_container() {
|
|
local container_id="$1"
|
|
$SMOLVM container rm --container "$container_id" -f 2>/dev/null || true
|
|
}
|
|
|
|
# Run a command with a timeout (default 60 seconds).
|
|
# Usage: run_with_timeout [timeout_seconds] command [args...]
|
|
# Returns the command's exit code, or 124 if timed out.
|
|
# Output is written to stdout.
|
|
run_with_timeout() {
|
|
local timeout_secs="${1:-60}"
|
|
shift
|
|
|
|
# Create temp file for output
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
|
|
# Run command in background, redirecting output to temp file
|
|
"$@" > "$tmpfile" 2>&1 &
|
|
local pid=$!
|
|
|
|
# Wait with timeout
|
|
local count=0
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
sleep 1
|
|
((count++))
|
|
if [[ $count -ge $timeout_secs ]]; then
|
|
echo "[TIMEOUT] Command timed out after ${timeout_secs}s: $*" >&2
|
|
# Kill the process and all its children
|
|
kill -9 "$pid" 2>/dev/null
|
|
pkill -9 -P "$pid" 2>/dev/null
|
|
wait "$pid" 2>/dev/null
|
|
cat "$tmpfile"
|
|
rm -f "$tmpfile"
|
|
return 124
|
|
fi
|
|
done
|
|
|
|
# Get exit code and output
|
|
wait "$pid"
|
|
local exit_code=$?
|
|
cat "$tmpfile"
|
|
rm -f "$tmpfile"
|
|
return $exit_code
|
|
}
|
|
|
|
# Kill any orphaned smolvm processes that might be holding the database lock.
|
|
# This includes:
|
|
# - smolvm serve (API server)
|
|
# - smolvm-bin machine start (VM processes from previous test runs)
|
|
# - Packed binaries running as daemons
|
|
#
|
|
# When SMOLVM_ORCHESTRATED=1 (set by run_tests.sh), this is a no-op: the
|
|
# orchestrator does a single pre-flight kill before launching any suites, so
|
|
# per-suite kills are skipped to avoid parallel suites killing each other's
|
|
# in-flight VM starts.
|
|
#
|
|
# Call this before running tests to ensure clean state.
|
|
kill_orphan_smolvm_processes() {
|
|
if [[ "${SMOLVM_ORCHESTRATED:-0}" == "1" ]]; then
|
|
return 0
|
|
fi
|
|
local killed=0
|
|
|
|
# Kill any smolvm serve processes
|
|
if pkill -f "smolvm serve" 2>/dev/null; then
|
|
((killed++)) || true
|
|
fi
|
|
if pkill -f "smolvm-bin serve" 2>/dev/null; then
|
|
((killed++)) || true
|
|
fi
|
|
|
|
# Kill any orphaned machine processes (from smolvm-bin in dist/)
|
|
if pkill -f "smolvm-bin machine start" 2>/dev/null; then
|
|
((killed++)) || true
|
|
fi
|
|
|
|
# Kill any orphaned machine processes (from target/release)
|
|
if pkill -f "smolvm machine start" 2>/dev/null; then
|
|
((killed++)) || true
|
|
fi
|
|
|
|
# Wait briefly for processes to die
|
|
if [[ $killed -gt 0 ]]; then
|
|
sleep 1
|
|
fi
|
|
}
|
|
|
|
# Check if any smolvm processes are running that might interfere with tests
|
|
check_smolvm_processes() {
|
|
local procs
|
|
procs=$(pgrep -f "(smolvm serve|smolvm-bin machine start|smolvm machine start)" 2>/dev/null || true)
|
|
if [[ -n "$procs" ]]; then
|
|
return 1 # Processes found
|
|
fi
|
|
return 0 # No interfering processes
|
|
}
|
|
|
|
# Ensure clean test environment - call at start of test suite
|
|
ensure_clean_test_environment() {
|
|
# First, try to kill any orphan processes
|
|
kill_orphan_smolvm_processes
|
|
|
|
# Verify they're gone
|
|
if ! check_smolvm_processes; then
|
|
log_info "Warning: Some smolvm processes are still running after cleanup"
|
|
log_info "Processes:"
|
|
ps aux | grep -E "(smolvm serve|smolvm-bin machine|smolvm machine)" | grep -v grep || true
|
|
fi
|
|
}
|