diff --git a/test/shell.d/bar-icon-geometry-test.sh b/test/shell.d/bar-icon-geometry-test.sh index cbdd8da3..1bfcdf0c 100644 --- a/test/shell.d/bar-icon-geometry-test.sh +++ b/test/shell.d/bar-icon-geometry-test.sh @@ -4,10 +4,7 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping bar icon geometry test" - exit 0 -fi +require_compositor "bar icon geometry test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping bar icon geometry test" diff --git a/test/shell.d/bar-widget-contract-test.sh b/test/shell.d/bar-widget-contract-test.sh index 5d985680..95fd52b9 100755 --- a/test/shell.d/bar-widget-contract-test.sh +++ b/test/shell.d/bar-widget-contract-test.sh @@ -18,10 +18,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping bar widget contract test" - exit 0 -fi +require_compositor "bar widget contract test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping bar widget contract test" diff --git a/test/shell.d/base-test.sh b/test/shell.d/base-test.sh index 7bb019a7..9de42cf3 100644 --- a/test/shell.d/base-test.sh +++ b/test/shell.d/base-test.sh @@ -29,6 +29,53 @@ require_command() { command -v "$command" >/dev/null || fail "required command is available: $command" } +# WAYLAND_DISPLAY proves the variable was inherited, not that the compositor +# answers. Sandboxes pass the environment through while blocking +# $XDG_RUNTIME_DIR, so Quickshell clears a bare variable check and then aborts +# inside QGuiApplication, before any QML loads: a core dump per launch where a +# skip belonged. Probe the socket, then Hyprland itself, since a compositor that +# died mid-session can leave its socket behind. +compositor_reachable() { + local socket=${WAYLAND_DISPLAY:-} + + [[ -n $socket ]] || return 1 + [[ $socket == /* ]] || socket=${XDG_RUNTIME_DIR:-}/$socket + [[ -S $socket ]] || return 1 + + # A compositor that died can leave its socket behind, so ask Hyprland whether + # it is still answering. Only when it can be asked: hyprctl needs + # HYPRLAND_INSTANCE_SIGNATURE, and treating a missing signature as a dead + # compositor would skip tests that would have run fine. + [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]] || return 0 + + # Hyprland can miss a query while it reconfigures outputs, and one miss is not + # a dead compositor; retry the way omarchy-launch-shell does rather than + # discard a whole file's runtime coverage. Only a leftover socket gets this + # far, so the waiting is rare. + local attempt + for attempt in 1 2 3; do + hyprctl -j monitors >/dev/null 2>&1 && return 0 + (( attempt < 3 )) && sleep 0.5 + done + + return 1 +} + +require_compositor() { + local description="$1" + + if compositor_reachable; then + # No probe outruns a compositor that dies mid-run, and Quickshell leaves + # through qFatal() when its connection drops. Keep that abort from writing a + # core; the test still fails, just without the debris. + ulimit -c 0 2>/dev/null || true + return 0 + fi + + pass "no Wayland compositor; skipping $description" + exit 0 +} + run_node_test() { require_command node diff --git a/test/shell.d/button-border-stability-test.sh b/test/shell.d/button-border-stability-test.sh index 6a1b9eee..a120f8ef 100644 --- a/test/shell.d/button-border-stability-test.sh +++ b/test/shell.d/button-border-stability-test.sh @@ -17,10 +17,7 @@ assert( ) JS -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping Button hover geometry runtime test" - exit 0 -fi +require_compositor "Button hover geometry runtime test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping Button hover geometry runtime test" diff --git a/test/shell.d/compositor-guard-test.sh b/test/shell.d/compositor-guard-test.sh new file mode 100644 index 00000000..d7525a12 --- /dev/null +++ b/test/shell.d/compositor-guard-test.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +require_command python3 + +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +runtime_dir="$test_dir/run" +stub_bin="$test_dir/bin" +mkdir -p "$runtime_dir" "$stub_bin" + +# A bound AF_UNIX socket outlives the process that bound it, which is exactly the +# corpse a dead compositor leaves behind. +python3 -c 'import socket, sys; socket.socket(socket.AF_UNIX).bind(sys.argv[1])' "$runtime_dir/wayland-1" + +attempts_log="$test_dir/hyprctl-attempts" + +stub_hyprctl() { + : >"$attempts_log" + cat >"$stub_bin/hyprctl" <>"$attempts_log" +exit $1 +STUB + chmod +x "$stub_bin/hyprctl" +} + +# Answers only from the second query on, the way Hyprland behaves while it is +# busy reconfiguring outputs. +stub_flaky_hyprctl() { + : >"$attempts_log" + cat >"$stub_bin/hyprctl" <>"$attempts_log" +(( \$(grep -c asked "$attempts_log") > 1 )) +STUB + chmod +x "$stub_bin/hyprctl" +} + +attempts() { + grep -c asked "$attempts_log" || true +} + +# The guard exits the shell it runs in, so run it in a child and report back what +# it did: the skip line, or the core limit it left behind for Quickshell. +run_guard() { + env "$@" PATH="$stub_bin:$PATH" bash -c ' + source "$1/base-test.sh" + ulimit -c unlimited 2>/dev/null || true + require_compositor "sample runtime test" + printf "launched with core limit %s\n" "$(ulimit -c)" + ' bash "$SHELL_TEST_DIR" 2>&1 || printf 'guard exited %s\n' "$?" +} + +skipped="ok - no Wayland compositor; skipping sample runtime test" + +output=$(run_guard -u WAYLAND_DISPLAY XDG_RUNTIME_DIR="$runtime_dir") +[[ $output == "$skipped" ]] || fail "guard skips without a display" "$output" +pass "guard skips without a display" + +# The sandbox shape from the bug report: the variable comes through, the runtime +# directory does not. +output=$(run_guard WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR="$test_dir/blocked") +[[ $output == "$skipped" ]] || fail "guard skips when the socket is unreachable" "$output" +pass "guard skips when the socket is unreachable" + +# Hyprland can miss a single query mid-reconfigure, so only a compositor that +# stays silent counts as gone. +stub_hyprctl 1 +output=$(run_guard WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR="$runtime_dir" HYPRLAND_INSTANCE_SIGNATURE=test) +[[ $output == "$skipped" ]] || fail "guard skips when the compositor stopped answering" "$output" +pass "guard skips when the compositor stopped answering" + +[[ $(attempts) == 3 ]] || fail "guard retries a silent compositor before giving up" "asked $(attempts) times" +pass "guard retries a silent compositor before giving up" + +stub_flaky_hyprctl +output=$(run_guard WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR="$runtime_dir" HYPRLAND_INSTANCE_SIGNATURE=test) +[[ $output == "launched with core limit 0" ]] || fail "guard rides out a missed query" "$output" +pass "guard rides out a missed query" + +# Without a signature there is nothing to ask, and a live socket is all the +# evidence there is: run rather than skip real coverage. +stub_hyprctl 1 +output=$(run_guard -u HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR="$runtime_dir") +[[ $output == "launched with core limit 0" ]] || fail "guard runs when hyprctl cannot be asked" "$output" +pass "guard runs when hyprctl cannot be asked" + +[[ $(attempts) == 0 ]] || fail "guard leaves hyprctl alone without a signature" "asked $(attempts) times" +pass "guard leaves hyprctl alone without a signature" + +# Quickshell aborts if the compositor disappears mid-run, so the tests it is +# about to launch must not be able to dump core. +stub_hyprctl 0 +output=$(run_guard WAYLAND_DISPLAY=wayland-1 XDG_RUNTIME_DIR="$runtime_dir" HYPRLAND_INSTANCE_SIGNATURE=test) +[[ $output == "launched with core limit 0" ]] || fail "guard runs with core dumps disabled" "$output" +pass "guard runs with core dumps disabled" diff --git a/test/shell.d/indicator-contract-test.sh b/test/shell.d/indicator-contract-test.sh index e47d90c2..06f3f9cf 100755 --- a/test/shell.d/indicator-contract-test.sh +++ b/test/shell.d/indicator-contract-test.sh @@ -18,10 +18,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping QML contract test" - exit 0 -fi +require_compositor "QML contract test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping QML contract test" diff --git a/test/shell.d/lock-fingerprint-indicator-test.sh b/test/shell.d/lock-fingerprint-indicator-test.sh index 89c3db28..fef29783 100755 --- a/test/shell.d/lock-fingerprint-indicator-test.sh +++ b/test/shell.d/lock-fingerprint-indicator-test.sh @@ -18,10 +18,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping lock fingerprint indicator test" - exit 0 -fi +require_compositor "lock fingerprint indicator test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping lock fingerprint indicator test" diff --git a/test/shell.d/lock-password-overflow-test.sh b/test/shell.d/lock-password-overflow-test.sh index 916a1b23..5541882a 100755 --- a/test/shell.d/lock-password-overflow-test.sh +++ b/test/shell.d/lock-password-overflow-test.sh @@ -18,10 +18,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping lock password overflow test" - exit 0 -fi +require_compositor "lock password overflow test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping lock password overflow test" diff --git a/test/shell.d/manifest-entrypoints-test.sh b/test/shell.d/manifest-entrypoints-test.sh index b8b91ce1..0172aa84 100755 --- a/test/shell.d/manifest-entrypoints-test.sh +++ b/test/shell.d/manifest-entrypoints-test.sh @@ -18,10 +18,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping manifest entrypoint load test" - exit 0 -fi +require_compositor "manifest entrypoint load test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping manifest entrypoint load test" diff --git a/test/shell.d/plugin-registry-contract-test.sh b/test/shell.d/plugin-registry-contract-test.sh index 8c073381..38964043 100755 --- a/test/shell.d/plugin-registry-contract-test.sh +++ b/test/shell.d/plugin-registry-contract-test.sh @@ -18,10 +18,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping plugin registry contract test" - exit 0 -fi +require_compositor "plugin registry contract test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping plugin registry contract test" diff --git a/test/shell.d/pointer-move-gate-test.sh b/test/shell.d/pointer-move-gate-test.sh index 1a4192cf..048f5822 100644 --- a/test/shell.d/pointer-move-gate-test.sh +++ b/test/shell.d/pointer-move-gate-test.sh @@ -51,10 +51,7 @@ assert( ) JS -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping pointer movement gate runtime test" - exit 0 -fi +require_compositor "pointer movement gate runtime test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping pointer movement gate runtime test" diff --git a/test/shell.d/runtime-smoke-test.sh b/test/shell.d/runtime-smoke-test.sh index 42d9746f..f9e52bfd 100755 --- a/test/shell.d/runtime-smoke-test.sh +++ b/test/shell.d/runtime-smoke-test.sh @@ -17,10 +17,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping shell runtime smoke test" - exit 0 -fi +require_compositor "shell runtime smoke test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping shell runtime smoke test" diff --git a/test/shell.d/screenshot-sanity-test.sh b/test/shell.d/screenshot-sanity-test.sh index d2fd24b2..636642ed 100755 --- a/test/shell.d/screenshot-sanity-test.sh +++ b/test/shell.d/screenshot-sanity-test.sh @@ -18,10 +18,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping screenshot sanity test" - exit 0 -fi +require_compositor "screenshot sanity test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping screenshot sanity test" diff --git a/test/shell.d/tray-menu-test.sh b/test/shell.d/tray-menu-test.sh index 1b2fba92..514c174c 100644 --- a/test/shell.d/tray-menu-test.sh +++ b/test/shell.d/tray-menu-test.sh @@ -22,10 +22,7 @@ cleanup() { } trap cleanup EXIT -if [[ -z ${WAYLAND_DISPLAY:-} ]]; then - pass "no Wayland compositor; skipping tray menu activation test" - exit 0 -fi +require_compositor "tray menu activation test" if ! command -v quickshell >/dev/null 2>&1; then pass "quickshell not installed; skipping tray menu activation test"