#!/usr/bin/env bash set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" service="$repo_dir/config/dot/quickshell/services/Wallpaper.qml" scanner="$repo_dir/config/dot/quickshell/scripts/panama-wallpaper-scan" fixture="$(mktemp -d /tmp/panama-wallpaper-service.XXXXXX)" config_home="$fixture/config" state_home="$fixture/state" config_path="$config_home/quickshell" harness="$config_path/wallpaper-service-harness.qml" test_bin="$fixture/bin" command_log="$fixture/commands.log" active_state="$fixture/active.json" control="$fixture/control" shell_log="$fixture/quickshell.log" harness_pid="" quoted_home="$fixture/home's" quoted_wallpaper="$quoted_home/Pictures/Wallpapers/quoted.jpg" bulk_wallpapers="$fixture/bulk-wallpapers" bulk_segment="wallpaper-root-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" fail() { printf 'wallpaper service contract: %s\n' "$1" >&2 [[ -s "$shell_log" ]] && sed -n '1,180p' "$shell_log" >&2 exit 1 } for needle in 'property var activeByOutput' 'function applyPolicy' 'function setSingle'; do rg -Fq "$needle" "$service" || fail "Wallpaper service is missing $needle" done instances_for_harness() { qs list --all 2>/dev/null | awk -v expected="$harness" ' /^Instance / { pid = "" } /^[[:space:]]*Process ID:/ { pid = $3 } /^[[:space:]]*Config path:/ { path = $0 sub(/^[[:space:]]*Config path: /, "", path) if (path == expected && pid ~ /^[0-9]+$/) print pid } ' } cleanup() { if [[ "$harness_pid" =~ ^[0-9]+$ ]] && kill -0 "$harness_pid" 2>/dev/null; then kill "$harness_pid" 2>/dev/null || true fi rm -rf "$fixture" } trap cleanup EXIT for _ in $(seq 1 15); do bulk_wallpapers="$bulk_wallpapers/$bulk_segment" done mkdir -p "$config_home/panama" "$state_home" "$test_bin" \ "$quoted_home/Pictures/Wallpapers" "$bulk_wallpapers" cp -a "$repo_dir/config/dot/quickshell" "$config_path" printf 'quoted wallpaper\n' >"$quoted_wallpaper" for index in $(seq 1 401); do wallpaper="$bulk_wallpapers/wallpaper-$index.jpg" printf 'bulk wallpaper %s\n' "$index" >"$wallpaper" touch -d "@$((1700000000 + index))" "$wallpaper" done if ! bulk_scan="$("$scanner" "$bulk_wallpapers")"; then fail 'a normal scan with more than 60 images exited non-zero' fi expected_bulk_scan="$(for index in $(seq 401 -1 342); do printf '%s\n' "$bulk_wallpapers/wallpaper-$index.jpg" done)" [[ "$bulk_scan" == "$expected_bulk_scan" ]] \ || fail "over-cap scan did not emit exactly the newest 60 images: $bulk_scan" cat >"$config_home/panama/settings.json" <<'JSON' { "displays": { "DP-2": {"mode":"4500x3000@60.00","scale":1.5,"transform":0,"x":0,"y":0,"primary":true}, "HDMI-A-1": {"mode":"2560x1440@60.00","scale":1,"transform":0,"x":3000,"y":0,"primary":false} } } JSON printf '%s\n' '{}' >"$active_state" : >"$command_log" : >"$control" cat >"$test_bin/hyprctl" <<'EOF' #!/usr/bin/env bash set -euo pipefail printf '%s' 'hyprctl' >>"$PANAMA_WALLPAPER_COMMAND_LOG" printf ' %s' "$@" >>"$PANAMA_WALLPAPER_COMMAND_LOG" printf '\n' >>"$PANAMA_WALLPAPER_COMMAND_LOG" if [[ "${1:-}" == "hyprpaper" && "${2:-}" == "wallpaper" ]]; then request="${3:-}" output="${request%%,*}" path="${request#*,}" if [[ "$(<"$PANAMA_WALLPAPER_CONTROL")" == fail-second && "$output" == HDMI-A-1 ]]; then exit 7 fi next="$(jq -c --arg output "$output" --arg path "$path" '. + {($output):$path}' \ "$PANAMA_WALLPAPER_ACTIVE_STATE")" printf '%s\n' "$next" >"$PANAMA_WALLPAPER_ACTIVE_STATE" exit 0 fi if [[ "${1:-}" == "hyprpaper" && "${2:-}" == "listactive" ]]; then if [[ "$(<"$PANAMA_WALLPAPER_CONTROL")" == wrong ]]; then printf 'DP-2: /images/wrong.jpg\nHDMI-A-1: /images/b.jpg\n' exit 0 fi jq -r 'to_entries[] | "\(.key): \(.value)"' "$PANAMA_WALLPAPER_ACTIVE_STATE" exit 0 fi exit 91 EOF chmod +x "$test_bin/hyprctl" qs_for_harness() { env_args=( XDG_CONFIG_HOME="$config_home" XDG_STATE_HOME="$state_home" PATH="$test_bin:$PATH" HOME="$quoted_home" PANAMA_WALLPAPER_COMMAND_LOG="$command_log" PANAMA_WALLPAPER_ACTIVE_STATE="$active_state" PANAMA_WALLPAPER_CONTROL="$control" ) if [[ "$harness_pid" =~ ^[0-9]+$ && "${1:-}" == "ipc" ]]; then env "${env_args[@]}" qs -p "$harness" ipc --pid "$harness_pid" "${@:2}" else env "${env_args[@]}" qs -p "$harness" "$@" fi } wait_idle() { local status="" for _ in $(seq 1 80); do status="$(qs_for_harness ipc call wallpaper-service-test status)" [[ "$(jq -r '.busy' <<<"$status")" == false ]] && { printf '%s' "$status"; return; } sleep 0.1 done fail "wallpaper transaction did not settle: $status" } wait_for_scan() { local status="" for _ in $(seq 1 80); do status="$(qs_for_harness ipc call wallpaper-service-test status)" jq -e --arg path "$quoted_wallpaper" \ '.scanning == false and (.available | index($path) != null)' \ <<<"$status" >/dev/null && return sleep 0.1 done fail "argument-safe wallpaper scan did not discover the quoted HOME image: $status" } qs_for_harness --daemonize >"$shell_log" 2>&1 || fail 'isolated service harness did not launch' for _ in $(seq 1 60); do harness_pid="$(instances_for_harness | head -1)" if [[ "$harness_pid" =~ ^[0-9]+$ ]] \ && qs_for_harness ipc show 2>/dev/null | rg -q '^target wallpaper-service-test$'; then break fi sleep 0.1 done [[ "$harness_pid" =~ ^[0-9]+$ ]] || fail 'isolated service harness process did not start' wait_for_scan wait_idle >/dev/null : >"$command_log" printf '%s\n' '{}' >"$active_state" qs_for_harness ipc call wallpaper-service-test applyPerMonitor >/dev/null success="$(wait_idle)" expected_calls=$'hyprctl hyprpaper wallpaper DP-2,/images/a.jpg\nhyprctl hyprpaper wallpaper HDMI-A-1,/images/b.jpg\nhyprctl hyprpaper listactive' [[ "$(<"$command_log")" == "$expected_calls" ]] || fail "transaction argv/order was wrong: $(<"$command_log")" jq -e '.lastError == "" and .mode == "per-monitor" and .path == "/images/a.jpg" and .assignments == {"HDMI-A-1":"/images/b.jpg"} and .activeByOutput == {"DP-2":"/images/a.jpg","HDMI-A-1":"/images/b.jpg"}' \ <<<"$success" >/dev/null || fail "verified policy was not persisted: $success" printf 'wrong\n' >"$control" : >"$command_log" qs_for_harness ipc call wallpaper-service-test applySingle /images/c.jpg >/dev/null wrong="$(wait_idle)" jq -e '.path == "/images/a.jpg" and .mode == "per-monitor" and .lastError == "Hyprpaper did not confirm that background."' \ <<<"$wrong" >/dev/null || fail "wrong readback persisted an unverified policy: $wrong" printf 'fail-second\n' >"$control" : >"$command_log" qs_for_harness ipc call wallpaper-service-test applyPerMonitor >/dev/null failed="$(wait_idle)" jq -e '.path == "/images/a.jpg" and .mode == "per-monitor" and .lastError == "Hyprpaper did not apply that background."' \ <<<"$failed" >/dev/null || fail "partial output failure changed policy: $failed" [[ "$(tail -1 "$command_log")" == 'hyprctl hyprpaper wallpaper HDMI-A-1,/images/b.jpg' ]] \ || fail 'transaction continued after the failed output' printf '\n' >"$control" printf '%s\n' '{}' >"$active_state" : >"$command_log" qs_for_harness ipc call wallpaper-service-test seedSlideshow \ ' ["/images/a.jpg","/images/b.jpg","/images/c.jpg"]' false >/dev/null seeded="$(qs_for_harness ipc call wallpaper-service-test status)" jq -e '.slideshowTimerRunning == true and .slideshowPath == "/images/a.jpg"' \ <<<"$seeded" >/dev/null || fail "multi-image slideshow did not arm: $seeded" qs_for_harness ipc call wallpaper-service-test advanceSlideshow >/dev/null advanced="$(wait_idle)" jq -e '.mode == "slideshow" and .path == "/images/a.jpg" and .slideshowPath == "/images/b.jpg" and .activeByOutput == {"DP-2":"/images/b.jpg","HDMI-A-1":"/images/b.jpg"}' \ <<<"$advanced" >/dev/null || fail "automatic advance persisted policy or lost runtime state: $advanced" printf 'wrong\n' >"$control" qs_for_harness ipc call wallpaper-service-test advanceSlideshow >/dev/null automatic_failure="$(wait_idle)" jq -e '.path == "/images/a.jpg" and .slideshowPath == "/images/b.jpg" and .slideshowTimerRunning == true and .lastError == "Hyprpaper did not confirm that background."' \ <<<"$automatic_failure" >/dev/null \ || fail "failed automatic advance changed runtime policy or stopped normal scheduling: $automatic_failure" printf '\n' >"$control" qs_for_harness ipc call wallpaper-service-test seedSlideshow ' ["/images/a.jpg"]' false >/dev/null single_item="$(qs_for_harness ipc call wallpaper-service-test status)" jq -e '.slideshowTimerRunning == false' <<<"$single_item" >/dev/null \ || fail "one-item slideshow armed a repeating timer: $single_item" qs_for_harness ipc call wallpaper-service-test applySingle /images/a.jpg >/dev/null wait_idle >/dev/null printf '%s\n' '{}' >"$active_state" : >"$command_log" qs_for_harness ipc call wallpaper-service-test rapidOutputs >/dev/null sleep 0.6 hotplug="$(wait_idle)" [[ "$(rg -c '^hyprctl hyprpaper listactive$' "$command_log")" -eq 1 ]] \ || fail "rapid output changes did not coalesce: $(<"$command_log")" jq -e '.lastError == "" and .activeByOutput == {"DP-2":"/images/a.jpg","HDMI-A-1":"/images/a.jpg"}' \ <<<"$hotplug" >/dev/null || fail "hotplug reapply was not verified: $hotplug" if rg -n 'ReferenceError|TypeError|Binding loop|Unable to assign|Cannot assign' "$shell_log"; then fail 'service harness emitted a QML runtime warning' fi printf 'wallpaper service contract: PASS\n'