@@ -6,5 +6,10 @@
#
# Usage: test_sunshine_qemu.sh [test-name...] (default: every test_* function)
# Prints "ok <name>", "skip <name>: <reason>" or "FAIL <name>: <reason>" per test; exits 1 on any failure.
# A test that sets a failure message fails even when it returns 0.
#
# Environment:
# SUNSHINE_QEMU_PKG_REQUIRE_TOOLS 1 turns skips (systemd-analyze or shellcheck missing) into failures,
# for CI where the tools must be there
# shellcheck disable=SC2329 # test functions are called by name from the runner at the end
set -uo pipefail
@@ -86,7 +91,10 @@
local unit
for unit in "${pkg}/sunshine-qemu@.service" "${pkg}/user/sunshine-qemu@.service"; do
grep -qE '^StateDirectory=sunshine-qemu/%i$' "${unit}" || fail "$(basename "$(dirname "${unit}")")/$(basename "${unit}"): no per-instance StateDirectory" || return 1
grep -qE '^Environment=XDG_CONFIG_HOME=%S/sunshine-qemu/%i$' "${unit}" || fail "${unit}: Sunshine's appdata isn't per instance" || return 1
grep -qE '^Environment=CONFIGURATION_DIRECTORY=%S/sunshine-qemu/%i$' "${unit}" || fail "${unit}: Sunshine's appdata isn't per instance" || return 1
# XDG_CONFIG_HOME would be inherited by virsh and libvirt too (a session libvirt they start would
# read its domains from the per-VM directory)
! grep -qE '^Environment=XDG_CONFIG_HOME=' "${unit}" || fail "${unit}: sets XDG_CONFIG_HOME for every child process" || return 1
grep -qE '^ExecStart=/usr/bin/sunshine (/etc|%E)/sunshine-qemu/%I\.conf$' "${unit}" || fail "${unit}: ExecStart doesn't read the instance's config" || return 1
grep -qE '^ExecStartPre=/usr/lib/sunshine-qemu/sunshine-qemu-wait-vm (/etc|%E)/sunshine-qemu/%I\.conf$' "${unit}" || fail "${unit}: doesn't wait for the VM before Sunshine's startup check" || return 1
grep -qE '^Restart=on-failure$' "${unit}" || fail "${unit}: no Restart=on-failure" || return 1
@@ -151,9 +159,23 @@
# @tag requirements: [REQ-DEP-002]
test_wait_vm_waits_for_the_qmp_socket() {
printf 'capture = qemu\nqemu_dbus_address = qmp:%s/vm.qmp\n' "${tmp}" > "${tmp}/vm.conf"
# a QMP server that greets every client, as QEMU does
(
sleep 1
python3 -c '
import socket, sys, time
s = socket.socket(socket.AF_UNIX)
s.bind(sys.argv[1])
s.listen()
s.settimeout(5)
try:
while True:
c, _ = s.accept()
c.sendall(b"{\"QMP\": {\"version\": {}, \"capabilities\": []}}\r\n")
c.close()
except OSError:
pass
python3 -c 'import socket, sys, time; s = socket.socket(socket.AF_UNIX); s.bind(sys.argv[1]); s.listen(); time.sleep(5)' "${tmp}/vm.qmp"
' "${tmp}/vm.qmp"
) &
local server=$!
local start=${SECONDS} status
@@ -166,6 +188,36 @@
}
# @tag requirements: [REQ-DEP-002]
test_wait_vm_doesnt_take_a_stale_qmp_socket_for_a_running_vm() {
command -v python3 > /dev/null || {
failure="skip: python3 isn't installed"
return 2
}
# QEMU killed: its socket file stays, but nothing accepts connections on it
python3 -c 'import socket, sys; s = socket.socket(socket.AF_UNIX); s.bind(sys.argv[1])' "${tmp}/stale.qmp"
[[ -S "${tmp}/stale.qmp" ]] || fail "couldn't create a stale socket" || return 1
printf 'qemu_dbus_address = qmp:%s/stale.qmp\n' "${tmp}" > "${tmp}/vm.conf"
local status
SUNSHINE_QEMU_WAIT_TIMEOUT=1 SUNSHINE_QEMU_WAIT_INTERVAL=0.1 "${pkg}/sunshine-qemu-wait-vm" "${tmp}/vm.conf" 2> "${tmp}/wait.log"
status=$?
expect_eq "${status}" 1 "exit status for a stale socket ($(cat "${tmp}/wait.log"))" || return 1
# a socket that accepts but never greets (another program) isn't QEMU either
python3 -c 'import socket, sys, time; s = socket.socket(socket.AF_UNIX); s.bind(sys.argv[1]); s.listen(); time.sleep(10)' "${tmp}/silent.qmp" &
local server=$!
for _ in $(seq 1 50); do
[[ -S "${tmp}/silent.qmp" ]] && break
sleep 0.05
done
printf 'qemu_dbus_address = qmp:%s/silent.qmp\n' "${tmp}" > "${tmp}/vm.conf"
SUNSHINE_QEMU_WAIT_TIMEOUT=1 SUNSHINE_QEMU_WAIT_INTERVAL=0.1 "${pkg}/sunshine-qemu-wait-vm" "${tmp}/vm.conf" 2> "${tmp}/wait.log"
status=$?
kill "${server}" 2> /dev/null
wait "${server}" 2> /dev/null
expect_eq "${status}" 1 "exit status for a socket without a QMP greeting"
}
# @tag requirements: [REQ-DEP-002]
test_wait_vm_waits_until_qemu_owns_its_bus_name() {
# the fake gdbus answers "(false,)" for the first two calls
fake_bin gdbus "n=\$(cat '${tmp}/gdbus.count' 2> /dev/null || echo 0); echo \$((n + 1)) > '${tmp}/gdbus.count'; echo \"\$*\" >> '${tmp}/gdbus.log'; if ((n < 2)); then echo '(false,)'; else echo '(true,)'; fi"
@@ -195,7 +247,21 @@
PATH="${tmp}/bin:${PATH}" SUNSHINE_QEMU_WAIT_TIMEOUT=10 SUNSHINE_QEMU_WAIT_INTERVAL=0.1 "${pkg}/sunshine-qemu-wait-vm" "${tmp}/vm.conf" 2> "${tmp}/wait.log"
status=$?
expect_eq "${status}" 0 "exit status ($(cat "${tmp}/wait.log"))" || return 1
expect_eq "$(sort -u "${tmp}/virsh.log")" "-c qemu:///system domstate win11" "virsh calls"
expect_eq "$(sort -u "${tmp}/virsh.log")" "-c qemu:///system domstate win11" "virsh calls" || return 1
# without ?uri= the system libvirt, as Sunshine uses: an unprivileged unit user would get qemu:///session
rm -f "${tmp}/virsh.log"
printf 'qemu_dbus_address = libvirt:win11\n' > "${tmp}/vm.conf"
PATH="${tmp}/bin:${PATH}" SUNSHINE_QEMU_WAIT_TIMEOUT=10 SUNSHINE_QEMU_WAIT_INTERVAL=0.1 "${pkg}/sunshine-qemu-wait-vm" "${tmp}/vm.conf" 2> "${tmp}/wait.log"
status=$?
expect_eq "${status}" 0 "exit status without ?uri= ($(cat "${tmp}/wait.log"))" || return 1
expect_eq "$(sort -u "${tmp}/virsh.log")" "-c qemu:///system domstate win11" "virsh calls without ?uri=" || return 1
# ?uri=qemu:///session for session libvirt
rm -f "${tmp}/virsh.log"
printf 'qemu_dbus_address = libvirt:win11?uri=qemu:///session\n' > "${tmp}/vm.conf"
PATH="${tmp}/bin:${PATH}" SUNSHINE_QEMU_WAIT_TIMEOUT=10 SUNSHINE_QEMU_WAIT_INTERVAL=0.1 "${pkg}/sunshine-qemu-wait-vm" "${tmp}/vm.conf" 2> "${tmp}/wait.log"
expect_eq "$(sort -u "${tmp}/virsh.log")" "-c qemu:///session domstate win11" "virsh calls with ?uri=qemu:///session"
}
# @tag requirements: [REQ-DEP-002]
@@ -218,17 +284,19 @@
printf 'qemu_dbus_address = libvirt:win11\n' > "${tmp}/libvirt.conf"
PATH="${tmp}/nobin" SUNSHINE_QEMU_WAIT_TIMEOUT=1 "$(command -v bash)" "${pkg}/sunshine-qemu-wait-vm" "${tmp}/libvirt.conf" 2> "${tmp}/wait.log" || fail "a libvirt address without virsh must not block: $(cat "${tmp}/wait.log")" || return 1
SUNSHINE_QEMU_WAIT_TIMEOUT=1 "${pkg}/sunshine-qemu-wait-vm" "${tmp}/missing.conf" 2> /dev/null && fail "a missing config must fail"
return 0
if SUNSHINE_QEMU_WAIT_TIMEOUT=1 "${pkg}/sunshine-qemu-wait-vm" "${tmp}/missing.conf" 2> /dev/null; then
fail "a missing config must fail"
return 1
fi
}
# @tag requirements: [REQ-DEP-001]
test_port_helper_allocates_non_overlapping_slots() {
local port="${pkg}/sunshine-qemu-port" etc="${tmp}/etc"
mkdir -p "${etc}"
printf 'capture = qemu\n' > "${etc}/a.conf"
printf 'capture = qemu\n' > "${etc}/b.conf"
printf 'port = 48150\n' > "${etc}/manual.conf"
printf 'capture = qemu\nsunshine_name = a\n' > "${etc}/a.conf"
printf 'capture = qemu\nsunshine_name = b' > "${etc}/b.conf" # no newline at the end
printf 'port = 48150\nsunshine_name = manual\n' > "${etc}/manual.conf"
expect_eq "$("${port}" --dir "${etc}" a)" 48089 "first VM, while b has no port yet (counts as 47989)" || return 1
expect_eq "$("${port}" --dir "${etc}" a)" 48089 "same answer again" || return 1
@@ -238,20 +306,65 @@
expect_eq "$(grep -c '^port' "${etc}/a.conf")" 1 "--write doesn't add a second port" || return 1
# slot 1 is taken by a; 48189 (48184-48210) doesn't overlap manual.conf's 48150 (48145-48171)
expect_eq "$("${port}" --dir "${etc}" --write b)" 48189 "second VM skips the used slot" || return 1
expect_eq "$(tail -n 2 "${etc}/b.conf")" $'sunshine_name = b\nport = 48189' "--write after a last line without a newline" || return 1
"${port}" --dir "${etc}" --check || fail "--check failed on non-overlapping ports" || return 1
printf 'port = 48200\nsunshine_name = c\n' > "${etc}/c.conf"
printf 'port = 48200\n' > "${etc}/c.conf"
local output
if output="$("${port}" --dir "${etc}" --check 2>&1)"; then
fail "--check passed with 48189 and 48200 overlapping"
output="$("${port}" --dir "${etc}" --check 2>&1)" && fail "--check passed with 48189 and 48200 overlapping" || true
return 1
fi
[[ "${output}" == *"overlaps"* ]] || fail "--check didn't name the overlap: ${output}" || return 1
expect_eq "$("${port}" --dir "${etc}" d)" 48289 "a new VM skips every overlapping slot" || return 1
if "${port}" --dir "${etc}" "../x" 2> /dev/null; then
"${port}" --dir "${etc}" "../x" 2> /dev/null && fail "accepted a path as VM name"
printf 'port = 65530\n' > "${etc}/c.conf"
"${port}" --dir "${etc}" --check 2> /dev/null && fail "--check accepted a range above 65535"
return 0
fail "accepted a path as VM name"
return 1
fi
printf 'port = 65530\nsunshine_name = c\n' > "${etc}/c.conf"
if "${port}" --dir "${etc}" --check 2> /dev/null; then
fail "--check accepted a range above 65535"
return 1
fi
}
# @tag requirements: [REQ-DEP-001]
test_port_helper_checks_that_names_differ() {
local port="${pkg}/sunshine-qemu-port" etc="${tmp}/etc"
mkdir -p "${etc}"
printf 'port = 48089\nsunshine_name = Gaming VM (1)\n' > "${etc}/a.conf"
printf 'port = 48189\nsunshine_name = Gaming VM (2)\n' > "${etc}/b.conf"
"${port}" --dir "${etc}" --check || fail "--check failed on distinct names" || return 1
local output
printf 'port = 48189\nsunshine_name = gaming vm (1) \n' > "${etc}/b.conf"
if output="$("${port}" --dir "${etc}" --check 2>&1)"; then
fail "--check passed with the same name in two configs (mDNS ignores case)"
return 1
fi
[[ "${output}" == *"same sunshine_name"* ]] || fail "--check didn't name the duplicate: ${output}" || return 1
# configs without sunshine_name all advertise the host name
printf 'port = 48089\n' > "${etc}/a.conf"
printf 'port = 48189\n' > "${etc}/b.conf"
if "${port}" --dir "${etc}" --check 2> /dev/null; then
fail "--check passed with two configs without sunshine_name"
return 1
fi
}
# @tag requirements: [REQ-DEP-001]
test_port_helper_allocates_for_a_copy_of_the_example_config() {
local port="${pkg}/sunshine-qemu-port" etc="${tmp}/etc"
mkdir -p "${etc}"
sed -e 's/^sunshine_name = .*/sunshine_name = vm-a/' "${pkg}/example.conf" > "${etc}/vm-a.conf"
sed -e 's/^sunshine_name = .*/sunshine_name = vm-b/' "${pkg}/example.conf" > "${etc}/vm-b.conf"
expect_eq "$("${port}" --dir "${etc}" --write vm-a)" 48089 "first copy" || return 1
expect_eq "$("${port}" --dir "${etc}" --write vm-b)" 48189 "second copy" || return 1
"${port}" --dir "${etc}" --check || fail "--check failed on two copies of example.conf"
}
# @tag requirements: [REQ-DEP-002]
test_scripts_are_shellcheck_clean() {
[[ -x "${shellcheck_bin}" ]] || {
@@ -273,6 +386,13 @@
failure=""
"${name}"
status=$?
if [[ ${status} == 0 && -n "${failure}" ]]; then
status=1
fi
if [[ ${status} == 2 && "${SUNSHINE_QEMU_PKG_REQUIRE_TOOLS:-0}" == 1 ]]; then
failure="${failure#skip: } (SUNSHINE_QEMU_PKG_REQUIRE_TOOLS=1)"
status=1
fi
case "${status}" in
0) echo "ok ${name}" ;;
2) echo "skip ${name}: ${failure#skip: }" ;;