1778 lines
66 KiB
Bash
Executable File
1778 lines
66 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Pack tests for smolvm.
|
|
#
|
|
# Tests the `smolvm pack` command and packed binary execution.
|
|
# Requires VM environment and sufficient disk space (~500MB for images).
|
|
#
|
|
# Usage:
|
|
# ./tests/test_pack.sh [--quick]
|
|
#
|
|
# Options:
|
|
# --quick Skip slow tests (large image packing, daemon mode)
|
|
|
|
source "$(dirname "$0")/common.sh"
|
|
init_smolvm
|
|
|
|
# Pack tests must not inherit the repo-local libkrun search path from
|
|
# common.sh. Packed stubs are expected to start without host-provided libkrun.
|
|
if [[ "$(uname -s)" == "Linux" ]]; then
|
|
repo_lib_dir="$PROJECT_ROOT/lib/linux-$(uname -m)"
|
|
if [[ -n "${LD_LIBRARY_PATH:-}" ]]; then
|
|
filtered_ld_library_path=""
|
|
IFS=':' read -r -a ld_library_path_entries <<< "$LD_LIBRARY_PATH"
|
|
for entry in "${ld_library_path_entries[@]}"; do
|
|
[[ "$entry" == "$repo_lib_dir" ]] && continue
|
|
if [[ -z "$filtered_ld_library_path" ]]; then
|
|
filtered_ld_library_path="$entry"
|
|
else
|
|
filtered_ld_library_path="${filtered_ld_library_path}:$entry"
|
|
fi
|
|
done
|
|
|
|
if [[ -n "$filtered_ld_library_path" ]]; then
|
|
export LD_LIBRARY_PATH="$filtered_ld_library_path"
|
|
else
|
|
unset LD_LIBRARY_PATH
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Pre-flight: Kill any existing smolvm processes that might hold database lock
|
|
log_info "Pre-flight cleanup: killing orphan processes..."
|
|
kill_orphan_smolvm_processes
|
|
|
|
QUICK_MODE=false
|
|
if [[ "${1:-}" == "--quick" ]]; then
|
|
QUICK_MODE=true
|
|
fi
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " smolvm Pack Tests"
|
|
echo "=========================================="
|
|
echo ""
|
|
|
|
# Test output directory (cleaned up at end)
|
|
TEST_DIR=$(mktemp -d)
|
|
trap "rm -rf '$TEST_DIR'; $SMOLVM pack prune --all 2>/dev/null || true" EXIT
|
|
|
|
# =============================================================================
|
|
# Pack Command - Basic Tests
|
|
# =============================================================================
|
|
|
|
test_pack_help() {
|
|
# Verify pack command exists and shows help
|
|
$SMOLVM pack --help 2>&1 | grep -q "Package an OCI image"
|
|
}
|
|
|
|
test_pack_requires_output() {
|
|
# Pack should fail without -o flag
|
|
local exit_code=0
|
|
$SMOLVM pack create --image alpine:latest 2>&1 || exit_code=$?
|
|
[[ $exit_code -ne 0 ]]
|
|
}
|
|
|
|
test_pack_alpine() {
|
|
# Pack a minimal image
|
|
local output="$TEST_DIR/test-alpine"
|
|
local result
|
|
result=$($SMOLVM pack create --image alpine:latest -o "$output" 2>&1)
|
|
|
|
# Binary should exist
|
|
[[ -f "$output" ]]
|
|
|
|
# Sidecar should exist
|
|
[[ -f "$output.smolmachine" ]]
|
|
|
|
# Binary should be executable
|
|
[[ -x "$output" ]]
|
|
}
|
|
|
|
test_pack_with_custom_resources() {
|
|
# Pack with custom CPU/memory defaults
|
|
local output="$TEST_DIR/test-resources"
|
|
$SMOLVM pack create --image alpine:latest -o "$output" --cpus 2 --mem 512 2>&1
|
|
|
|
# Verify manifest has custom values
|
|
local info
|
|
info=$("$output" info 2>&1)
|
|
[[ "$info" == *"CPUs:"*"2"* ]] && [[ "$info" == *"Memory:"*"512"* ]]
|
|
}
|
|
|
|
test_pack_with_platform() {
|
|
# Pack with explicit platform
|
|
local output="$TEST_DIR/test-platform"
|
|
|
|
# Determine host platform for the test
|
|
local host_arch
|
|
if [[ "$(uname -m)" == "arm64" ]] || [[ "$(uname -m)" == "aarch64" ]]; then
|
|
host_arch="linux/arm64"
|
|
else
|
|
host_arch="linux/amd64"
|
|
fi
|
|
|
|
$SMOLVM pack create --image alpine:latest -o "$output" --oci-platform "$host_arch" 2>&1
|
|
|
|
# Binary should exist
|
|
[[ -f "$output" ]]
|
|
|
|
# Verify manifest shows correct platform
|
|
local info
|
|
info=$("$output" info 2>&1)
|
|
[[ "$info" == *"Platform:"* ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Packed Binary - Info
|
|
# =============================================================================
|
|
|
|
test_packed_info() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Test info subcommand
|
|
local info_output
|
|
info_output=$("$output" info 2>&1)
|
|
[[ "$info_output" == *"Image:"* ]] && \
|
|
[[ "$info_output" == *"Platform:"* ]] && \
|
|
[[ "$info_output" == *"Checksum:"* ]] || return 1
|
|
}
|
|
|
|
test_packed_version() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# --version should succeed and print the smolvm version
|
|
local result
|
|
local exit_code=0
|
|
result=$("$output" --version 2>&1) || exit_code=$?
|
|
[[ $exit_code -eq 0 ]] || return 1
|
|
# Should contain a version-like string (e.g., "packed-binary 0.2.0")
|
|
[[ "$result" =~ [0-9]+\.[0-9]+ ]]
|
|
}
|
|
|
|
test_packed_help() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
local result
|
|
result=$("$output" --help 2>&1) || true
|
|
[[ "$result" == *"run"* ]] || [[ "$result" == *"start"* ]]
|
|
}
|
|
|
|
test_sidecar_has_magic() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
echo "SKIP: No sidecar"
|
|
return 0
|
|
fi
|
|
|
|
# Check last 64 bytes (footer) for SMOLPACK magic
|
|
local magic
|
|
magic=$(tail -c 64 "$output.smolmachine" | head -c 8 2>/dev/null) || true
|
|
[[ "$magic" == "SMOLPACK" ]]
|
|
}
|
|
|
|
test_binary_is_clean_macho() {
|
|
if [[ "$(uname)" != "Darwin" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
echo "SKIP: No packed binary"
|
|
return 0
|
|
fi
|
|
|
|
local file_result
|
|
file_result=$(file "$output" 2>&1) || true
|
|
[[ "$file_result" == *"Mach-O"* ]]
|
|
}
|
|
|
|
test_sidecar_has_no_libs() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
echo "SKIP: No sidecar"
|
|
return 0
|
|
fi
|
|
|
|
# Extract sidecar and verify no lib/ directory (V3: libs are in stub)
|
|
local tmpdir
|
|
tmpdir=$(mktemp -d)
|
|
# Sidecar is: [assets.tar.zst][manifest json][64-byte footer]
|
|
# Read footer to get assets_size, then extract just the tar.zst
|
|
# Simpler: just list the tar contents and check for lib/
|
|
local footer_size=64
|
|
local file_size
|
|
file_size=$(stat -f%z "$output.smolmachine" 2>/dev/null || stat -c%s "$output.smolmachine" 2>/dev/null)
|
|
|
|
# The assets tar.zst starts at offset 0 in the sidecar
|
|
# Check that decompressed tar has no lib/ entries
|
|
if command -v zstd >/dev/null 2>&1; then
|
|
# Use head to get just the compressed portion (before manifest+footer)
|
|
# This is approximate but sufficient — if lib/ is in the tar, tar -t will find it
|
|
zstd -d < "$output.smolmachine" 2>/dev/null | tar -t 2>/dev/null | grep -q "^lib/" && return 1
|
|
else
|
|
echo "SKIP: zstd not installed"
|
|
fi
|
|
|
|
rm -rf "$tmpdir"
|
|
return 0
|
|
}
|
|
|
|
test_stub_has_libs_footer() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
echo "SKIP: No packed binary"
|
|
return 0
|
|
fi
|
|
|
|
# Check last 32 bytes for SMOLLIBS magic
|
|
local magic
|
|
magic=$(tail -c 32 "$output" | head -c 8 2>/dev/null) || true
|
|
[[ "$magic" == "SMOLLIBS" ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Packed Binary - Library Compatibility
|
|
# =============================================================================
|
|
|
|
test_pack_bundled_libkrun_has_required_symbols() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Boot the VM briefly to trigger lib extraction, capture debug output
|
|
local debug_output
|
|
debug_output=$(run_with_timeout 60 "$output" --debug run -- true 2>&1) || true
|
|
|
|
# Extract the lib_dir from debug output (e.g., "lib_dir=/path/to/lib")
|
|
local lib_dir
|
|
lib_dir=$(echo "$debug_output" | grep -o 'lib_dir=[^ ]*' | head -1 | cut -d= -f2)
|
|
|
|
if [[ -z "$lib_dir" ]]; then
|
|
echo "FAIL: could not determine lib_dir from --debug output"
|
|
return 1
|
|
fi
|
|
|
|
local libkrun="$lib_dir/libkrun.dylib"
|
|
if [[ "$(uname)" != "Darwin" ]]; then
|
|
libkrun="$lib_dir/libkrun.so"
|
|
fi
|
|
|
|
if [[ ! -f "$libkrun" ]]; then
|
|
echo "FAIL: bundled libkrun not found at $libkrun"
|
|
return 1
|
|
fi
|
|
|
|
# Verify all required symbols exist in the bundled library.
|
|
# This catches the bug where an older system libkrun gets bundled
|
|
# instead of the one smolvm was built against.
|
|
#
|
|
# Symbol inspection is platform-specific here:
|
|
# - macOS uses Mach-O, where `nm -gU` lists external symbols and C exports
|
|
# appear with a leading underscore (for example `_krun_create_ctx`)
|
|
# - Linux uses ELF, where `nm -D --defined-only` lists dynamic exports and
|
|
# the same symbol appears without the underscore (`krun_create_ctx`)
|
|
local symbols nm_prefix
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
symbols=$(nm -gU "$libkrun" 2>/dev/null) || {
|
|
echo "FAIL: nm failed on $libkrun"
|
|
return 1
|
|
}
|
|
nm_prefix="_"
|
|
else
|
|
symbols=$(nm -D --defined-only "$libkrun" 2>/dev/null) || {
|
|
echo "FAIL: nm failed on $libkrun"
|
|
return 1
|
|
}
|
|
nm_prefix=""
|
|
fi
|
|
|
|
local missing=0
|
|
for sym in krun_create_ctx krun_add_disk2 krun_add_vsock_port2 krun_start_enter; do
|
|
if ! echo "$symbols" | grep -q "${nm_prefix}${sym}$"; then
|
|
echo "FAIL: bundled libkrun missing required symbol: $sym"
|
|
missing=1
|
|
fi
|
|
done
|
|
|
|
[[ $missing -eq 0 ]]
|
|
}
|
|
|
|
test_pack_uses_loaded_libkrun() {
|
|
# Verify the packer prefers dladdr over directory search
|
|
local output="$TEST_DIR/test-dladdr"
|
|
local pack_output
|
|
pack_output=$(RUST_LOG=debug $SMOLVM pack create --image alpine:latest -o "$output" 2>&1)
|
|
|
|
# Debug log should show "using libkrun from running process"
|
|
if echo "$pack_output" | grep -q "using libkrun from running process"; then
|
|
return 0
|
|
else
|
|
echo "WARN: dladdr path not used — falling back to directory search"
|
|
# Not a hard failure: dladdr may not work in all link configurations.
|
|
# The symbol check test above is the real safety net.
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# Packed Binary - Ephemeral Execution (Requires VM)
|
|
# =============================================================================
|
|
|
|
test_packed_run_echo() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary with sidecar
|
|
if [[ ! -f "$output" ]] || [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Run with 60s timeout to prevent indefinite hangs
|
|
local result
|
|
result=$(run_with_timeout 60 "$output" run -- echo "pack-test-marker-12345" 2>&1)
|
|
local exit_code=$?
|
|
|
|
if [[ $exit_code -eq 124 ]]; then
|
|
echo "TIMEOUT: packed binary hung"
|
|
return 1
|
|
fi
|
|
|
|
[[ "$result" == *"pack-test-marker-12345"* ]]
|
|
}
|
|
|
|
test_packed_exit_code() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Exit code 0 (with timeout)
|
|
run_with_timeout 60 "$output" run -- sh -c "exit 0" 2>&1
|
|
local exit_zero=$?
|
|
[[ $exit_zero -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
|
|
# Exit code 42 (with timeout)
|
|
local exit_42=0
|
|
run_with_timeout 60 "$output" run -- sh -c "exit 42" 2>&1 || exit_42=$?
|
|
[[ $exit_42 -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
|
|
[[ $exit_zero -eq 0 ]] && [[ $exit_42 -eq 42 ]]
|
|
}
|
|
|
|
test_packed_env_var() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
local result
|
|
result=$(run_with_timeout 60 "$output" run -e TEST_VAR=hello_pack -- sh -c 'echo $TEST_VAR' 2>&1)
|
|
[[ $? -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"hello_pack"* ]]
|
|
}
|
|
|
|
test_packed_workdir() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
local result
|
|
result=$(run_with_timeout 60 "$output" run -w /tmp -- pwd 2>&1)
|
|
[[ $? -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"/tmp"* ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Sidecar File Tests
|
|
# =============================================================================
|
|
|
|
test_sidecar_required() {
|
|
local output="$TEST_DIR/test-sidecar"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Remove sidecar
|
|
rm -f "$output.smolmachine"
|
|
|
|
# Binary should fail without sidecar
|
|
local exit_code=0
|
|
"$output" info 2>&1 || exit_code=$?
|
|
|
|
# Restore sidecar for other tests
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1 >/dev/null
|
|
|
|
[[ $exit_code -ne 0 ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Single-File Mode Tests (--single-file)
|
|
# =============================================================================
|
|
|
|
test_single_file_pack() {
|
|
# Pack with --single-file flag
|
|
local output="$TEST_DIR/test-single-file"
|
|
$SMOLVM pack create --image alpine:latest -o "$output" --single-file 2>&1
|
|
|
|
# Binary should exist and be executable
|
|
[[ -f "$output" ]] || return 1
|
|
[[ -x "$output" ]] || return 1
|
|
|
|
# Sidecar should NOT exist
|
|
[[ ! -f "$output.smolmachine" ]] || return 1
|
|
|
|
# Should work when moved (no sidecar needed)
|
|
local new_dir="$TEST_DIR/standalone-test"
|
|
mkdir -p "$new_dir"
|
|
cp "$output" "$new_dir/myapp"
|
|
local info_output
|
|
info_output=$("$new_dir/myapp" info 2>&1)
|
|
[[ "$info_output" == *"Image:"* ]]
|
|
}
|
|
|
|
test_single_file_run_echo() {
|
|
local output="$TEST_DIR/test-single-file"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" --single-file 2>&1
|
|
fi
|
|
|
|
# Run with 60s timeout to prevent indefinite hangs
|
|
local result
|
|
result=$(run_with_timeout 60 "$output" run -- echo "single-file-test-marker" 2>&1)
|
|
local exit_code=$?
|
|
|
|
if [[ $exit_code -eq 124 ]]; then
|
|
echo "TIMEOUT: packed binary hung"
|
|
return 1
|
|
fi
|
|
|
|
[[ "$result" == *"single-file-test-marker"* ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# pack run subcommand - Basic Tests
|
|
# =============================================================================
|
|
|
|
test_pack_run_help() {
|
|
# Verify pack run subcommand exists and shows help
|
|
$SMOLVM pack run --help 2>&1 | grep -q "Run a VM from a packed"
|
|
}
|
|
|
|
test_pack_run_info() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary with sidecar
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Test --info via pack run
|
|
local info_output
|
|
info_output=$($SMOLVM pack run --sidecar "$output.smolmachine" --info 2>&1)
|
|
[[ "$info_output" == *"Image:"* ]] && \
|
|
[[ "$info_output" == *"Platform:"* ]] && \
|
|
[[ "$info_output" == *"Checksum:"* ]] || return 1
|
|
}
|
|
|
|
test_pack_run_info_no_sidecar() {
|
|
# Should error clearly when sidecar doesn't exist
|
|
local exit_code=0
|
|
$SMOLVM pack run --sidecar /tmp/nonexistent-file.smolmachine --info 2>&1 || exit_code=$?
|
|
[[ $exit_code -ne 0 ]]
|
|
}
|
|
|
|
test_pack_run_auto_detect() {
|
|
# Test auto-detection of .smolmachine file in current directory
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Create a temp dir with a single .smolmachine file
|
|
local detect_dir="$TEST_DIR/auto-detect"
|
|
mkdir -p "$detect_dir"
|
|
cp "$output.smolmachine" "$detect_dir/myapp.smolmachine"
|
|
|
|
# pack run --info from that directory should auto-detect
|
|
local info_output
|
|
info_output=$(cd "$detect_dir" && $SMOLVM pack run --info 2>&1)
|
|
[[ "$info_output" == *"Image:"* ]]
|
|
}
|
|
|
|
test_pack_run_auto_detect_ambiguous() {
|
|
# Should error when multiple .smolmachine files exist and no --sidecar given
|
|
local detect_dir="$TEST_DIR/multi-detect"
|
|
mkdir -p "$detect_dir"
|
|
|
|
# Create two dummy .smolmachine files (just need them to exist for detection)
|
|
touch "$detect_dir/app1.smolmachine"
|
|
touch "$detect_dir/app2.smolmachine"
|
|
|
|
local exit_code=0
|
|
(cd "$detect_dir" && $SMOLVM pack run --info 2>&1) || exit_code=$?
|
|
[[ $exit_code -ne 0 ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# pack run subcommand - Execution Tests (Requires VM)
|
|
# =============================================================================
|
|
|
|
test_pack_run_resource_override() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Verify resource override flags are accepted (boot with custom resources)
|
|
# We use --debug to see the config, and run a quick command
|
|
local result
|
|
result=$(run_with_timeout 60 $SMOLVM pack run --sidecar "$output.smolmachine" --cpus 2 --mem 512 --debug -- echo "resource-test" 2>&1)
|
|
local exit_code=$?
|
|
|
|
[[ $exit_code -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
|
|
# Should contain the debug output showing the resource overrides
|
|
[[ "$result" == *"cpus=2"* ]] && [[ "$result" == *"mem=512"* ]] && \
|
|
[[ "$result" == *"resource-test"* ]]
|
|
}
|
|
|
|
test_pack_run_force_extract() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Run with --force-extract and --debug to verify re-extraction
|
|
local result
|
|
result=$(run_with_timeout 60 $SMOLVM pack run --sidecar "$output.smolmachine" --force-extract --debug -- echo "re-extracted" 2>&1)
|
|
local exit_code=$?
|
|
|
|
[[ $exit_code -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
|
|
# Debug output should show extraction happening
|
|
[[ "$result" == *"extract"* ]] && [[ "$result" == *"re-extracted"* ]]
|
|
}
|
|
|
|
test_pack_run_cached_fast() {
|
|
# Second run should use cached assets (no extraction)
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# First run ensures cache exists
|
|
run_with_timeout 60 $SMOLVM pack run --sidecar "$output.smolmachine" -- true 2>&1 || true
|
|
|
|
# Second run with --debug should show "using cached assets"
|
|
local result
|
|
result=$(run_with_timeout 60 $SMOLVM pack run --sidecar "$output.smolmachine" --debug -- echo "cached-run" 2>&1)
|
|
local exit_code=$?
|
|
|
|
[[ $exit_code -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"cached"* ]] && [[ "$result" == *"cached-run"* ]]
|
|
}
|
|
|
|
test_pack_run_python() {
|
|
if [[ "$QUICK_MODE" == "true" ]]; then
|
|
echo "SKIP: --quick mode"
|
|
return 0
|
|
fi
|
|
|
|
local output="$TEST_DIR/test-python"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image python:3.12-slim -o "$output" 2>&1
|
|
fi
|
|
|
|
local result
|
|
result=$(run_with_timeout 90 $SMOLVM pack run --sidecar "$output.smolmachine" -- python -c "print('Hello from pack run Python')" 2>&1)
|
|
[[ $? -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"Hello from pack run Python"* ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Pack --from-vm Tests (Requires VM + Network)
|
|
# =============================================================================
|
|
|
|
# Shared VM name for --from-vm tests (cleaned up in test_from_vm_cleanup)
|
|
FROM_VM_NAME="pack-from-vm-test-$$"
|
|
FROM_VM_OUTPUT="$TEST_DIR/test-from-vm"
|
|
|
|
test_from_vm_setup() {
|
|
# Create a named VM with network, install a package, then stop it
|
|
$SMOLVM machine stop --name "$FROM_VM_NAME" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$FROM_VM_NAME" -f 2>/dev/null || true
|
|
|
|
$SMOLVM machine create --name "$FROM_VM_NAME" --net 2>&1 || return 1
|
|
$SMOLVM machine start --name "$FROM_VM_NAME" 2>&1 || {
|
|
$SMOLVM machine delete --name "$FROM_VM_NAME" -f 2>/dev/null
|
|
return 1
|
|
}
|
|
|
|
# Install curl so we can verify it persists into the packed binary.
|
|
# apk add may exit non-zero due to busybox trigger errors (busybox was
|
|
# baked into the rootfs, not installed from a repo, so re-extraction
|
|
# fails). The package itself installs fine — verify with `which curl`.
|
|
$SMOLVM machine exec --name "$FROM_VM_NAME" -- apk add --no-cache curl 2>&1 || true
|
|
|
|
# Verify curl was installed
|
|
local which_output
|
|
which_output=$($SMOLVM machine exec --name "$FROM_VM_NAME" -- which curl 2>&1) || {
|
|
$SMOLVM machine stop --name "$FROM_VM_NAME" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$FROM_VM_NAME" -f 2>/dev/null || true
|
|
return 1
|
|
}
|
|
[[ "$which_output" == *"/usr/bin/curl"* ]] || {
|
|
$SMOLVM machine stop --name "$FROM_VM_NAME" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$FROM_VM_NAME" -f 2>/dev/null || true
|
|
return 1
|
|
}
|
|
|
|
# Stop the VM (pack requires it to be stopped)
|
|
$SMOLVM machine stop --name "$FROM_VM_NAME" 2>&1
|
|
}
|
|
|
|
test_from_vm_rejects_running() {
|
|
# --from-vm should fail if the VM is still running
|
|
local vm_name="pack-running-test-$$"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
|
|
$SMOLVM machine create --name "$vm_name" 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
return 1
|
|
}
|
|
|
|
local exit_code=0
|
|
$SMOLVM pack create --from-vm "$vm_name" -o "$TEST_DIR/should-fail" 2>&1 || exit_code=$?
|
|
|
|
# Clean up
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
|
|
[[ $exit_code -ne 0 ]]
|
|
}
|
|
|
|
test_from_vm_pack() {
|
|
# Pack the stopped VM snapshot
|
|
$SMOLVM pack create --from-vm "$FROM_VM_NAME" -o "$FROM_VM_OUTPUT" 2>&1 || return 1
|
|
|
|
# Binary and sidecar should exist
|
|
[[ -f "$FROM_VM_OUTPUT" ]] || return 1
|
|
[[ -f "$FROM_VM_OUTPUT.smolmachine" ]] || return 1
|
|
[[ -x "$FROM_VM_OUTPUT" ]] || return 1
|
|
}
|
|
|
|
test_from_vm_run_finds_installed_package() {
|
|
if [[ ! -f "$FROM_VM_OUTPUT" ]]; then
|
|
echo "SKIP: no packed binary (setup or pack failed)"
|
|
return 1
|
|
fi
|
|
|
|
# The packed binary should have curl from the VM snapshot
|
|
local result
|
|
result=$(run_with_timeout 60 "$FROM_VM_OUTPUT" run -- which curl 2>&1)
|
|
local exit_code=$?
|
|
|
|
[[ $exit_code -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"/usr/bin/curl"* ]]
|
|
}
|
|
|
|
test_from_vm_cleanup() {
|
|
$SMOLVM machine stop --name "$FROM_VM_NAME" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$FROM_VM_NAME" -f 2>/dev/null || true
|
|
rm -f "$FROM_VM_OUTPUT" "$FROM_VM_OUTPUT.smolmachine"
|
|
return 0
|
|
}
|
|
|
|
# End-to-end test: --from-vm on an IMAGE-BASED VM captures container overlay.
|
|
#
|
|
# BUG-17 regression: `pack create --from-vm` for image-based VMs only captured
|
|
# the base OCI layers, missing packages installed via apk/apt. The container
|
|
# overlay (upper dir on the storage disk) wasn't exported. This test verifies
|
|
# the full round-trip:
|
|
# 1. Create image-based VM → install package → stop
|
|
# 2. Pack with --from-vm (must export overlay as additional layer)
|
|
# 3. Run packed binary — installed package must be present
|
|
# 4. Create machine from .smolmachine — installed package must be present
|
|
# 5. Stop/start machine — package persists across restart
|
|
test_from_vm_image_overlay() {
|
|
local vm_name="from-vm-img-$$"
|
|
local pack_output="$TEST_DIR/from-vm-img-pack"
|
|
local machine_name="from-vm-img-machine-$$"
|
|
|
|
# Cleanup any leftovers
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
$SMOLVM machine stop --name "$machine_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$machine_name" -f 2>/dev/null || true
|
|
rm -f "$pack_output" "$pack_output.smolmachine"
|
|
|
|
# 1. Create image-based VM, install curl, verify, stop
|
|
echo " Step 1: Create image-based VM and install curl..."
|
|
$SMOLVM machine create --name "$vm_name" --image alpine:latest --net 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
$SMOLVM machine exec --name "$vm_name" -- apk add --no-cache curl 2>&1 || true
|
|
local which_result
|
|
which_result=$($SMOLVM machine exec --name "$vm_name" -- which curl 2>&1)
|
|
[[ "$which_result" == *"/usr/bin/curl"* ]] || {
|
|
echo "FAIL: curl not installed in source VM"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
$SMOLVM machine stop --name "$vm_name" 2>&1 || return 1
|
|
|
|
# 2. Pack with --from-vm (this must export the container overlay)
|
|
echo " Step 2: Pack image-based VM with --from-vm..."
|
|
$SMOLVM pack create --from-vm "$vm_name" -o "$pack_output" 2>&1 || {
|
|
echo "FAIL: pack --from-vm failed"
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
[[ -f "$pack_output.smolmachine" ]] || {
|
|
echo "FAIL: no .smolmachine sidecar produced"
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# 3. Run packed binary — curl must be present
|
|
echo " Step 3: Verify packed binary has curl..."
|
|
local run_result
|
|
run_result=$(run_with_timeout 60 $SMOLVM pack run --sidecar "$pack_output.smolmachine" -- which curl 2>&1)
|
|
[[ "$run_result" == *"/usr/bin/curl"* ]] || {
|
|
echo "FAIL: curl not found in packed binary (got: $run_result)"
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
rm -f "$pack_output" "$pack_output.smolmachine"; return 1
|
|
}
|
|
|
|
# 4. Create machine from .smolmachine — curl must be present
|
|
echo " Step 4: Create machine from .smolmachine and verify curl..."
|
|
$SMOLVM machine create --name "$machine_name" --from "$pack_output.smolmachine" --net 2>&1 || {
|
|
echo "FAIL: machine create --from failed"
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
rm -f "$pack_output" "$pack_output.smolmachine"; return 1
|
|
}
|
|
$SMOLVM machine start --name "$machine_name" 2>&1 || {
|
|
echo "FAIL: machine start failed"
|
|
$SMOLVM machine delete --name "$machine_name" -f 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
rm -f "$pack_output" "$pack_output.smolmachine"; return 1
|
|
}
|
|
local exec_result
|
|
exec_result=$($SMOLVM machine exec --name "$machine_name" -- which curl 2>&1)
|
|
[[ "$exec_result" == *"/usr/bin/curl"* ]] || {
|
|
echo "FAIL: curl not found in machine from .smolmachine (got: $exec_result)"
|
|
$SMOLVM machine stop --name "$machine_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$machine_name" -f 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
rm -f "$pack_output" "$pack_output.smolmachine"; return 1
|
|
}
|
|
|
|
# 5. Stop/start — curl persists
|
|
echo " Step 5: Verify persistence across restart..."
|
|
$SMOLVM machine stop --name "$machine_name" 2>&1 || true
|
|
$SMOLVM machine start --name "$machine_name" 2>&1 || {
|
|
echo "FAIL: restart failed"
|
|
$SMOLVM machine delete --name "$machine_name" -f 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
rm -f "$pack_output" "$pack_output.smolmachine"; return 1
|
|
}
|
|
exec_result=$($SMOLVM machine exec --name "$machine_name" -- which curl 2>&1)
|
|
[[ "$exec_result" == *"/usr/bin/curl"* ]] || {
|
|
echo "FAIL: curl not found after restart (got: $exec_result)"
|
|
$SMOLVM machine stop --name "$machine_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$machine_name" -f 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
|
|
rm -f "$pack_output" "$pack_output.smolmachine"; return 1
|
|
}
|
|
|
|
# Cleanup
|
|
$SMOLVM machine stop --name "$machine_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$machine_name" -f 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
rm -f "$pack_output" "$pack_output.smolmachine"
|
|
}
|
|
|
|
# Regression: default --from-vm repack preserves imported artifact layers.
|
|
#
|
|
# A machine created from a .smolmachine has two kinds of container state:
|
|
# imported layers from the source artifact, and the current writable overlay.
|
|
# Repacking it without --rebase-from-image must keep both.
|
|
test_from_vm_imported_layers_preserved_on_repack() {
|
|
local vm_a="from-vm-nested-a-$$"
|
|
local vm_b="from-vm-nested-b-$$"
|
|
local vm_c="from-vm-nested-c-$$"
|
|
local pack_a="$TEST_DIR/from-vm-nested-a"
|
|
local pack_b="$TEST_DIR/from-vm-nested-b"
|
|
|
|
cleanup_from_vm_imported_layers_preserved() {
|
|
$SMOLVM machine stop --name "$vm_c" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_c" -f 2>/dev/null || true
|
|
$SMOLVM machine stop --name "$vm_b" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_b" -f 2>/dev/null || true
|
|
$SMOLVM machine stop --name "$vm_a" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_a" -f 2>/dev/null || true
|
|
rm -f "$pack_a" "$pack_a.smolmachine" "$pack_b" "$pack_b.smolmachine"
|
|
}
|
|
|
|
# Cleanup any leftovers
|
|
cleanup_from_vm_imported_layers_preserved
|
|
|
|
echo " Step 1: Create source image VM with artifact marker..."
|
|
$SMOLVM machine create --name "$vm_a" --image alpine:latest --net 2>&1 || {
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
$SMOLVM machine start --name "$vm_a" 2>&1 || {
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
$SMOLVM machine exec --name "$vm_a" -- sh -c "echo inherited > /artifact-origin.txt" 2>&1 || {
|
|
echo "FAIL: could not write source artifact marker"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
$SMOLVM machine stop --name "$vm_a" 2>&1 || {
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
|
|
echo " Step 2: Pack source VM..."
|
|
$SMOLVM pack create --from-vm "$vm_a" -o "$pack_a" 2>&1 || {
|
|
echo "FAIL: pack source VM failed"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
[[ -f "$pack_a.smolmachine" ]] || {
|
|
echo "FAIL: no source .smolmachine sidecar produced"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
|
|
echo " Step 3: Create nested VM and verify inherited marker..."
|
|
$SMOLVM machine create --name "$vm_b" --from "$pack_a.smolmachine" --net 2>&1 || {
|
|
echo "FAIL: nested machine create --from failed"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
$SMOLVM machine start --name "$vm_b" 2>&1 || {
|
|
echo "FAIL: nested machine start failed"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
local inherited_result
|
|
inherited_result=$($SMOLVM machine exec --name "$vm_b" -- cat /artifact-origin.txt 2>&1)
|
|
[[ "$inherited_result" == *"inherited"* ]] || {
|
|
echo "FAIL: inherited marker missing in nested VM (got: $inherited_result)"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
$SMOLVM machine exec --name "$vm_b" -- sh -c "echo nested > /nested-mutation.txt" 2>&1 || {
|
|
echo "FAIL: could not write nested mutation marker"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
$SMOLVM machine stop --name "$vm_b" 2>&1 || {
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
|
|
echo " Step 4: Repack nested VM without rebase..."
|
|
$SMOLVM pack create --from-vm "$vm_b" -o "$pack_b" 2>&1 || {
|
|
echo "FAIL: repack nested VM failed"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
[[ -f "$pack_b.smolmachine" ]] || {
|
|
echo "FAIL: no nested .smolmachine sidecar produced"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
|
|
echo " Step 5: Create final VM and verify both markers..."
|
|
$SMOLVM machine create --name "$vm_c" --from "$pack_b.smolmachine" --net 2>&1 || {
|
|
echo "FAIL: final machine create --from failed"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
$SMOLVM machine start --name "$vm_c" 2>&1 || {
|
|
echo "FAIL: final machine start failed"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
inherited_result=$($SMOLVM machine exec --name "$vm_c" -- cat /artifact-origin.txt 2>&1)
|
|
[[ "$inherited_result" == *"inherited"* ]] || {
|
|
echo "FAIL: inherited marker missing after repack (got: $inherited_result)"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
local nested_result
|
|
nested_result=$($SMOLVM machine exec --name "$vm_c" -- cat /nested-mutation.txt 2>&1)
|
|
[[ "$nested_result" == *"nested"* ]] || {
|
|
echo "FAIL: nested marker missing after repack (got: $nested_result)"
|
|
cleanup_from_vm_imported_layers_preserved; return 1
|
|
}
|
|
|
|
cleanup_from_vm_imported_layers_preserved
|
|
}
|
|
|
|
# Regression: --rebase-from-image intentionally rebuilds lower image layers and
|
|
# drops imported artifact layers, while keeping the current writable overlay.
|
|
test_from_vm_rebase_from_image_drops_imported_layers() {
|
|
local vm_a="from-vm-rebase-a-$$"
|
|
local vm_b="from-vm-rebase-b-$$"
|
|
local vm_c="from-vm-rebase-c-$$"
|
|
local pack_a="$TEST_DIR/from-vm-rebase-a"
|
|
local pack_b="$TEST_DIR/from-vm-rebase-b"
|
|
|
|
cleanup_from_vm_rebase_layers() {
|
|
$SMOLVM machine stop --name "$vm_c" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_c" -f 2>/dev/null || true
|
|
$SMOLVM machine stop --name "$vm_b" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_b" -f 2>/dev/null || true
|
|
$SMOLVM machine stop --name "$vm_a" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_a" -f 2>/dev/null || true
|
|
rm -f "$pack_a" "$pack_a.smolmachine" "$pack_b" "$pack_b.smolmachine"
|
|
}
|
|
|
|
# Cleanup any leftovers
|
|
cleanup_from_vm_rebase_layers
|
|
|
|
echo " Step 1: Create source image VM with artifact marker..."
|
|
$SMOLVM machine create --name "$vm_a" --image alpine:latest --net 2>&1 || {
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
$SMOLVM machine start --name "$vm_a" 2>&1 || {
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
$SMOLVM machine exec --name "$vm_a" -- sh -c "echo inherited > /artifact-origin.txt" 2>&1 || {
|
|
echo "FAIL: could not write source artifact marker"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
$SMOLVM machine stop --name "$vm_a" 2>&1 || {
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
|
|
echo " Step 2: Pack source VM..."
|
|
$SMOLVM pack create --from-vm "$vm_a" -o "$pack_a" 2>&1 || {
|
|
echo "FAIL: pack source VM failed"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
[[ -f "$pack_a.smolmachine" ]] || {
|
|
echo "FAIL: no source .smolmachine sidecar produced"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
|
|
echo " Step 3: Create nested VM and write mutation marker..."
|
|
$SMOLVM machine create --name "$vm_b" --from "$pack_a.smolmachine" --net 2>&1 || {
|
|
echo "FAIL: nested machine create --from failed"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
$SMOLVM machine start --name "$vm_b" 2>&1 || {
|
|
echo "FAIL: nested machine start failed"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
local inherited_result
|
|
inherited_result=$($SMOLVM machine exec --name "$vm_b" -- cat /artifact-origin.txt 2>&1)
|
|
[[ "$inherited_result" == *"inherited"* ]] || {
|
|
echo "FAIL: inherited marker missing in nested VM (got: $inherited_result)"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
$SMOLVM machine exec --name "$vm_b" -- sh -c "echo nested > /nested-mutation.txt" 2>&1 || {
|
|
echo "FAIL: could not write nested mutation marker"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
$SMOLVM machine stop --name "$vm_b" 2>&1 || {
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
|
|
echo " Step 4: Repack nested VM with --rebase-from-image..."
|
|
$SMOLVM pack create --from-vm "$vm_b" --rebase-from-image -o "$pack_b" 2>&1 || {
|
|
echo "FAIL: rebase repack nested VM failed"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
[[ -f "$pack_b.smolmachine" ]] || {
|
|
echo "FAIL: no rebased .smolmachine sidecar produced"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
|
|
echo " Step 5: Create final VM and verify rebase semantics..."
|
|
$SMOLVM machine create --name "$vm_c" --from "$pack_b.smolmachine" --net 2>&1 || {
|
|
echo "FAIL: final machine create --from failed"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
$SMOLVM machine start --name "$vm_c" 2>&1 || {
|
|
echo "FAIL: final machine start failed"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
local presence_result
|
|
presence_result=$($SMOLVM machine exec --name "$vm_c" -- sh -c 'if [ -e /artifact-origin.txt ]; then echo PRESENT; else echo ABSENT; fi' 2>&1)
|
|
[[ "$presence_result" == *"ABSENT"* ]] || {
|
|
echo "FAIL: inherited marker present after --rebase-from-image (got: $presence_result)"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
local nested_result
|
|
nested_result=$($SMOLVM machine exec --name "$vm_c" -- cat /nested-mutation.txt 2>&1)
|
|
[[ "$nested_result" == *"nested"* ]] || {
|
|
echo "FAIL: nested marker missing after --rebase-from-image (got: $nested_result)"
|
|
cleanup_from_vm_rebase_layers; return 1
|
|
}
|
|
|
|
cleanup_from_vm_rebase_layers
|
|
}
|
|
|
|
# =============================================================================
|
|
# Debian Pack Roundtrip (Char device entry regression)
|
|
#
|
|
# Debian's update-alternatives creates Char device entries in /etc/alternatives/
|
|
# in the overlayfs upper layer. These entries caused safe_unpack() to abort
|
|
# mid-stream, silently dropping files that appeared after the Char entry in the
|
|
# tar. This test verifies the full round-trip:
|
|
# 1. Create image-based VM with Debian image
|
|
# 2. Install a package that triggers update-alternatives (creates Char entries)
|
|
# 3. Write a test file
|
|
# 4. Pack the VM
|
|
# 5. Create a new machine from the pack
|
|
# 6. Verify the test file survived (it appears after Char entries in the tar)
|
|
# =============================================================================
|
|
|
|
test_from_vm_debian_roundtrip() {
|
|
local vm_name="debian-pack-$$"
|
|
local from_name="debian-from-$$"
|
|
local pack_output="$TEST_DIR/test-debian-roundtrip"
|
|
|
|
# Cleanup any leftovers
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
$SMOLVM machine stop --name "$from_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$from_name" -f 2>/dev/null || true
|
|
|
|
# 1. Create a Debian-based VM
|
|
echo " Step 1: Creating Debian VM..."
|
|
$SMOLVM machine create --name "$vm_name" --image python:3.12-slim --net 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# 2. Install a package that creates Char entries via update-alternatives
|
|
echo " Step 2: Installing man-db (creates Char entries in /etc/alternatives/)..."
|
|
$SMOLVM machine exec --name "$vm_name" -- \
|
|
bash -c "apt-get update -qq && apt-get install -y -qq man-db >/dev/null 2>&1" || {
|
|
echo "FAIL: apt-get install failed"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# 3. Write a test file (appears late in tar stream — after Char entries)
|
|
echo " Step 3: Writing test marker..."
|
|
$SMOLVM machine exec --name "$vm_name" -- \
|
|
bash -c "echo 'debian-roundtrip-ok' > /test-marker.txt"
|
|
|
|
local content
|
|
content=$($SMOLVM machine exec --name "$vm_name" -- cat /test-marker.txt 2>&1)
|
|
[[ "$content" == *"debian-roundtrip-ok"* ]] || {
|
|
echo "FAIL: test marker not written (got: '$content')"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# 4. Stop and pack
|
|
echo " Step 4: Packing VM..."
|
|
$SMOLVM machine stop --name "$vm_name" 2>&1
|
|
$SMOLVM pack create --from-vm "$vm_name" -o "$pack_output" 2>&1 || {
|
|
echo "FAIL: pack create --from-vm failed"
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# 5. Create a new machine from the pack
|
|
echo " Step 5: Creating machine from pack..."
|
|
$SMOLVM machine create --name "$from_name" --from "$pack_output.smolmachine" 2>&1 || {
|
|
echo "FAIL: machine create --from failed (Char device extraction bug)"
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
$SMOLVM machine start --name "$from_name" 2>&1 || {
|
|
echo "FAIL: machine start failed"
|
|
$SMOLVM machine delete --name "$from_name" -f 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# 6. Verify test file survived the roundtrip
|
|
echo " Step 6: Verifying test marker survived..."
|
|
local result
|
|
result=$($SMOLVM machine exec --name "$from_name" -- cat /test-marker.txt 2>&1) || {
|
|
echo "FAIL: test marker missing after roundtrip (Char device data loss)"
|
|
$SMOLVM machine stop --name "$from_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$from_name" -f 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
[[ "$result" == *"debian-roundtrip-ok"* ]] || {
|
|
echo "FAIL: test marker content mismatch (got: '$result')"
|
|
$SMOLVM machine stop --name "$from_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$from_name" -f 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
echo " Test marker survived Debian pack/unpack roundtrip"
|
|
|
|
# Cleanup
|
|
$SMOLVM machine stop --name "$from_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$from_name" -f 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
rm -f "$pack_output" "$pack_output.smolmachine"
|
|
}
|
|
|
|
# Regression: short VM names (1-3 chars) must not panic in pack create --from-vm.
|
|
# The overlay layer digest is derived from the VM name via SHA-256, so any length works.
|
|
test_from_vm_short_name() {
|
|
local vm_name="x"
|
|
local pack_output="$TEST_DIR/from-vm-short"
|
|
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
rm -f "$pack_output" "$pack_output.smolmachine"
|
|
|
|
# Create, start, modify, stop
|
|
$SMOLVM machine create --name "$vm_name" --image alpine:latest --net 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
$SMOLVM machine exec --name "$vm_name" -- touch /etc/short-name-marker 2>&1 || true
|
|
$SMOLVM machine stop --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# Pack — this is where the panic used to occur
|
|
local exit_code=0
|
|
$SMOLVM pack create --from-vm "$vm_name" -o "$pack_output" 2>&1 || exit_code=$?
|
|
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
rm -f "$pack_output" "$pack_output.smolmachine"
|
|
|
|
[[ $exit_code -eq 0 ]] || {
|
|
echo "FAIL: pack create --from-vm with 1-char name failed (exit $exit_code)"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# Case-Insensitive Collision Test (macOS regression)
|
|
#
|
|
# =============================================================================
|
|
# Case-Insensitive Collision Test (macOS regression)
|
|
#
|
|
# Regression test for macOS case-insensitive APFS. Linux OCI layers may
|
|
# contain paths that differ only in case (e.g., "gdebi" script vs "GDebi/"
|
|
# directory). On case-insensitive macOS, these collide during host-side layer
|
|
# extraction and previously caused a fatal "failed to unpack" error.
|
|
#
|
|
# This test builds a minimal Docker image with intentional case-conflicting
|
|
# paths, pushes it to a local registry, packs it, and verifies the packed
|
|
# binary runs successfully.
|
|
# =============================================================================
|
|
|
|
test_pack_case_collision() {
|
|
if [[ "$(uname)" != "Darwin" ]]; then
|
|
echo "SKIP: case-insensitive collision test only relevant on macOS"
|
|
return 0
|
|
fi
|
|
|
|
# Check if docker is available
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "SKIP: docker not installed"
|
|
return 0
|
|
fi
|
|
|
|
local img_tag="smolvm-case-test:latest"
|
|
local registry_port=5051
|
|
local registry_name="smolvm-test-registry-$$"
|
|
local registry_img="localhost:${registry_port}/smolvm-case-test:latest"
|
|
local output="$TEST_DIR/test-case-collision"
|
|
|
|
# 1. Build a minimal image with case-conflicting paths.
|
|
# Creates both "mymod" (file) and "MyMod/" (directory) under /usr/share/pkg/ —
|
|
# these are identical on case-insensitive APFS.
|
|
local dockerfile_dir
|
|
dockerfile_dir=$(mktemp -d)
|
|
cat > "$dockerfile_dir/Dockerfile" <<'DOCKERFILE'
|
|
FROM alpine:latest
|
|
RUN mkdir -p /usr/share/pkg/MyMod && \
|
|
echo '#!/bin/sh' > /usr/share/pkg/mymod && \
|
|
chmod +x /usr/share/pkg/mymod && \
|
|
echo 'print("init")' > /usr/share/pkg/MyMod/__init__.py && \
|
|
echo 'print("module")' > /usr/share/pkg/MyMod/Core.py
|
|
DOCKERFILE
|
|
|
|
echo " Building test image with case-conflicting paths..."
|
|
docker build --platform linux/arm64 -t "$img_tag" "$dockerfile_dir" >/dev/null 2>&1 || {
|
|
echo "SKIP: docker build failed"
|
|
rm -rf "$dockerfile_dir"
|
|
return 0
|
|
}
|
|
rm -rf "$dockerfile_dir"
|
|
|
|
# 2. Start a temporary local registry to push the image to.
|
|
echo " Starting temporary registry on port $registry_port..."
|
|
docker run -d -p "${registry_port}:5000" --name "$registry_name" registry:2 >/dev/null 2>&1 || {
|
|
echo "SKIP: could not start local registry"
|
|
return 0
|
|
}
|
|
|
|
# Ensure cleanup on exit (registry container + image)
|
|
local cleanup_done=false
|
|
cleanup_case_test() {
|
|
if [[ "$cleanup_done" == "true" ]]; then return; fi
|
|
cleanup_done=true
|
|
docker stop "$registry_name" >/dev/null 2>&1 || true
|
|
docker rm "$registry_name" >/dev/null 2>&1 || true
|
|
docker rmi "$img_tag" "$registry_img" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup_case_test RETURN
|
|
|
|
# 3. Push to the local registry.
|
|
docker tag "$img_tag" "$registry_img" >/dev/null 2>&1
|
|
docker push "$registry_img" >/dev/null 2>&1 || {
|
|
echo "SKIP: could not push to local registry"
|
|
cleanup_case_test
|
|
return 0
|
|
}
|
|
|
|
# 4. Pack the image.
|
|
echo " Packing image from local registry..."
|
|
$SMOLVM pack create --image "$registry_img" -o "$output" 2>&1 || {
|
|
echo "FAIL: pack create failed"
|
|
cleanup_case_test
|
|
return 1
|
|
}
|
|
|
|
[[ -f "$output" ]] || { echo "FAIL: packed binary not created"; return 1; }
|
|
[[ -f "$output.smolmachine" ]] || { echo "FAIL: sidecar not created"; return 1; }
|
|
|
|
# 5. Run the packed binary — this is the critical test.
|
|
# Previously this failed with: "failed to unpack .../GDebi"
|
|
# Verify BOTH case-conflicting paths exist in the guest filesystem.
|
|
# Just proving "echo works" is not enough — we need to confirm the
|
|
# image contents are faithful (both mymod file AND MyMod/ directory).
|
|
echo " Running packed binary (verifying filesystem fidelity)..."
|
|
local result
|
|
result=$(run_with_timeout 60 "$output" run -- /bin/sh -c \
|
|
"test -f /usr/share/pkg/mymod && test -d /usr/share/pkg/MyMod && test -f /usr/share/pkg/MyMod/__init__.py && echo 'case-fidelity-ok'" 2>&1)
|
|
local exit_code=$?
|
|
|
|
[[ $exit_code -eq 124 ]] && { echo "TIMEOUT: packed binary hung"; return 1; }
|
|
[[ "$result" == *"case-fidelity-ok"* ]] || {
|
|
echo "FAIL: case-conflicting paths not preserved in guest. Output: $result"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# Error Handling Tests
|
|
# =============================================================================
|
|
|
|
test_pack_nonexistent_image() {
|
|
local output="$TEST_DIR/test-nonexistent"
|
|
local exit_code=0
|
|
$SMOLVM pack create --image nonexistent-image-that-does-not-exist:v999 -o "$output" 2>&1 || exit_code=$?
|
|
[[ $exit_code -ne 0 ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Python Image Test (Larger image, skip in quick mode)
|
|
# =============================================================================
|
|
|
|
test_pack_python() {
|
|
if [[ "$QUICK_MODE" == "true" ]]; then
|
|
echo "SKIP: --quick mode"
|
|
return 0
|
|
fi
|
|
|
|
local output="$TEST_DIR/test-python"
|
|
$SMOLVM pack create --image python:3.12-slim -o "$output" 2>&1
|
|
|
|
[[ -f "$output" ]] && [[ -f "$output.smolmachine" ]]
|
|
}
|
|
|
|
test_packed_python_run() {
|
|
if [[ "$QUICK_MODE" == "true" ]]; then
|
|
echo "SKIP: --quick mode"
|
|
return 0
|
|
fi
|
|
|
|
local output="$TEST_DIR/test-python"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image python:3.12-slim -o "$output" 2>&1
|
|
fi
|
|
|
|
local result
|
|
result=$(run_with_timeout 90 "$output" run -- python -c "print('Hello from packed Python')" 2>&1)
|
|
[[ $? -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"Hello from packed Python"* ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Large Image: r-base
|
|
#
|
|
# Regression test for streaming layer export. r-base:latest is ~9 GB with 6
|
|
# layers. Previously failed with "connection closed" because the agent wrote
|
|
# a temp tar file that filled the 20 GB storage disk. The fix pipes tar stdout
|
|
# directly to the vsock stream with zero temp files.
|
|
# =============================================================================
|
|
|
|
test_pack_rbase() {
|
|
if [[ "$QUICK_MODE" == "true" ]]; then
|
|
echo "SKIP: --quick mode"
|
|
return 0
|
|
fi
|
|
|
|
local output="$TEST_DIR/test-rbase"
|
|
$SMOLVM pack create --image r-base:latest -o "$output" 2>&1
|
|
|
|
[[ -f "$output" ]] && [[ -f "$output.smolmachine" ]]
|
|
}
|
|
|
|
test_packed_rbase_run() {
|
|
if [[ "$QUICK_MODE" == "true" ]]; then
|
|
echo "SKIP: --quick mode"
|
|
return 0
|
|
fi
|
|
|
|
local output="$TEST_DIR/test-rbase"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
echo "SKIP: no packed binary (pack failed)"
|
|
return 1
|
|
fi
|
|
|
|
local result
|
|
result=$(run_with_timeout 120 "$output" run -- R --version 2>&1)
|
|
local exit_code=$?
|
|
|
|
[[ $exit_code -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"R version"* ]] || [[ "$result" == *"r ('littler') version"* ]]
|
|
}
|
|
|
|
test_packed_rbase_auto_storage() {
|
|
# Regression test: large images used to fail with "No space left on device"
|
|
# because the default 20 GiB storage wasn't enough. The fix auto-sizes the
|
|
# storage disk based on image_size in the manifest. --force-extract ensures
|
|
# a clean extraction (no cached overlay from a previous --storage 50 run).
|
|
if [[ "$QUICK_MODE" == "true" ]]; then
|
|
echo "SKIP: --quick mode"
|
|
return 0
|
|
fi
|
|
|
|
local output="$TEST_DIR/test-rbase"
|
|
|
|
if [[ ! -f "$output.smolmachine" ]]; then
|
|
echo "SKIP: no sidecar (pack failed)"
|
|
return 1
|
|
fi
|
|
|
|
local result
|
|
result=$(run_with_timeout 120 $SMOLVM pack run --sidecar "$output.smolmachine" --force-extract -- echo "auto-storage-ok" 2>&1)
|
|
local exit_code=$?
|
|
|
|
[[ $exit_code -eq 124 ]] && { echo "TIMEOUT"; return 1; }
|
|
[[ "$result" == *"auto-storage-ok"* ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Multi-Layer First Exec Performance Regression Test
|
|
#
|
|
# Regression test: images with many layers (e.g., rocker/tidyverse has 20)
|
|
# caused a 16-second first exec because overlayfs multi-lowerdir mount fails
|
|
# on virtiofs-backed layers. The fallback physically merged all layers on
|
|
# every first exec after restart. Fix: pre-merge layers at pack time so the
|
|
# packed binary has a single lowerdir that mounts instantly.
|
|
#
|
|
# Uses python:3.12 (7 layers) — enough to verify multi-layer handling while
|
|
# keeping test time reasonable. The threshold is 5 seconds; the old path
|
|
# took 10-16s, the fixed path takes <1s.
|
|
# =============================================================================
|
|
|
|
test_multi_layer_first_exec_fast() {
|
|
local pack_output="$TEST_DIR/test-multilayer"
|
|
local vm_name="multilayer-perf-$$"
|
|
local MAX_FIRST_EXEC_SECS=5
|
|
|
|
# Pack a multi-layer image (python:3.12 has 7 layers)
|
|
if [[ ! -f "$pack_output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image python:3.12 -o "$pack_output" 2>&1 || {
|
|
echo "SKIP: pack create failed"
|
|
return 0
|
|
}
|
|
fi
|
|
[[ -f "$pack_output.smolmachine" ]] || { echo "SKIP: no sidecar"; return 0; }
|
|
|
|
# Create machine from .smolmachine
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
$SMOLVM machine create --name "$vm_name" --from "$pack_output.smolmachine" --net 2>&1 || return 1
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
sleep 2
|
|
|
|
# Time the first exec (the critical measurement)
|
|
local t_start t_end elapsed
|
|
t_start=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s)
|
|
local result
|
|
result=$($SMOLVM machine exec --name "$vm_name" -- python3 -c "print('fast')" 2>&1)
|
|
t_end=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s)
|
|
elapsed=$(python3 -c "print(f'{$t_end - $t_start:.1f}')" 2>/dev/null || echo "?")
|
|
|
|
echo " First exec: ${elapsed}s (threshold: ${MAX_FIRST_EXEC_SECS}s)"
|
|
|
|
# Verify it worked
|
|
[[ "$result" == *"fast"* ]] || {
|
|
echo "FAIL: exec failed: $result"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
# Verify it was fast
|
|
local over_threshold
|
|
over_threshold=$(python3 -c "print('yes' if $t_end - $t_start > $MAX_FIRST_EXEC_SECS else 'no')" 2>/dev/null || echo "no")
|
|
if [[ "$over_threshold" == "yes" ]]; then
|
|
echo "FAIL: first exec took ${elapsed}s (>${MAX_FIRST_EXEC_SECS}s) — multi-layer overlay merge regression?"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
fi
|
|
|
|
# Also verify stop/start doesn't regress
|
|
$SMOLVM machine stop --name "$vm_name" 2>&1 || true
|
|
$SMOLVM machine start --name "$vm_name" 2>&1 || {
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
sleep 2
|
|
|
|
t_start=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s)
|
|
result=$($SMOLVM machine exec --name "$vm_name" -- python3 -c "print('still-fast')" 2>&1)
|
|
t_end=$(python3 -c 'import time; print(time.time())' 2>/dev/null || date +%s)
|
|
elapsed=$(python3 -c "print(f'{$t_end - $t_start:.1f}')" 2>/dev/null || echo "?")
|
|
|
|
echo " After restart: ${elapsed}s"
|
|
|
|
[[ "$result" == *"still-fast"* ]] || {
|
|
echo "FAIL: exec after restart failed: $result"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
}
|
|
|
|
over_threshold=$(python3 -c "print('yes' if $t_end - $t_start > $MAX_FIRST_EXEC_SECS else 'no')" 2>/dev/null || echo "no")
|
|
if [[ "$over_threshold" == "yes" ]]; then
|
|
echo "FAIL: exec after restart took ${elapsed}s (>${MAX_FIRST_EXEC_SECS}s)"
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1
|
|
fi
|
|
|
|
# Cleanup
|
|
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
|
|
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
|
|
}
|
|
|
|
# =============================================================================
|
|
# Run Tests
|
|
# =============================================================================
|
|
|
|
echo "Running Pack Command Tests..."
|
|
echo ""
|
|
|
|
run_test "Pack help" test_pack_help || true
|
|
run_test "Pack requires output" test_pack_requires_output || true
|
|
run_test "Pack alpine" test_pack_alpine || true
|
|
run_test "Pack with custom resources" test_pack_with_custom_resources || true
|
|
run_test "Pack with --oci-platform" test_pack_with_platform || true
|
|
|
|
echo ""
|
|
echo "Running Packed Binary Info Tests..."
|
|
echo ""
|
|
|
|
run_test "Packed --info" test_packed_info || true
|
|
run_test "Packed --version" test_packed_version || true
|
|
run_test "Packed --help" test_packed_help || true
|
|
run_test "Sidecar has SMOLPACK magic" test_sidecar_has_magic || true
|
|
run_test "Sidecar has no libs (V3)" test_sidecar_has_no_libs || true
|
|
run_test "Stub has SMOLLIBS footer" test_stub_has_libs_footer || true
|
|
run_test "Binary is clean Mach-O" test_binary_is_clean_macho || true
|
|
|
|
echo ""
|
|
echo "Running Library Compatibility Tests..."
|
|
echo ""
|
|
|
|
run_test "Bundled libkrun has required symbols" test_pack_bundled_libkrun_has_required_symbols || true
|
|
run_test "Pack uses loaded libkrun (dladdr)" test_pack_uses_loaded_libkrun || true
|
|
|
|
echo ""
|
|
echo "Running Sidecar Tests..."
|
|
echo ""
|
|
|
|
run_test "Sidecar required" test_sidecar_required || true
|
|
|
|
echo ""
|
|
echo "Running Single-File Mode Tests..."
|
|
echo ""
|
|
|
|
run_test "Single-file pack" test_single_file_pack || true
|
|
run_test "Single-file run echo (requires VM)" test_single_file_run_echo || true
|
|
|
|
echo ""
|
|
echo "Running Packed Binary Execution Tests (requires VM)..."
|
|
echo ""
|
|
|
|
run_test "Packed run echo" test_packed_run_echo || true
|
|
run_test "Packed exit code" test_packed_exit_code || true
|
|
run_test "Packed env variable" test_packed_env_var || true
|
|
run_test "Packed workdir" test_packed_workdir || true
|
|
|
|
echo ""
|
|
echo "Running pack run subcommand Tests..."
|
|
echo ""
|
|
|
|
run_test "pack run help" test_pack_run_help || true
|
|
run_test "pack run --info" test_pack_run_info || true
|
|
run_test "pack run --info with missing sidecar" test_pack_run_info_no_sidecar || true
|
|
run_test "pack run auto-detect sidecar" test_pack_run_auto_detect || true
|
|
run_test "pack run auto-detect ambiguous" test_pack_run_auto_detect_ambiguous || true
|
|
|
|
echo ""
|
|
echo "Running pack run execution tests (requires VM)..."
|
|
echo ""
|
|
|
|
run_test "pack run resource override" test_pack_run_resource_override || true
|
|
run_test "pack run --force-extract" test_pack_run_force_extract || true
|
|
run_test "pack run cached fast" test_pack_run_cached_fast || true
|
|
|
|
if [[ "$QUICK_MODE" != "true" ]]; then
|
|
echo ""
|
|
echo "Running --from-vm Tests (requires VM + network)..."
|
|
echo ""
|
|
|
|
run_test "from-vm: rejects running VM" test_from_vm_rejects_running || true
|
|
run_test "from-vm: setup VM with curl" test_from_vm_setup || true
|
|
run_test "from-vm: pack stopped VM" test_from_vm_pack || true
|
|
run_test "from-vm: finds installed package" test_from_vm_run_finds_installed_package || true
|
|
run_test "from-vm: cleanup" test_from_vm_cleanup || true
|
|
|
|
echo ""
|
|
echo "Running --from-vm Image-Based Tests (BUG-17 regression)..."
|
|
echo ""
|
|
|
|
run_test "from-vm-image: container overlay captured" test_from_vm_image_overlay || true
|
|
run_test "from-vm-image: imported layers preserved on repack" test_from_vm_imported_layers_preserved_on_repack || true
|
|
run_test "from-vm-image: rebase flag drops imported layers" test_from_vm_rebase_from_image_drops_imported_layers || true
|
|
|
|
echo ""
|
|
echo "Running Debian Pack Roundtrip Tests..."
|
|
echo ""
|
|
|
|
run_test "from-vm-debian: Char device entries don't break extraction" test_from_vm_debian_roundtrip || true
|
|
|
|
echo ""
|
|
echo "Running --from-vm Short Name Tests..."
|
|
echo ""
|
|
|
|
run_test "from-vm: short VM name (1 char) does not panic" test_from_vm_short_name || true
|
|
fi
|
|
|
|
# =============================================================================
|
|
# Packed Binary - Bare Invocation (no subcommand)
|
|
# =============================================================================
|
|
|
|
test_packed_bare_invocation() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# ./my-app with no subcommand should run the manifest entrypoint.
|
|
# Alpine's default is /bin/sh — without -it it reads stdin, gets EOF,
|
|
# and exits 0. That exit-0 proves the VM booted and ran the entrypoint
|
|
# instead of printing clap usage (which would exit non-zero).
|
|
local exit_code=0
|
|
run_with_timeout 30 "$output" </dev/null 2>&1 || exit_code=$?
|
|
|
|
if [[ $exit_code -eq 124 ]]; then
|
|
echo "TIMEOUT: bare invocation hung"
|
|
return 1
|
|
fi
|
|
|
|
[[ $exit_code -eq 0 ]]
|
|
}
|
|
|
|
# =============================================================================
|
|
# Packed Binary - Daemon Lifecycle (start/exec/status/stop)
|
|
# =============================================================================
|
|
|
|
test_packed_daemon_lifecycle() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
# Ensure we have a packed binary
|
|
if [[ ! -f "$output" ]] || [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# 1. Start the daemon
|
|
local start_result
|
|
start_result=$(run_with_timeout 60 "$output" start 2>&1)
|
|
local start_exit=$?
|
|
[[ $start_exit -eq 124 ]] && { echo "TIMEOUT on start"; return 1; }
|
|
[[ "$start_result" == *"Daemon started"* ]] || { echo "Start failed: $start_result"; return 1; }
|
|
|
|
# 2. Check status
|
|
local status_result
|
|
status_result=$("$output" status 2>&1) || true
|
|
[[ "$status_result" == *"running"* ]] || { echo "Status failed: $status_result"; "$output" stop 2>/dev/null || true; return 1; }
|
|
|
|
# 3. Exec a command
|
|
local exec_result
|
|
exec_result=$(run_with_timeout 30 "$output" exec -- echo "daemon-exec-marker" 2>&1)
|
|
local exec_exit=$?
|
|
[[ $exec_exit -eq 124 ]] && { echo "TIMEOUT on exec"; "$output" stop 2>/dev/null || true; return 1; }
|
|
[[ "$exec_result" == *"daemon-exec-marker"* ]] || { echo "Exec failed: $exec_result"; "$output" stop 2>/dev/null || true; return 1; }
|
|
|
|
# 4. Stop the daemon
|
|
local stop_result
|
|
stop_result=$("$output" stop 2>&1) || true
|
|
[[ "$stop_result" == *"Daemon stopped"* ]] || { echo "Stop failed: $stop_result"; return 1; }
|
|
|
|
# 5. Verify stopped
|
|
local status_after
|
|
status_after=$("$output" status 2>&1) || true
|
|
[[ "$status_after" == *"not running"* ]]
|
|
}
|
|
|
|
test_packed_daemon_already_running() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]] || [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Start the daemon
|
|
run_with_timeout 60 "$output" start 2>&1 || { echo "Initial start failed"; return 1; }
|
|
|
|
# Starting again should say already running (not error)
|
|
local result
|
|
result=$("$output" start 2>&1) || true
|
|
[[ "$result" == *"already running"* ]] || { "$output" stop 2>/dev/null || true; echo "Expected 'already running': $result"; return 1; }
|
|
|
|
# Clean up
|
|
"$output" stop 2>/dev/null || true
|
|
}
|
|
|
|
test_packed_exec_without_daemon() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]] || [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Make sure daemon is not running
|
|
"$output" stop 2>/dev/null || true
|
|
|
|
# exec without a running daemon should fail with a clear message
|
|
local result
|
|
local exit_code=0
|
|
result=$("$output" exec -- echo hello 2>&1) || exit_code=$?
|
|
[[ $exit_code -ne 0 ]] || return 1
|
|
[[ "$result" == *"not running"* ]]
|
|
}
|
|
|
|
test_packed_stop_without_daemon() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]] || [[ ! -f "$output.smolmachine" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Make sure daemon is not running
|
|
"$output" stop 2>/dev/null || true
|
|
|
|
# stop without a running daemon should succeed gracefully
|
|
local result
|
|
result=$("$output" stop 2>&1) || true
|
|
[[ "$result" == *"not running"* ]] || [[ "$result" == *"Daemon stopped"* ]]
|
|
}
|
|
|
|
test_packed_info_subcommand() {
|
|
local output="$TEST_DIR/test-alpine"
|
|
|
|
if [[ ! -f "$output" ]]; then
|
|
$SMOLVM pack create --image alpine:latest -o "$output" 2>&1
|
|
fi
|
|
|
|
# Test info as subcommand
|
|
local info_output
|
|
info_output=$("$output" info 2>&1)
|
|
[[ "$info_output" == *"Image:"* ]] && \
|
|
[[ "$info_output" == *"Platform:"* ]] && \
|
|
[[ "$info_output" == *"Checksum:"* ]] || return 1
|
|
}
|
|
|
|
echo ""
|
|
echo "Running Packed Binary Bare Invocation Tests (requires VM)..."
|
|
echo ""
|
|
|
|
run_test "Packed bare invocation (no subcommand)" test_packed_bare_invocation || true
|
|
|
|
echo ""
|
|
echo "Running Packed Daemon Lifecycle Tests (requires VM)..."
|
|
echo ""
|
|
|
|
run_test "Daemon lifecycle: start -> exec -> status -> stop" test_packed_daemon_lifecycle || true
|
|
run_test "Daemon already running" test_packed_daemon_already_running || true
|
|
run_test "Exec without daemon" test_packed_exec_without_daemon || true
|
|
run_test "Stop without daemon" test_packed_stop_without_daemon || true
|
|
run_test "Info subcommand" test_packed_info_subcommand || true
|
|
|
|
echo ""
|
|
echo "Running Error Handling Tests..."
|
|
echo ""
|
|
|
|
run_test "Pack nonexistent image" test_pack_nonexistent_image || true
|
|
|
|
echo ""
|
|
echo "Running Case-Insensitive Collision Tests (macOS)..."
|
|
echo ""
|
|
|
|
run_test "Pack image with case-conflicting paths" test_pack_case_collision || true
|
|
|
|
if [[ "$QUICK_MODE" != "true" ]]; then
|
|
echo ""
|
|
echo "Running Large Image Tests..."
|
|
echo ""
|
|
|
|
run_test "Pack Python image" test_pack_python || true
|
|
run_test "Packed Python run" test_packed_python_run || true
|
|
run_test "pack run Python" test_pack_run_python || true
|
|
run_test "Pack r-base (large image, streaming export)" test_pack_rbase || true
|
|
run_test "Packed r-base run" test_packed_rbase_run || true
|
|
run_test "Packed r-base auto-sized storage (force-extract)" test_packed_rbase_auto_storage || true
|
|
run_test "First exec instant on multi-layer .smolmachine" test_multi_layer_first_exec_fast || true
|
|
fi
|
|
|
|
print_summary "Pack Tests"
|