#!/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) TARGET_SCRIPT="$REPO_DIR/scripts/system-maintenance/bin/shutdown-timer-monitor.sh" SETUP_SCRIPT="$REPO_DIR/scripts/digital_wellbeing/setup_midnight_shutdown.sh" 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" 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; then' "$SETUP_SCRIPT" \ || fail 'setup_midnight_shutdown.sh should prefer busctl when available' grep -Fq 'current_epoch now_ts' "$SETUP_SCRIPT" \ || fail 'setup_midnight_shutdown.sh should use out-var epoch helper in D-Bus throttling path' if grep -Fq 'now_ts=$(current_epoch)' "$SETUP_SCRIPT"; then fail 'setup_midnight_shutdown.sh should avoid subshell epoch capture in D-Bus path' fi 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_SCRIPT" \ || fail 'setup_midnight_shutdown.sh should run watchdog timer at 300s cadence' grep -Fq 'wait_seconds()' "$SETUP_SCRIPT" \ || fail 'setup_midnight_shutdown.sh should install builtin wait helper in polling fallback' 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'