ref:main
#!/usr/bin/env bash
# @tag requirements: [REQ-E2E-001, REQ-NFR-001]
# End-to-end test (REQ-E2E-001): an unmodified Moonlight client streams a QEMU VM that has no guest agent.
#
# 1. start a private dbus-daemon and a QEMU guest that draws a known pattern (run_vm.sh)
# 2. start Sunshine with capture = qemu on a unique port (software encoding unless E2E_ENCODER is set)
# 3. pair a headless Moonlight client (PIN approved through Sunshine's web API), launch Desktop
# 4. decode the stream and check the quadrant colors of the guest pattern
# 5. tear everything down, also on failure
#
# Environment:
# SUNSHINE_BIN Sunshine binary (default: <repo>/cmake-build-debug/sunshine)
# CLIENT_BIN E2E client (default: <repo>/cmake-build-e2e-client/moonlight_e2e_client)
# QEMU QEMU binary (default: qemu-system-x86_64)
# E2E_CACHE cache for the guest image (default: ~/.cache/sunshine-qemu/e2e)
# E2E_ARTIFACTS where logs and the last frame go (default: <work-dir>/artifacts, kept on failure)
# E2E_PORT Sunshine base port (default: random in 21000-39900, step 100)
# E2E_TIMEOUT stream timeout in seconds (default: 90; raise it for TCG)
# E2E_WIDTH/E2E_HEIGHT/E2E_FPS stream mode (default: 1280x800 at 30 fps)
# E2E_FRAMES decoded frames to receive before checking (default: 30; raise for latency runs)
# E2E_ENCODER Sunshine encoder: software, nvenc, vaapi, vulkan (default: software)
# E2E_KEEP set to 1 to keep the work directory
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo="$(cd "${script_dir}/../../.." && pwd)"
sunshine_bin="${SUNSHINE_BIN:-${repo}/cmake-build-debug/sunshine}"
client_bin="${CLIENT_BIN:-${repo}/cmake-build-e2e-client/moonlight_e2e_client}"
cache="${E2E_CACHE:-${HOME}/.cache/sunshine-qemu/e2e}"
port="${E2E_PORT:-$((21000 + (RANDOM % 190) * 100))}"
timeout_s="${E2E_TIMEOUT:-90}"
width="${E2E_WIDTH:-1280}"
height="${E2E_HEIGHT:-800}"
fps="${E2E_FPS:-30}"
frames="${E2E_FRAMES:-30}"
encoder="${E2E_ENCODER:-software}"
for bin in "${sunshine_bin}" "${client_bin}"; do
if [[ ! -x "${bin}" ]]; then
echo "missing ${bin}; build it first" >&2
exit 2
fi
done
work="$(mktemp -d "${TMPDIR:-/tmp}/sunshine-e2e.XXXXXX")"
artifacts="${E2E_ARTIFACTS:-${work}/artifacts}"
mkdir -p "${artifacts}"
result=1
cleanup() {
set +e
for pidfile in "${work}/sunshine.pid" "${work}/vm/qemu.pid" "${work}/vm/dbus.pid"; do
if [[ -f "${pidfile}" ]]; then
pid="$(cat "${pidfile}")"
kill "${pid}" 2>/dev/null
for _ in $(seq 1 50); do
kill -0 "${pid}" 2>/dev/null || break
sleep 0.1
done
kill -9 "${pid}" 2>/dev/null
fi
done
cp -f "${work}/sunshine.log" "${work}/sunshine.stdout" "${work}/client.log" "${work}/vm/qemu.log" "${work}/vm/dbus.log" "${artifacts}/" 2>/dev/null
if [[ ${result} == 0 && "${E2E_KEEP:-0}" != 1 && -z "${E2E_ARTIFACTS:-}" ]]; then
rm -rf "${work}"
else
echo "e2e: work directory kept at ${work} (artifacts: ${artifacts})" >&2
fi
}
trap cleanup EXIT
# 1. guest
image="${cache}/pattern-$(sha256sum "${script_dir}/guest/pattern.S" | cut -c1-16).img"
if [[ ! -f "${image}" ]]; then
"${script_dir}/guest/build_guest.sh" "${image}" > /dev/null
fi
bus_address="$("${script_dir}/run_vm.sh" "${work}/vm" "${image}")"
echo "e2e: VM up on ${bus_address}" >&2
# 2. Sunshine
cat > "${work}/apps.json" <<'EOF'
{
"env": {},
"apps": [
{"name": "Desktop", "image-path": "desktop.png"}
]
}
EOF
cat > "${work}/sunshine.conf" <<EOF
capture = qemu
qemu_dbus_address = ${bus_address}
encoder = ${encoder}
port = ${port}
sunshine_name = sunshine-e2e-${port}
file_state = ${work}/sunshine_state.json
credentials_file = ${work}/sunshine_credentials.json
pkey = ${work}/sunshine.key
cert = ${work}/sunshine.crt
file_apps = ${work}/apps.json
log_path = ${work}/sunshine.log
min_log_level = debug
upnp = disabled
system_tray = disabled
origin_web_ui_allowed = pc
notify_pre_releases = disabled
EOF
api_user="e2e"
api_pass="$(head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n')"
"${sunshine_bin}" "${work}/sunshine.conf" --creds "${api_user}" "${api_pass}" > "${work}/creds.log" 2>&1
"${sunshine_bin}" "${work}/sunshine.conf" > "${work}/sunshine.stdout" 2>&1 &
echo $! > "${work}/sunshine.pid"
for _ in $(seq 1 300); do
if ! kill -0 "$(cat "${work}/sunshine.pid")" 2>/dev/null; then
echo "e2e: Sunshine exited early" >&2
tail -50 "${work}/sunshine.stdout" >&2
exit 1
fi
if curl -s --max-time 1 "http://127.0.0.1:${port}/serverinfo" | grep -q "status_code=\"200\""; then
break
fi
sleep 0.1
done
echo "e2e: Sunshine up on port ${port}" >&2
# 3-4. pair, stream, check the pattern (red, green, blue, white quadrants)
set +e
"${client_bin}" \
--port "${port}" \
--api-user "${api_user}" \
--api-pass "${api_pass}" \
--state-dir "${work}/client" \
--width "${width}" --height "${height}" --fps "${fps}" \
--frames "${frames}" \
--timeout "${timeout_s}" \
--expect "255,0,0;0,255,0;0,0,255;255,255,255" \
--tolerance 48 \
--out "${artifacts}/last_frame.ppm" \
--summary "${artifacts}/summary.json" \
2> "${work}/client.log"
result=$?
set -e
if [[ ${result} == 0 ]]; then
echo "e2e: PASS $(cat "${artifacts}/summary.json")"
else
echo "e2e: FAIL (client exit ${result})" >&2
tail -40 "${work}/client.log" >&2
fi
exit "${result}"