aboutsummaryrefslogtreecommitdiff
path: root/tests/boot
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-01 00:00:00 +0000
committerLena <lena@omega>2026-08-01 00:00:00 +0000
commitc235cb62beefd03087e7cfc7f7f55f0c7c6601c9 (patch)
tree2776f86d9226d55c78e3d735764fcd9ecbe5e08e /tests/boot
downloadvmm-master.tar.gz
Enter vmmHEADmaster
A QEMU/KVM virtual machine manager in one POSIX shell script. Each VM is a directory of plain files under $VMMDIR, configured by a hand-edited KEY=VALUE file that is parsed rather than sourced, so nothing in it can inject a raw QEMU argument. Control is a QMP FIFO pair inside that directory, guarded by its permissions alone; nothing binds a TCP port. The tests are TAP without a framework and boot real guests where the host allows.
Diffstat (limited to 'tests/boot')
-rw-r--r--tests/boot250
1 files changed, 250 insertions, 0 deletions
diff --git a/tests/boot b/tests/boot
new file mode 100644
index 0000000..2f778eb
--- /dev/null
+++ b/tests/boot
@@ -0,0 +1,250 @@
+# A real guest, booted for real. Skipped without a usable /dev/kvm, so the
+# suite still exits 0 on a machine with no virtualisation.
+
+if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then
+ skip 1 "no usable /dev/kvm, skipping the boot tests"
+ return 0
+fi
+
+"$VMM" create bt1 64M >/dev/null 2>&1
+cfg bt1 <<EOF
+MEM=256
+CPUS=1
+EOF
+
+exits 0 "start boots a guest" start bt1
+PID=$(cat "$VMMDIR/bt1/pid" 2>/dev/null || echo)
+if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
+ ok "qemu is running"
+else
+ notok "qemu is running" "$(tail -n 5 "$VMMDIR/bt1/stderr" 2>/dev/null)"
+fi
+
+holds "qemu's stdout lands in a file" test -f "$VMMDIR/bt1/stdout"
+holds "qemu's stderr lands in another" test -f "$VMMDIR/bt1/stderr"
+outputs '==> .*stderr <==' "logs shows every stream" logs bt1
+
+exits 0 "status of a running VM exits 0" status bt1
+outputs 'STATE=running' "status reports running" status bt1
+outputs 'running' "list reports running" list
+
+# The lock must be free after start, or every later verb reports busy.
+holds "start releases its lock" flock -n "$VMMDIR/bt1" true
+
+exits 0 "start is idempotent" start bt1
+outputs 'already running' "a second start says so" start bt1
+
+# restart swallows exit 5 from a stop that had to force the guest, and
+# nothing else. A guest with no OS ignores ACPI, so this is that path,
+# and it must come back as a different process.
+old=$(cat "$VMMDIR/bt1/pid" 2>/dev/null || echo)
+exits 0 "restart survives a stop that had to force" restart bt1
+new=$(cat "$VMMDIR/bt1/pid" 2>/dev/null || echo)
+if [ -n "$new" ] && [ "$new" != "$old" ]; then
+ ok "restart comes back as a new process"
+else
+ notok "restart comes back as a new process" "was [$old] now [$new]"
+fi
+outputs 'STATE=running' "the restarted guest is running" status bt1
+
+# A restart that cannot start again must not report success: the config
+# is broken under the guest, so stop works and start refuses.
+cp "$VMMDIR/bt1/config" "$WORK/bt1.config"
+cfg bt1 <<EOF
+NOSUCHKEY=1
+EOF
+refuses 'unknown key' "restart fails loudly when it cannot start again" restart bt1
+exits 3 "and leaves the guest stopped, not half restarted" status bt1
+cp "$WORK/bt1.config" "$VMMDIR/bt1/config"
+exits 0 "start again for the tests below" start bt1
+
+outputs 'running' "the monitor answers" monitor bt1 info status
+
+# Commands read from stdin: the count says every one was sent.
+out=$(printf 'info status\ninfo version\n' | "$VMM" monitor bt1 2>&1) && got=0 || got=$?
+n=$(printf '%s\n' "$out" | grep -c '"return"' || :)
+if [ "$got" = 0 ] && [ "$n" = 2 ]; then
+ ok "monitor answers every command it reads from stdin"
+else
+ notok "monitor answers every command it reads from stdin" \
+ "exit $got, $n replies" "$out"
+fi
+
+# A final line with no newline is a command too: dropped, it would be
+# exit 0 and nothing done.
+out=$(printf 'info status' | "$VMM" monitor bt1 2>&1) && got=0 || got=$?
+if [ "$got" = 0 ] && printf '%s\n' "$out" | grep -q '"return"'; then
+ ok "monitor sends a final line with no newline"
+else
+ notok "monitor sends a final line with no newline" "exit $got: $out"
+fi
+
+# The command is interpolated into a JSON string. This payload closes
+# that string and the object around it, so an unescaped one reaches
+# QEMU's parser as a second complete command and stops the guest. An
+# unescaped backslash instead makes the object unparseable, and QEMU
+# refuses it with an error carrying no id, so no reply arrives at all.
+outputs 'unknown command' "a quote cannot close the monitor's own object" \
+ monitor bt1 'x"}},{"execute":"quit'
+exits 0 "and the quit it carried never ran" status bt1
+outputs '"return"' "a backslash still leaves the monitor a parseable object" \
+ monitor bt1 'info \ block'
+
+exits 5 "stop escalates and exits 5 on a guest with no OS" stop bt1
+holds "stop removes the pidfile once dead" test ! -e "$VMMDIR/bt1/pid"
+exits 0 "stop is idempotent" stop bt1
+
+# SNAPSHOT=yes must build a real overlay, never rely on -snapshot.
+cp "$VMMDIR/bt1/disk.qcow2" "$WORK/bt1-before-snapshot.qcow2"
+cfg bt1 <<EOF
+MEM=256
+CPUS=1
+SNAPSHOT=yes
+EOF
+exits 0 "start with SNAPSHOT=yes" start bt1
+holds "the overlay is unlinked once qemu holds it" \
+ test ! -e "$VMMDIR/bt1/ephemeral.qcow2"
+# Unlinked, but open: qemu still serves the guest from it, and says so.
+outputs 'ephemeral.qcow2' "the guest runs on the overlay, not the disk" \
+ monitor bt1 info block
+outputs '"return": ""' "a snapshot accepts guest disk writes" monitor bt1 \
+ 'qemu-io -d /machine/peripheral/blk0/virtio-backend "write -P 0xa5 1048576 4096"'
+exits 0 "the guest survives losing the overlay's name" status bt1
+
+"$VMM" kill bt1 >/dev/null 2>&1 || :
+holds "snapshot writes do not change the durable disk" qemu-img compare -q \
+ "$WORK/bt1-before-snapshot.qcow2" "$VMMDIR/bt1/disk.qcow2"
+exits 0 "kill is idempotent" kill bt1
+
+# The same write without SNAPSHOT must survive a complete QEMU restart.
+cfg bt1 <<EOF
+MEM=256
+CPUS=1
+EOF
+exits 0 "start without snapshot mode" start bt1
+outputs '"return": ""' "an ordinary guest accepts a disk write" monitor bt1 \
+ 'qemu-io -d /machine/peripheral/blk0/virtio-backend "write -P 0x5a 2097152 4096"'
+"$VMM" kill bt1 >/dev/null 2>&1 || :
+exits 0 "restart after a durable disk write" start bt1
+outputs '"return": ""' "an ordinary disk write survives restart" monitor bt1 \
+ 'qemu-io -d /machine/peripheral/blk0/virtio-backend "read -P 0x5a 2097152 4096"'
+"$VMM" kill bt1 >/dev/null 2>&1 || :
+
+# A running VM must be undeletable and unclonable.
+cfg bt1 <<EOF
+MEM=256
+CPUS=1
+EOF
+"$VMM" start bt1 >/dev/null 2>&1 || :
+exits 4 "delete refuses a running VM" delete -f bt1
+exits 4 "clone refuses a running source" clone bt1 bt9
+"$VMM" kill bt1 >/dev/null 2>&1 || :
+exits 0 "delete -f removes it once stopped" delete -f bt1
+
+# status answers about the process, so an unparseable config must not stop
+# it, and a config edited under a live guest must not make it lie.
+"$VMM" create bt2 64M >/dev/null 2>&1
+cfg bt2 <<EOF
+MEM=256
+CPUS=1
+EOF
+"$VMM" start bt2 >/dev/null 2>&1 || :
+cfg bt2 <<EOF
+THIS IS NOT A CONFIG
+EOF
+outputs 'STATE=running' "status ignores a broken config" status bt2
+omits '^MEM=' "status reports no config values at all" status bt2
+
+# A live guest must be visible under every spelling of VMMDIR, or the
+# verbs that clean up delete the state of a running guest.
+state=$(VMMDIR="$VMMDIR/" "$VMM" status bt2 2>/dev/null | sed -n 's/^STATE=//p')
+if [ "$state" = running ]; then
+ ok "a running guest is visible through a trailing slash"
+else
+ notok "a running guest is visible through a trailing slash" "got [$state]"
+fi
+
+# vm_pid coming back empty is not proof of death. When some other live
+# qemu owns the pidfile, the cleanup paths must leave the directory alone.
+P2=$(cat "$VMMDIR/bt2/pid" 2>/dev/null || echo)
+mkdir -p "$VMMDIR/bt3"
+cp "$VMMDIR/bt2/config" "$VMMDIR/bt3/config"
+printf '%s\n' "$P2" > "$VMMDIR/bt3/pid"
+exits 4 "a different live qemu is reported as unknown state" stop bt3
+holds "state owned by an unrecognised live qemu is left alone" \
+ test -f "$VMMDIR/bt3/pid"
+state=$("$VMM" list | awk '$1 == "bt3" { print $2 }')
+if [ "$state" = unknown ]; then
+ ok "list exposes unrecognised live qemu state"
+else
+ notok "list exposes unrecognised live qemu state" "got [$state]"
+fi
+rm -rf "$VMMDIR/bt3"
+
+# A wedged monitor must be reported, not silently reported as success.
+P=$(cat "$VMMDIR/bt2/pid" 2>/dev/null || echo)
+if [ -n "$P" ]; then
+ kill -STOP "$P" 2>/dev/null || :
+ out=$(MONITOR_TIMEOUT=1 "$VMM" monitor bt2 info status 2>&1) && got=0 || got=$?
+ kill -CONT "$P" 2>/dev/null || :
+ if [ "$got" != 0 ] && printf '%s' "$out" | grep -q 'no reply'; then
+ ok "a silent monitor is an error, not exit 0"
+ else
+ notok "a silent monitor is an error, not exit 0" "exit $got: $out"
+ fi
+ # The shell reports the job timeout kills unless it is reaped where
+ # nobody can see it, and that report is not vmm's to print.
+ if printf '%s' "$out" | grep -q 'Terminated'; then
+ notok "a timed out call prints nothing but vmm's own error" "$out"
+ else
+ ok "a timed out call prints nothing but vmm's own error"
+ fi
+else
+ skip 2 "bt2 did not start, cannot test the wedged monitor"
+fi
+
+"$VMM" kill bt2 >/dev/null 2>&1 || :
+exits 3 "status of the killed guest exits 3" status bt2
+
+# GRAPHICS=vnc listens on a unix socket in the VM directory, and status
+# reports the socket that is really there rather than what the config says.
+"$VMM" create bt4 64M >/dev/null 2>&1
+cfg bt4 <<EOF
+MEM=256
+CPUS=1
+GRAPHICS=vnc
+EOF
+if "$VMM" start bt4 >/dev/null 2>&1; then
+ holds "GRAPHICS=vnc creates the socket" test -S "$VMMDIR/bt4/vnc.sock"
+ outputs "VNC=$VMMDIR/bt4/vnc.sock" "status names the VNC socket" status bt4
+ "$VMM" kill bt4 >/dev/null 2>&1 || :
+ holds "the socket is removed once stopped" test ! -e "$VMMDIR/bt4/vnc.sock"
+ out=$("$VMM" status bt4 2>&1) && got=0 || got=$?
+ if [ "$got" = 3 ] && ! printf '%s\n' "$out" | grep -q '^VNC='; then
+ ok "a stopped VM has no VNC line"
+ else
+ notok "a stopped VM has no VNC line" "exit $got: $out"
+ fi
+else
+ skip 4 "this qemu cannot do GRAPHICS=vnc"
+fi
+
+# One directory per VM means two guests are independent of each other:
+# each names its own process, and killing one leaves the other alone.
+"$VMM" create bt5 64M >/dev/null 2>&1
+"$VMM" create bt6 64M >/dev/null 2>&1
+for v in bt5 bt6; do
+ cfg "$v" <<CFG
+MEM=256
+CPUS=1
+CFG
+ "$VMM" start "$v" >/dev/null 2>&1 || :
+done
+p5=$(cat "$VMMDIR/bt5/pid" 2>/dev/null || echo)
+p6=$(cat "$VMMDIR/bt6/pid" 2>/dev/null || echo)
+holds "two guests run at once, as two processes" \
+ test -n "$p5" -a -n "$p6" -a "$p5" != "$p6"
+"$VMM" kill bt5 >/dev/null 2>&1 || :
+exits 3 "killing one stops that one" status bt5
+exits 0 "and leaves the other running" status bt6
+"$VMM" kill bt6 >/dev/null 2>&1 || :