testsAndMisc/linux_configuration/tests/test_i3blocks_leisure_shadow.sh
Krzysztof kuhy Rudnicki 5e6aee64cf
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
i3blocks: leisure_shadow says "no data" when generated_at is missing
A daily-limits cache without a numeric generated_at made the age
arithmetic hit "null: unbound variable" under set -u: exit 1 and an empty
block. The render now floors a numeric generated_at and the script emits
the orange "SHADOW no data" fallback for anything else, still exiting 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
2026-10-19 13:17:59 +02:00

153 lines
6.0 KiB
Bash
Executable File

#!/bin/bash
# Behaviour tests for the i3blocks leisure_shadow block: it renders the
# `leisure_shadow` part of daily-limits' cache ($DAILY_LIMITS_CACHE, contract
# C5). Covers text, the yellow/red thresholds, the "no data" states (no cache,
# no key, null numbers), stale/garbage caches, "never urgent" and the
# "re-parse only when the cache changes" contract.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
BLOCK="$REPO_DIR/i3blocks/leisure_shadow.sh"
TMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_eq() {
local expected=$1
local actual=$2
local context=$3
if [[ "$expected" != "$actual" ]]; then
fail "$context (expected '$expected', actual '$actual')"
fi
}
printf '{}' | jq -e . >/dev/null 2>&1 ||
fail 'a working jq is required for the leisure_shadow tests (pacman -S jq)'
CACHE="$TMP_DIR/daily-limits.json"
RENDER="$TMP_DIR/render"
NOW=1790000000
# write_cache USED BUDGET [GENERATED_AT] -- USED/BUDGET may be "null";
# would_block and left_minutes follow from them as daily-limits writes them.
write_cache() {
local used=$1 budget=$2 generated=${3:-$NOW} left=null block=false
if [[ $used != null && $budget != null ]]; then
left=$((budget > used ? budget - used : 0))
((used >= budget)) && block=true
fi
printf '{"date":"2026-10-19","generated_at":%s,"gaming":{"day":"2026-10-19","budget_minutes":300,"used_minutes":0,"ceiling_minutes":480},"leisure_shadow":{"day":"2026-10-19","budget_minutes":%s,"used_minutes":%s,"left_minutes":%s,"would_block":%s,"sources":[]}}\n' \
"$generated" "$budget" "$used" "$left" "$block" >"$CACHE"
rm -f "$RENDER" # force a re-render for the new content
}
run_block() {
NOW_EPOCH="$NOW" DAILY_LIMITS_CACHE="$CACHE" LEISURE_SHADOW_RENDER_CACHE="$RENDER" bash "$BLOCK"
}
line() {
run_block | sed -n "${1}p"
}
printf 'Checking the normal day: time left and used...\n'
write_cache 108 300
assert_eq '🛋 SHADOW 3h12 left · 1h48 used' "$(line 1)" 'full text'
assert_eq '🛋 3h12' "$(line 2)" 'short text'
assert_eq '#F8F8F2' "$(line 3)" 'plain foreground above 30 min'
assert_eq '3' "$(run_block | wc -l)" 'exactly full/short/colour, no urgent line'
printf 'Checking yellow strictly under 30 min...\n'
write_cache 270 300
assert_eq '#F8F8F2' "$(line 3)" '30 min left is not yet yellow'
write_cache 271 300
assert_eq '#F1FA8C' "$(line 3)" '29 min left is yellow'
assert_eq '🛋 SHADOW 0h29 left · 4h31 used' "$(line 1)" 'yellow text'
write_cache 299 300
assert_eq '#F1FA8C' "$(line 3)" '1 min left is still yellow, not red'
printf 'Checking would-block at and over 0, never urgent...\n'
write_cache 300 300
assert_eq '🛋 SHADOW WOULD BLOCK · 5h00 used' "$(line 1)" 'at 0'
assert_eq '🛋 WOULD BLOCK' "$(line 2)" 'would-block short text'
assert_eq '#FF5555' "$(line 3)" 'would-block is red'
write_cache 605 600
assert_eq '🛋 SHADOW WOULD BLOCK · 10h05 used' "$(line 1)" 'over'
set +e
run_block >/dev/null
status=$?
set -e
assert_eq '0' "$status" 'exit 0: i3blocks marks exit 33 urgent (flashing)'
assert_eq '3' "$(run_block | wc -l)" 'would-block emits no urgent line'
printf 'Checking "no data" is said, never faked as zero...\n'
write_cache null null
assert_eq '🛋 SHADOW ⏱? no data' "$(line 1)" 'C4 fields absent'
assert_eq '🛋 ⏱?' "$(line 2)" 'no data short text'
assert_eq '#FFB86C' "$(line 3)" 'no data is orange'
printf '{"date":"2026-10-19","generated_at":%s,"gaming":{}}\n' "$NOW" >"$CACHE"
rm -f "$RENDER"
assert_eq '🛋 SHADOW ⏱? no data' "$(line 1)" 'a cache without leisure_shadow'
rm -f "$CACHE"
assert_eq '🛋 SHADOW ⏱? no data' "$(line 1)" 'missing cache'
printf 'Checking a cache without a usable generated_at says no data, exit 0...\n'
for generated in '' '"generated_at":null,' '"generated_at":"soon",'; do
printf '{%s"leisure_shadow":{"budget_minutes":300,"used_minutes":60,"left_minutes":240,"would_block":false}}\n' \
"$generated" >"$CACHE"
rm -f "$RENDER"
set +e
output=$(run_block 2>&1)
status=$?
set -e
assert_eq '0' "$status" "generated_at '${generated:-absent}' still exits 0"
assert_eq '🛋 SHADOW ⏱? no data' "$(sed -n 1p <<<"$output")" "generated_at '${generated:-absent}'"
assert_eq '#FFB86C' "$(sed -n 3p <<<"$output")" "generated_at '${generated:-absent}' is orange"
done
write_cache 60 300 "$NOW.75"
assert_eq '🛋 SHADOW 4h00 left · 1h00 used' "$(line 1)" 'a fractional generated_at is a timestamp'
printf 'Checking a stale or garbage cache...\n'
write_cache 60 300 "$((NOW - 181))"
assert_eq '🛋 SHADOW ⏱? stale 3m' "$(line 1)" 'older than 180 s'
assert_eq '#FFB86C' "$(line 3)" 'stale is orange'
write_cache 60 300 "$((NOW - 180))"
assert_eq '🛋 SHADOW 4h00 left · 1h00 used' "$(line 1)" '180 s is still fresh'
printf 'not json' >"$CACHE"
rm -f "$RENDER"
assert_eq '🛋 SHADOW unreadable cache' "$(line 1)" 'garbage body'
assert_eq '#FF5555' "$(line 3)" 'garbage is red'
printf 'Checking jq runs only when the cache file changes...\n'
write_cache 60 300
mkdir -p "$TMP_DIR/bin"
printf '#!/bin/bash\necho x >>"%s/jq.calls"\nexec %s "$@"\n' "$TMP_DIR" "$(command -v jq)" >"$TMP_DIR/bin/jq"
chmod +x "$TMP_DIR/bin/jq"
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
assert_eq '1' "$(wc -l <"$TMP_DIR/jq.calls")" 'two ticks, one unchanged cache: one jq run'
sleep 0.05
touch "$CACHE"
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
assert_eq '2' "$(wc -l <"$TMP_DIR/jq.calls")" 'a rewritten cache is re-parsed'
printf 'Checking the config wires the block next to gaming_budget...\n'
CONFIG="$REPO_DIR/i3blocks/config"
grep -A3 '^\[leisure_shadow\]$' "$CONFIG" | grep -q '^command=~/.config/i3blocks/leisure_shadow.sh$' ||
fail '[leisure_shadow] should run leisure_shadow.sh'
next_section=$(sed -n '/^\[gaming_budget\]$/,$p' "$CONFIG" | grep -m2 '^\[' | tail -1)
assert_eq '[leisure_shadow]' "$next_section" '[leisure_shadow] sits right after [gaming_budget]'
! grep -A4 '^\[leisure_shadow\]$' "$CONFIG" | grep -q '^urgent' ||
fail '[leisure_shadow] must never be urgent'
printf 'All leisure_shadow checks passed.\n'