testsAndMisc/linux_configuration/tests/test_shutdown_timer_monitor.sh
Krzysztof kuhy Rudnicki a1f8044a4a Extract system-maintenance, the isolated subsystem
github.com/kuhyx/system-maintenance, cloned to ~/system-maintenance

First of the four live subsystems, and the one with no cross-unit source
edges, so it establishes the pattern for the harder three. Passed in
vmbox first -- the installer ran from an extracted-layout copy in the
guest and detected its distro without any monorepo path -- then on the
host.

Its 21 Python and shell test files lived in linux_configuration/tests/,
reaching bin/ through a shared conftest. Twenty moved with the code and
got their own conftest; the shared one stays, since it serves several
script directories that are not going anywhere.

test_shutdown_timer_monitor.sh stays here. It is a pair test: it checks
that digital_wellbeing's installer template and the monitor it installs
still agree, so it belongs to whichever half owns the installer. It now
reads the monitor from ~/system-maintenance and SKIPs when that checkout
is absent.

An intermediate version of that test fell back to /usr/local/bin, which
HUNG -- the installed copy is stale and differs from source, so the test
was comparing an installer template against a binary it never generated.
Falling back to it would have been worse than skipping, so the fallback
is gone and the comment says why.

Also removed test_hosts_file_monitor.sh from shell-tests.yml's explicit
list: it left with the subsystem, and CI would have failed on the missing
file rather than quietly skipping it.

Verified: 136 tests pass in the new repo, 202 still pass here, the pair
test passes against the checkout and skips without it, and both the shell
coverage gate and shellcheck are clean. Depth 294 -> 267.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:55:29 +02:00

223 lines
7.9 KiB
Bash
Executable File

