#!/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" &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"