项目文件夹

文件
2026-07-13 13:13:07 +08:00

337 行
13 KiB
Bash
可执行文件

#!/usr/bin/env bash
#
# Storage Tests (overlay, prune, resize)
#
# Part of the smolvm test suite. Run with: ./tests/test_storage.sh
#
source "$(dirname "$0")/common.sh"
init_smolvm
log_info "Pre-flight cleanup: killing orphan processes..."
kill_orphan_smolvm_processes
trap cleanup_machine EXIT
echo ""
echo "=========================================="
echo " Storage Tests (overlay, prune, resize)"
echo "=========================================="
echo ""
test_machine_overlay_root_active() {
local vm_name="overlay-active-test-$$"
# Clean up any existing
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
# Create and start VM
$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; }
# Check that root is an overlay mount
local output exit_code=0
output=$($SMOLVM machine exec --name "$vm_name" -- mount 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
ensure_data_dir_deleted "$vm_name"
[[ $exit_code -eq 0 ]] && [[ "$output" == *"overlay on / type overlay"* ]]
}
test_machine_rootfs_persists_across_reboot() {
local vm_name="overlay-persist-test-$$"
# Clean up any existing
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
# Create and start VM
$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; }
# Write a marker file to the rootfs
local exit_code=0
$SMOLVM machine exec --name "$vm_name" -- sh -c "echo persistence-test-ok > /tmp/overlay-test-marker" 2>&1 || exit_code=$?
if [[ $exit_code -ne 0 ]]; then
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
return 1
fi
# Verify file exists before reboot
local output
output=$($SMOLVM machine exec --name "$vm_name" -- cat /tmp/overlay-test-marker 2>&1) || {
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
return 1
}
if [[ "$output" != *"persistence-test-ok"* ]]; then
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
return 1
fi
# Stop and restart the VM
$SMOLVM machine stop --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; }
$SMOLVM machine start --name "$vm_name" 2>&1 || { $SMOLVM machine delete --name "$vm_name" -f 2>/dev/null; return 1; }
# Verify the file survived the reboot
exit_code=0
output=$($SMOLVM machine exec --name "$vm_name" -- cat /tmp/overlay-test-marker 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
ensure_data_dir_deleted "$vm_name"
[[ $exit_code -eq 0 ]] && [[ "$output" == *"persistence-test-ok"* ]]
}
test_machine_overlay_size() {
local vm_name="test-vm-overlay-size"
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
# Create VM with custom overlay size (4 GiB)
$SMOLVM machine create --name "$vm_name" --overlay 4 2>&1 || return 1
$SMOLVM machine start --name "$vm_name" 2>&1 || {
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null
return 1
}
# Check the overlay disk size inside the VM via df
local df_output
df_output=$($SMOLVM machine exec --name "$vm_name" -- df -m / 2>&1)
# Cleanup
$SMOLVM machine stop --name "$vm_name" 2>/dev/null || true
$SMOLVM machine delete --name "$vm_name" -f 2>/dev/null || true
ensure_data_dir_deleted "$vm_name"
# The 4GB overlay should show ~3800-4096 MB total (ext4 overhead)
# Just verify it's > 3000 MB (not the old 2GB default)
local total_mb
total_mb=$(echo "$df_output" | tail -1 | awk '{print $2}')
[[ "$total_mb" -gt 3000 ]]
}
test_machine_images() {
# Start default machine and check images command
$SMOLVM machine stop 2>/dev/null || true
$SMOLVM machine delete --name default -f 2>/dev/null || true
$SMOLVM machine create --net --name default 2>/dev/null || true
$SMOLVM machine start --name default 2>/dev/null || true
local output
output=$($SMOLVM machine images --name default 2>&1)
$SMOLVM machine stop 2>/dev/null || true
$SMOLVM machine delete --name default -f 2>/dev/null || true
# Should show storage info
[[ "$output" == *"Storage"* ]] || [[ "$output" == *"storage"* ]]
}
test_machine_prune_dry_run() {
$SMOLVM machine stop 2>/dev/null || true
$SMOLVM machine delete --name default -f 2>/dev/null || true
$SMOLVM machine create --net --name default 2>/dev/null || true
$SMOLVM machine start --name default 2>/dev/null || true
local output
output=$($SMOLVM machine prune --name default --dry-run 2>&1)
$SMOLVM machine stop 2>/dev/null || true
$SMOLVM machine delete --name default -f 2>/dev/null || true
# Should complete without error
[[ $? -eq 0 ]] || [[ "$output" == *"unreferenced"* ]] || [[ "$output" == *"No unreferenced"* ]]
}
assert_vm_stays_running() {
local desc="$1"; shift
ensure_machine_running
local status
status=$($SMOLVM machine status 2>&1)
[[ "$status" == *"running"* ]] || { echo "VM not running before '$desc'"; return 1; }
"$@" 2>&1 || return 1
status=$($SMOLVM machine status 2>&1)
[[ "$status" == *"running"* ]] || { echo "VM stopped after '$desc'"; return 1; }
}
test_images_does_not_stop_running_vm() {
assert_vm_stays_running "machine images" $SMOLVM machine images --name default
}
test_prune_on_running_vm() {
ensure_machine_running
local status
status=$($SMOLVM machine status 2>&1)
[[ "$status" == *"running"* ]] || { echo "VM not running before test"; return 1; }
# Regular prune should work without stopping the VM
local output
output=$($SMOLVM machine prune --name default 2>&1) || true
[[ "$output" == *"unreferenced"* ]] || [[ "$output" == *"No unreferenced"* ]] || {
echo "unexpected prune output: $output"
return 1
}
# VM should still be running after prune
status=$($SMOLVM machine status 2>&1)
[[ "$status" == *"running"* ]] || { echo "VM stopped after prune"; return 1; }
}
test_prune_dry_run_on_running_vm() {
ensure_machine_running
local output
output=$($SMOLVM machine prune --name default --dry-run 2>&1) || true
[[ "$output" == *"unreferenced"* ]] || [[ "$output" == *"No unreferenced"* ]] || {
echo "unexpected prune --dry-run output: $output"
return 1
}
# VM should still be running
local status
status=$($SMOLVM machine status 2>&1)
[[ "$status" == *"running"* ]] || { echo "VM stopped after prune --dry-run"; return 1; }
}
test_prune_all_refuses_on_running_vm() {
ensure_machine_running "true"
local status
status=$($SMOLVM machine status 2>&1)
[[ "$status" == *"running"* ]] || { echo "VM not running before test"; return 1; }
# --all should refuse while the VM is running
local output exit_code=0
output=$($SMOLVM machine prune --name default --all 2>&1) || exit_code=$?
[[ $exit_code -ne 0 ]] || { echo "prune --all should have failed on running VM"; return 1; }
[[ "$output" == *"cannot prune --all while machine"* ]] || {
echo "unexpected error: $output"
return 1
}
# VM should still be running
status=$($SMOLVM machine status 2>&1)
[[ "$status" == *"running"* ]] || { echo "VM stopped after rejected prune --all"; return 1; }
}
test_prune_all_keeps_in_use_image() {
# An image-backed machine needs its cached image to restart, so `prune --all`
# must keep it (and reclaim only unreferenced layers) rather than brick a
# stopped machine with "image not found" on the next start.
$SMOLVM machine stop 2>/dev/null || true
$SMOLVM machine delete --name default -f 2>/dev/null || true
$SMOLVM machine create --image alpine --net --name default 2>&1 || return 1
$SMOLVM machine start 2>&1 || return 1
# Verify the image is cached and the VM is reachable
$SMOLVM machine exec -- true 2>&1 || { echo "VM not reachable"; return 1; }
# Stop the VM (prune --all requires it)
$SMOLVM machine stop 2>&1
# Run prune --all: it must keep the in-use image, not remove it.
local output
output=$($SMOLVM machine prune --name default --all 2>&1) || {
echo "prune --all failed: $output"
$SMOLVM machine delete --name default -f 2>/dev/null
return 1
}
[[ "$output" == *"Kept"* ]] || [[ "$output" == *"image-backed"* ]] || {
echo "expected prune --all to keep the in-use image, got: $output"
$SMOLVM machine delete --name default -f 2>/dev/null
return 1
}
# The machine must still restart — its image was kept, not pruned.
$SMOLVM machine start 2>&1 || {
echo "FAIL: machine could not restart after prune --all (image was pruned)"
$SMOLVM machine delete --name default -f 2>/dev/null
return 1
}
$SMOLVM machine exec -- true 2>&1 || {
echo "FAIL: VM not reachable after prune + restart"
$SMOLVM machine delete --name default -f 2>/dev/null
return 1
}
$SMOLVM machine stop 2>/dev/null || true
$SMOLVM machine delete --name default -f 2>/dev/null || true
}
test_storage_resize_and_large_pull() {
# Force a fresh storage disk by deleting the default VM data directory.
# This ensures we exercise the template → resize → mount path.
$SMOLVM machine stop 2>/dev/null || true
local data_dir
data_dir=$(vm_data_dir "default")
rm -rf "$data_dir" 2>/dev/null || true
# Pull python:3.12 (full image: ~150MB compressed, ~1GB extracted).
# This exceeds the 512MB template size, so it will fail with ENOSPC
# if the storage disk was not properly resized from 512MB to 20GB.
local output exit_code=0
output=$($SMOLVM machine run --net --image python:3.12 -- python3 -c 'import sys; print(f"python {sys.version_info.major}.{sys.version_info.minor}")' 2>&1) || exit_code=$?
echo "$output"
# Verify the command succeeded and Python ran
[[ $exit_code -eq 0 ]] || { echo "Exit code: $exit_code"; return 1; }
[[ "$output" == *"python 3.12"* ]] || { echo "Expected python 3.12 output"; return 1; }
}
test_storage_mounted_as_ext4() {
# Verify /dev/vda is actually mounted at /storage as ext4 (not on overlay).
# This catches the bug where mount_storage_disk() silently fails and
# /storage is just a directory on the overlay rootfs.
local output
output=$($SMOLVM machine run --net -- sh -c '
mount_line=$(mount | grep "/dev/vda")
if [ -z "$mount_line" ]; then
echo "FAIL: /dev/vda not mounted"
exit 1
fi
echo "$mount_line"
# Verify the filesystem is large (>1GB = properly resized from 512MB template)
avail_kb=$(df /storage | tail -1 | awk "{print \$4}")
if [ "$avail_kb" -lt 1048576 ]; then
echo "FAIL: /storage too small (${avail_kb}KB available, expected >1GB)"
exit 1
fi
echo "PASS: storage mounted and resized"
' 2>&1)
echo "$output"
[[ "$output" == *"PASS: storage mounted and resized"* ]]
}
run_test "Overlay: root is overlayfs" test_machine_overlay_root_active || true
run_test "Overlay: rootfs persists across reboot" test_machine_rootfs_persists_across_reboot || true
run_test "Overlay: custom size via --overlay" test_machine_overlay_size || true
run_test "Machine images" test_machine_images || true
run_test "Machine prune --dry-run" test_machine_prune_dry_run || true
run_test "Images: does not stop running VM" test_images_does_not_stop_running_vm || true
run_test "Prune: works on running VM without stopping it" test_prune_on_running_vm || true
run_test "Prune --dry-run: works on running VM" test_prune_dry_run_on_running_vm || true
run_test "Prune --all: refuses on running VM" test_prune_all_refuses_on_running_vm || true
run_test "Prune --all: keeps in-use image, machine still restarts" test_prune_all_keeps_in_use_image || true
run_test "Storage: resize + large image pull (fresh disk)" test_storage_resize_and_large_pull || true
run_test "Storage: /dev/vda mounted as ext4 with correct size" test_storage_mounted_as_ext4 || true
print_summary "Storage Tests"