#!/bin/bash
# Regression tests for shutdown-timer-monitor.sh dispatcher behavior.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
# shutdown-timer-monitor.sh moved to github.com/kuhyx/system-maintenance with
# the rest of that subsystem, but this suite is the pair test: it checks that
# digital_wellbeing's installer and the monitor it installs still agree. Prefer
# a sibling checkout, fall back to the installed copy, and skip rather than
# fail when neither is present -- a missing checkout is not a regression.
TARGET_SCRIPT="${SYSTEM_MAINTENANCE_DIR:-$HOME/system-maintenance}/bin/shutdown-timer-monitor.sh"
# Deliberately NOT falling back to /usr/local/bin: the installed copy can be
# older than the source (it is, on this machine), and testing the installer
# template against a stale binary compares two things that were never meant to
# match. Skip instead -- a missing checkout is not a regression.
if [[ ! -f $TARGET_SCRIPT ]]; then
printf 'SKIP: shutdown-timer-monitor.sh not found (set SYSTEM_MAINTENANCE_DIR)\n'
exit 0
fi
SETUP_SCRIPT="$REPO_DIR/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh"
# The installer was split into lib/ms_*.sh under the 250-line cap, so these
# template checks search the entry script AND its libs: the monitor template
# now lives in lib/ms_monitor.sh, it did not disappear.
SETUP_SOURCES=("$SETUP_SCRIPT")
for _ms_lib in "$(dirname "$SETUP_SCRIPT")"/lib/ms_*.sh; do
[[ -f "$_ms_lib" ]] && SETUP_SOURCES+=("$_ms_lib")
done
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_equals() {
local expected="$1"
local actual="$2"
local context="$3"
if [[ "$expected" != "$actual" ]]; then
fail "$context (expected: '$expected', actual: '$actual')"
fi
}
TMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
WORKTREE="$TMP_DIR/worktree"
BIN_DIR="$TMP_DIR/bin"
mkdir -p "$WORKTREE/scripts/system-maintenance/bin" "$BIN_DIR"
cp "$TARGET_SCRIPT" "$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh"
cat >"$BIN_DIR/busctl" <<'EOF'
#!/bin/bash
if [[ $1 == monitor ]]; then
payload=${MOCK_BUSCTL_LINES:-${MOCK_BUSCTL_LINE:-no relevant event}}
printf '%b\n' "$payload" | while read -r line; do
printf '%s\n' "$line"
done
exit 0
fi
printf 'unexpected busctl args: %s\n' "$*" >&2
exit 1
EOF
chmod +x "$BIN_DIR/busctl"
cat >"$BIN_DIR/systemctl" <<'EOF'
#!/bin/bash
case "$*" in
'is-enabled day-specific-shutdown.timer'|'is-active day-specific-shutdown.timer')
exit 0
;;
'daemon-reload'|'enable day-specific-shutdown.timer'|'start day-specific-shutdown.timer')
exit 0
;;
*)
printf 'unexpected systemctl args: %s\n' "$*" >&2
exit 1
;;
esac
EOF
chmod +x "$BIN_DIR/systemctl"
cat >"$BIN_DIR/sleep" <<'EOF'
#!/bin/bash
printf '%s\n' "$1" >> "${SLEEP_LOG:?}"
exit 99
EOF
chmod +x "$BIN_DIR/sleep"
cat >"$BIN_DIR/tee" <<'EOF'
#!/bin/bash
while IFS= read -r _; do
:
done
EOF
chmod +x "$BIN_DIR/tee"
run_case() {
local expected_mode="$1"
local busctl_present="$2"
local sleep_log="$TMP_DIR/sleep.log"
local mode_file="$TMP_DIR/mode.log"
: >"$sleep_log"
: >"$mode_file"
if [[ $busctl_present -eq 0 ]]; then
mv "$BIN_DIR/busctl" "$BIN_DIR/busctl.off"
fi
mode=$(env -i PATH="$BIN_DIR" SLEEP_LOG="$sleep_log" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \
"source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \
timer_needs_restoration() { return 1; }; \
restore_timer() { :; }; \
monitor_with_dbus() { printf 'dbus'; }; \
monitor_with_polling() { printf 'polling'; }; \
start_monitoring")
assert_equals "$expected_mode" "$mode" 'shutdown timer monitor should choose the expected dispatcher'
if [[ -f "$BIN_DIR/busctl.off" ]]; then
mv "$BIN_DIR/busctl.off" "$BIN_DIR/busctl"
fi
}
run_dbus_throttle_case() {
local ts_sequence="$1"
local expected_calls="$2"
local check_interval="${3:-30}"
local calls
local counter_file="$TMP_DIR/timer_checks.log"
: >"$counter_file"
# The `bash -c` body below is a script for a CHILD shell - $1,
# $TEST_CHECK_INTERVAL, $COUNTER_FILE and ${mock_ts[...]} must reach it
# UNEXPANDED and be evaluated there. Single quotes are load-bearing.
# shellcheck disable=SC2016
calls=$(env -i PATH="$BIN_DIR" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 MOCK_BUSCTL_LINES="day-specific-shutdown.timer\nday-specific-shutdown.timer\nday-specific-shutdown.timer" MOCK_TS_SEQUENCE="$ts_sequence" COUNTER_FILE="$counter_file" TEST_CHECK_INTERVAL="$check_interval" /bin/bash -c '
source "$1"
CHECK_INTERVAL="$TEST_CHECK_INTERVAL"
timer_needs_restoration() { printf "x\n" >> "$COUNTER_FILE"; return 1; }
restore_timer() { :; }
mock_idx=0
IFS=" " read -r -a mock_ts <<< "$MOCK_TS_SEQUENCE"
current_epoch() {
local out_var="${1:-}"
local ts_value="${mock_ts[$mock_idx]:-0}"
mock_idx=$((mock_idx + 1))
if [[ -n $out_var ]]; then
printf -v "$out_var" '%s' "$ts_value"
else
printf "%s\n" "$ts_value"
fi
}
monitor_with_dbus >/dev/null 2>&1 || true
timer_checks=0
while IFS= read -r _; do
timer_checks=$((timer_checks + 1))
done < "$COUNTER_FILE"
printf "%s" "$timer_checks"
' _ "$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh")
assert_equals "$expected_calls" "$calls" 'monitor_with_dbus should throttle repeated relevant events'
}
printf 'Checking D-Bus path is preferred when busctl exists...\n'
run_case dbus 1
printf 'Checking polling fallback is used when busctl is absent...\n'
run_case polling 0
printf 'Checking D-Bus monitor throttles repeated events within interval...\n'
run_dbus_throttle_case '100 105 109' '1'
printf 'Checking D-Bus monitor can process all events when interval is zero...\n'
run_dbus_throttle_case '100 101 102' '3' '0'
printf 'Checking wait helper enforces delay even with /dev/null stdin...\n'
wait_elapsed=$(env -i PATH="/usr/bin:/bin" SHUTDOWN_TIMER_MONITOR_SKIP_MAIN=1 /bin/bash -c \
"source '$WORKTREE/scripts/system-maintenance/bin/shutdown-timer-monitor.sh'; \
start=\$(printf '%(%s)T' -1); \
wait_seconds 1; \
end=\$(printf '%(%s)T' -1); \
printf '%s' \$((end-start))" </dev/null)
assert_equals '1' "$wait_elapsed" 'wait_seconds should not return immediately on /dev/null stdin'
printf 'Checking installer template stays in sync with the event-driven monitor...\n'
grep -Fq 'monitor_with_dbus()' "${SETUP_SOURCES[@]}" ||
fail 'setup_midnight_shutdown.sh should install the D-Bus monitor helper'
grep -Fq 'start_monitoring()' "${SETUP_SOURCES[@]}" ||
fail 'setup_midnight_shutdown.sh should install the start_monitoring dispatcher'
grep -Fq 'if command -v busctl &>/dev/null; then' "${SETUP_SOURCES[@]}" ||
fail 'setup_midnight_shutdown.sh should prefer busctl when available'
grep -Fq 'current_epoch now_ts' "${SETUP_SOURCES[@]}" ||
fail 'setup_midnight_shutdown.sh should use out-var epoch helper in D-Bus throttling path'
# Searching for this literal source text; expanding it would run
# current_epoch and grep for its output instead.
# shellcheck disable=SC2016
if grep -Fq 'now_ts=$(current_epoch)' "${SETUP_SOURCES[@]}"; then
fail 'setup_midnight_shutdown.sh should avoid subshell epoch capture in D-Bus path'
fi
# Literal source text, as above.
# shellcheck disable=SC2016
if grep -Fq 'now_ts=$(current_epoch)' "$TARGET_SCRIPT"; then
fail 'runtime shutdown monitor should avoid subshell epoch capture in D-Bus path'
fi
grep -Fq 'OnUnitActiveSec=300' "${SETUP_SOURCES[@]}" ||
fail 'setup_midnight_shutdown.sh should run watchdog timer at 300s cadence'
grep -Fq 'wait_seconds()' "${SETUP_SOURCES[@]}" ||
fail 'setup_midnight_shutdown.sh should install builtin wait helper in polling fallback'
# Literal source text, as above.
# shellcheck disable=SC2016
grep -Fq 'wait_seconds "$CHECK_INTERVAL"' "$TARGET_SCRIPT" ||
fail 'runtime shutdown monitor polling fallback should use builtin wait helper'
printf 'shutdown-timer-monitor.sh regression checks passed.\n'