A 1500ms budget plus the one 100ms poll interval the trailing sleep can overshoot is exactly 1600ms, which was the bound — but the test measures a whole process around that, so startup pushed real runs to 1602ms. Carry another interval. Two intervals of overshoot, the regression this guards, still trips it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
342 lines
11 KiB
Bash
Executable File
342 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
sleep_lock="$ROOT/bin/omarchy-system-sleep-lock"
|
|
tmpdir=$(mktemp -d)
|
|
trap 'rm -rf "$tmpdir"' EXIT
|
|
|
|
# Each scenario gets its own mock PATH and call log, then runs the sleep lock
|
|
# with a short budget so a stalled shell cannot slow the suite down.
|
|
setup_scenario() {
|
|
scenario_dir="$tmpdir/$1"
|
|
mock_bin="$scenario_dir/bin"
|
|
call_log="$scenario_dir/calls"
|
|
state_dir="$scenario_dir/state"
|
|
notify_log="$scenario_dir/notifications"
|
|
journal_log="$scenario_dir/journal"
|
|
mkdir -p "$mock_bin" "$state_dir"
|
|
: >"$notify_log"
|
|
: >"$journal_log"
|
|
|
|
# The budget is derived from logind, so pin the window rather than letting the
|
|
# host's own configuration decide what these scenarios are testing.
|
|
mock_logind_window 5000000
|
|
|
|
# Capture the desktop warning instead of firing a real one at whoever is
|
|
# running the suite.
|
|
cat >"$mock_bin/omarchy-notification-send" <<SH
|
|
#!/bin/bash
|
|
|
|
printf '%s\n' "\$*" >>"$notify_log"
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-notification-send"
|
|
}
|
|
|
|
mock_logind_window() {
|
|
cat >"$mock_bin/busctl" <<SH
|
|
#!/bin/bash
|
|
|
|
printf 't %s\n' $1
|
|
SH
|
|
chmod +x "$mock_bin/busctl"
|
|
}
|
|
|
|
mock_clamshell() {
|
|
cat >"$mock_bin/omarchy-hyprland-monitor-clamshell" <<SH
|
|
#!/bin/bash
|
|
|
|
echo clamshell >>"\$CALL_LOG"
|
|
sleep ${1:-0}
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-hyprland-monitor-clamshell"
|
|
}
|
|
|
|
# Called with no budget to exercise the value derived from logind's window.
|
|
run_sleep_lock() {
|
|
local args=()
|
|
[[ -n ${1:-} ]] && args=("$1")
|
|
|
|
start_us=${EPOCHREALTIME//[!0-9]/}
|
|
set +e
|
|
CALL_LOG="$call_log" STATE_DIR="$state_dir" PATH="$mock_bin:$PATH" \
|
|
"$sleep_lock" "${args[@]}" 2>"$journal_log"
|
|
exit_status=$?
|
|
set -e
|
|
elapsed_us=$((10#${EPOCHREALTIME//[!0-9]/} - 10#$start_us))
|
|
|
|
mapfile -t calls <"$call_log"
|
|
}
|
|
|
|
# A responsive shell locks immediately, even when the clamshell sync stalls.
|
|
setup_scenario responsive
|
|
cat >"$mock_bin/omarchy-shell" <<'SH'
|
|
#!/bin/bash
|
|
|
|
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
|
if [[ $* == "lock lock" ]]; then
|
|
printf 'ok\n'
|
|
elif [[ $* == "lock status" ]]; then
|
|
printf '{"secure":true}\n'
|
|
fi
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-shell"
|
|
mock_clamshell 2
|
|
|
|
run_sleep_lock 4000
|
|
|
|
(( exit_status == 0 )) ||
|
|
fail "sleep lock succeeds once the session reports secure" "exit: $exit_status"
|
|
pass "sleep lock succeeds once the session reports secure"
|
|
|
|
[[ ${calls[0]} == "shell lock lock" ]] ||
|
|
fail "sleep lock requests the session lock first" "first call: ${calls[0]}"
|
|
pass "sleep lock requests the session lock first"
|
|
|
|
[[ ${calls[1]} == "clamshell" && ${calls[2]} == "shell lock status" ]] ||
|
|
fail "sleep lock checks security after clamshell reconciliation"
|
|
pass "sleep lock checks security after clamshell reconciliation"
|
|
|
|
(( elapsed_us < 1500000 )) ||
|
|
fail "sleep lock bounds a stalled clamshell sync" "elapsed: ${elapsed_us}us"
|
|
pass "sleep lock bounds a stalled clamshell sync"
|
|
|
|
# A shell that never secures the session must give up inside the budget rather
|
|
# than hold logind's delay inhibitor open.
|
|
setup_scenario never_secure
|
|
cat >"$mock_bin/omarchy-shell" <<'SH'
|
|
#!/bin/bash
|
|
|
|
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
|
if [[ $* == "lock lock" ]]; then
|
|
printf 'ok\n'
|
|
elif [[ $* == "lock status" ]]; then
|
|
printf '{"secure":false}\n'
|
|
fi
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-shell"
|
|
mock_clamshell
|
|
|
|
run_sleep_lock 1500
|
|
|
|
(( exit_status != 0 )) ||
|
|
fail "sleep lock reports failure when the session never secures"
|
|
pass "sleep lock reports failure when the session never secures"
|
|
|
|
# The contract is the budget plus at most one poll interval, since the pause
|
|
# between polls is not itself clipped. Derived budgets hold back a full second
|
|
# for logind, so that overshoot is always well inside the reserve.
|
|
#
|
|
# 1500ms budget + a 100ms interval is 1600ms of script time, and this measures a
|
|
# whole process around it, so the bound carries another interval for startup —
|
|
# without it the assertion sits exactly on the worst case and flakes. Two
|
|
# intervals of overshoot, the regression worth catching, is still 1800ms.
|
|
(( elapsed_us <= 1700000 )) ||
|
|
fail "sleep lock gives up within its budget" "elapsed: ${elapsed_us}us"
|
|
pass "sleep lock gives up within its budget"
|
|
|
|
polls=0
|
|
for call in "${calls[@]}"; do
|
|
[[ $call == "shell lock status" ]] && (( ++polls ))
|
|
done
|
|
(( polls > 1 )) ||
|
|
fail "sleep lock keeps polling until the deadline" "polls: $polls"
|
|
pass "sleep lock keeps polling until the deadline"
|
|
|
|
# A lock request that times out may never have landed, so the wait retries it
|
|
# instead of suspending an unlocked session over one slow IPC call.
|
|
setup_scenario retry_lock
|
|
cat >"$mock_bin/omarchy-shell" <<'SH'
|
|
#!/bin/bash
|
|
|
|
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
|
|
|
if [[ $* == "lock lock" ]]; then
|
|
if [[ -f $STATE_DIR/requested ]]; then
|
|
touch "$STATE_DIR/locked"
|
|
printf 'ok\n'
|
|
exit 0
|
|
fi
|
|
touch "$STATE_DIR/requested"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ $* == "lock status" ]]; then
|
|
if [[ -f $STATE_DIR/locked ]]; then
|
|
printf '{"secure":true}\n'
|
|
else
|
|
printf '{"secure":false}\n'
|
|
fi
|
|
fi
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-shell"
|
|
mock_clamshell
|
|
|
|
run_sleep_lock 4000
|
|
|
|
(( exit_status == 0 )) ||
|
|
fail "sleep lock retries a failed lock request" "exit: $exit_status"
|
|
pass "sleep lock retries a failed lock request"
|
|
|
|
requests=0
|
|
for call in "${calls[@]}"; do
|
|
[[ $call == "shell lock lock" ]] && (( ++requests ))
|
|
done
|
|
(( requests == 2 )) ||
|
|
fail "sleep lock stops requesting once the lock lands" "requests: $requests"
|
|
pass "sleep lock stops requesting once the lock lands"
|
|
|
|
# A request can land even when its IPC response times out. Pending status proves
|
|
# that Quickshell is already securing the session, so do not spend the remaining
|
|
# inhibitor budget sending the same request again.
|
|
setup_scenario pending_lock
|
|
cat >"$mock_bin/omarchy-shell" <<'SH'
|
|
#!/bin/bash
|
|
|
|
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
|
|
|
if [[ $* == "lock lock" ]]; then
|
|
exit 1
|
|
fi
|
|
|
|
if [[ $* == "lock status" ]]; then
|
|
if [[ -f $STATE_DIR/pending_seen ]]; then
|
|
printf '{"secure":true}\n'
|
|
else
|
|
touch "$STATE_DIR/pending_seen"
|
|
printf '{"secure":false,"requested":true,"pending":true,"sessionLocked":false}\n'
|
|
fi
|
|
fi
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-shell"
|
|
mock_clamshell
|
|
|
|
run_sleep_lock 4000
|
|
|
|
(( exit_status == 0 )) ||
|
|
fail "sleep lock succeeds after observing a pending lock" "exit: $exit_status"
|
|
pass "sleep lock succeeds after observing a pending lock"
|
|
|
|
requests=0
|
|
for call in "${calls[@]}"; do
|
|
[[ $call == "shell lock lock" ]] && (( ++requests ))
|
|
done
|
|
(( requests == 1 )) ||
|
|
fail "sleep lock does not retry an observed pending lock" "requests: $requests"
|
|
pass "sleep lock does not retry an observed pending lock"
|
|
|
|
# The shell reports a refusal on stdout with a zero exit, so a lock it can never
|
|
# perform has to end the wait instead of burning the rest of the window on it.
|
|
setup_scenario missing_pam
|
|
cat >"$mock_bin/omarchy-shell" <<'SH'
|
|
#!/bin/bash
|
|
|
|
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
|
if [[ $* == "lock lock" ]]; then
|
|
printf 'missing-pam\n'
|
|
fi
|
|
exit 0
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-shell"
|
|
mock_clamshell
|
|
|
|
run_sleep_lock 4000
|
|
|
|
(( exit_status != 0 )) ||
|
|
fail "sleep lock fails fast when the shell cannot lock at all"
|
|
(( elapsed_us < 500000 )) ||
|
|
fail "sleep lock fails fast when the shell cannot lock at all" "elapsed: ${elapsed_us}us"
|
|
pass "sleep lock fails fast when the shell cannot lock at all"
|
|
|
|
[[ ${calls[*]} != *"lock status"* ]] ||
|
|
fail "sleep lock stops polling a shell that refused to lock" "calls: ${calls[*]}"
|
|
pass "sleep lock stops polling a shell that refused to lock"
|
|
|
|
# logind suspends regardless of this exit status, so an unlocked suspend is
|
|
# otherwise invisible. The warning is the only trace the user ever sees, and the
|
|
# journal line is what makes it diagnosable after the fact.
|
|
grep -qF "did not lock before suspend" "$notify_log" ||
|
|
fail "sleep lock warns that the session was left unlocked" \
|
|
"notifications: $(< "$notify_log")"
|
|
pass "sleep lock warns that the session was left unlocked"
|
|
|
|
grep -qF "suspending without a secure lock" "$journal_log" ||
|
|
fail "sleep lock records the unlocked suspend in the journal" \
|
|
"journal: $(< "$journal_log")"
|
|
pass "sleep lock records the unlocked suspend in the journal"
|
|
|
|
# A never-securing shell is the scenario that runs out the whole budget, so it
|
|
# is also the one that shows which budget was derived.
|
|
never_secures() {
|
|
cat >"$mock_bin/omarchy-shell" <<'SH'
|
|
#!/bin/bash
|
|
|
|
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
|
if [[ $* == "lock lock" ]]; then
|
|
printf 'ok\n'
|
|
elif [[ $* == "lock status" ]]; then
|
|
printf '{"secure":false,"requested":true,"pending":true,"sessionLocked":false}\n'
|
|
fi
|
|
SH
|
|
chmod +x "$mock_bin/omarchy-shell"
|
|
mock_clamshell
|
|
}
|
|
|
|
# The drop-in only counts once logind has reloaded it, and a machine can carry
|
|
# its own override, so the budget follows whatever logind actually enforces.
|
|
setup_scenario derived_short_window
|
|
mock_logind_window 2000000
|
|
never_secures
|
|
|
|
run_sleep_lock
|
|
|
|
(( elapsed_us <= 1300000 )) ||
|
|
fail "sleep lock derives its budget from logind's window" "elapsed: ${elapsed_us}us"
|
|
pass "sleep lock derives its budget from logind's window"
|
|
|
|
# Without a readable window there is no way to know what logind will tolerate,
|
|
# so fall back to the budget that was safe before the drop-in existed.
|
|
setup_scenario unreadable_window
|
|
cat >"$mock_bin/busctl" <<'SH'
|
|
#!/bin/bash
|
|
exit 1
|
|
SH
|
|
chmod +x "$mock_bin/busctl"
|
|
never_secures
|
|
|
|
run_sleep_lock
|
|
|
|
(( elapsed_us > 1300000 && elapsed_us <= 4300000 )) ||
|
|
fail "sleep lock falls back to a conservative budget" "elapsed: ${elapsed_us}us"
|
|
pass "sleep lock falls back to a conservative budget when logind cannot be read"
|
|
|
|
# A hand-raised window must not strand a closed laptop awake in a bag. This
|
|
# scenario runs for the whole capped budget by design.
|
|
setup_scenario capped_window
|
|
mock_logind_window 600000000
|
|
never_secures
|
|
|
|
run_sleep_lock
|
|
|
|
(( exit_status != 0 )) ||
|
|
fail "sleep lock caps the budget a huge logind window would allow"
|
|
(( elapsed_us <= 12500000 )) ||
|
|
fail "sleep lock caps the budget a huge logind window would allow" \
|
|
"elapsed: ${elapsed_us}us"
|
|
pass "sleep lock caps the budget a huge logind window would allow"
|
|
|
|
# The cap is only reachable because the shipped drop-in widens logind's window
|
|
# past it. Ship one without the other and the cap is dead weight.
|
|
inhibit_delay=$(sed -n 's/^InhibitDelayMaxSec=//p' "$ROOT/etc/systemd/logind.conf.d/20-inhibit-delay.conf")
|
|
budget_cap_ms=$(sed -n 's/^budget_cap_ms=//p' "$sleep_lock")
|
|
|
|
[[ -n $inhibit_delay && -n $budget_cap_ms ]] ||
|
|
fail "sleep lock cap and logind window are both declared" \
|
|
"window: ${inhibit_delay:-unset} cap: ${budget_cap_ms:-unset}"
|
|
(( budget_cap_ms < inhibit_delay * 1000 )) ||
|
|
fail "sleep lock cap leaves logind room to act" \
|
|
"cap: ${budget_cap_ms}ms window: ${inhibit_delay}s"
|
|
pass "sleep lock cap stays inside the shipped logind inhibitor window"
|