From bd8ded486d11ac1e2693d13c93cd4ed9b5c87f9f Mon Sep 17 00:00:00 2001 From: Daniele Fucini Date: Mon, 9 Nov 2026 19:51:42 +0100 Subject: [PATCH] Add bats test suite Black-box tests that run the script with stub commands on a minimal PATH and temp config/state folders. --- tests/stubs/aur | 8 + tests/stubs/checkupdates | 8 + tests/stubs/fwupdmgr | 4 + tests/stubs/git-monitor | 3 + tests/stubs/notify-send | 4 + tests/stubs/pacdiff | 8 + tests/stubs/pacman | 13 ++ tests/stubs/uname | 3 + tests/update-notify.bats | 405 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 456 insertions(+) create mode 100755 tests/stubs/aur create mode 100755 tests/stubs/checkupdates create mode 100755 tests/stubs/fwupdmgr create mode 100755 tests/stubs/git-monitor create mode 100755 tests/stubs/notify-send create mode 100755 tests/stubs/pacdiff create mode 100755 tests/stubs/pacman create mode 100755 tests/stubs/uname create mode 100644 tests/update-notify.bats diff --git a/tests/stubs/aur b/tests/stubs/aur new file mode 100755 index 0000000..37e7f37 --- /dev/null +++ b/tests/stubs/aur @@ -0,0 +1,8 @@ +#!/bin/bash +# "aur repo -u -d ": prints $STUB_DATA/aur, exits $AUR_RC (default 0). +printf '%s %s\n' "${0##*/}" "$*" >> "$STUB_DATA/calls" +[[ ${1:-} == repo ]] || exit 1 +if [[ -s $STUB_DATA/aur ]]; then + printf '%s\n' "$(<"$STUB_DATA/aur")" +fi +exit "${AUR_RC:-0}" diff --git a/tests/stubs/checkupdates b/tests/stubs/checkupdates new file mode 100755 index 0000000..7501763 --- /dev/null +++ b/tests/stubs/checkupdates @@ -0,0 +1,8 @@ +#!/bin/bash +# Prints $STUB_DATA/checkupdates; exits $CHECKUPDATES_RC, else 0 with updates and 2 without, like the real one. +printf '%s %s\n' "${0##*/}" "$*" >> "$STUB_DATA/calls" +if [[ -s $STUB_DATA/checkupdates ]]; then + printf '%s\n' "$(<"$STUB_DATA/checkupdates")" + exit "${CHECKUPDATES_RC:-0}" +fi +exit "${CHECKUPDATES_RC:-2}" diff --git a/tests/stubs/fwupdmgr b/tests/stubs/fwupdmgr new file mode 100755 index 0000000..de782c4 --- /dev/null +++ b/tests/stubs/fwupdmgr @@ -0,0 +1,4 @@ +#!/bin/bash +# Exits $FWUPD_RC: 0 = firmware updates available, 2 (default) = none. +printf '%s %s\n' "${0##*/}" "$*" >> "$STUB_DATA/calls" +exit "${FWUPD_RC:-2}" diff --git a/tests/stubs/git-monitor b/tests/stubs/git-monitor new file mode 100755 index 0000000..4399ae8 --- /dev/null +++ b/tests/stubs/git-monitor @@ -0,0 +1,3 @@ +#!/bin/bash +# Only needs to exist: the script checks for it with command -v. +exit 0 diff --git a/tests/stubs/notify-send b/tests/stubs/notify-send new file mode 100755 index 0000000..6ff726c --- /dev/null +++ b/tests/stubs/notify-send @@ -0,0 +1,4 @@ +#!/bin/bash +# Appends the title, the body and a "--" line to $STUB_DATA/notify; exits $NOTIFY_RC (default 0). +printf '%s\n' "$1" "$2" -- >> "$STUB_DATA/notify" +exit "${NOTIFY_RC:-0}" diff --git a/tests/stubs/pacdiff b/tests/stubs/pacdiff new file mode 100755 index 0000000..8477544 --- /dev/null +++ b/tests/stubs/pacdiff @@ -0,0 +1,8 @@ +#!/bin/bash +# "pacdiff -o": prints $STUB_DATA/pacdiff. +printf '%s %s\n' "${0##*/}" "$*" >> "$STUB_DATA/calls" +[[ ${1:-} == -o ]] || exit 1 +if [[ -s $STUB_DATA/pacdiff ]]; then + printf '%s\n' "$(<"$STUB_DATA/pacdiff")" +fi +exit 0 diff --git a/tests/stubs/pacman b/tests/stubs/pacman new file mode 100755 index 0000000..e731d6a --- /dev/null +++ b/tests/stubs/pacman @@ -0,0 +1,13 @@ +#!/bin/bash +# "pacman -Q ": looks the package up in $STUB_DATA/installed ("name version" lines). +[[ ${1:-} == -Q && $# == 2 ]] || { echo "pacman stub: unsupported arguments: $*" >&2; exit 1; } +if [[ -f $STUB_DATA/installed ]]; then + while read -r name ver; do + if [[ $name == "$2" ]]; then + printf '%s %s\n' "$name" "$ver" + exit 0 + fi + done < "$STUB_DATA/installed" +fi +printf "error: package '%s' was not found\n" "$2" >&2 +exit 1 diff --git a/tests/stubs/uname b/tests/stubs/uname new file mode 100755 index 0000000..7f16cbc --- /dev/null +++ b/tests/stubs/uname @@ -0,0 +1,3 @@ +#!/bin/bash +# "uname -r": prints $STUB_KERNEL. +printf '%s\n' "$STUB_KERNEL" diff --git a/tests/update-notify.bats b/tests/update-notify.bats new file mode 100644 index 0000000..c202512 --- /dev/null +++ b/tests/update-notify.bats @@ -0,0 +1,405 @@ +#!/usr/bin/env bats +# Black-box tests: the script runs with the stubs in tests/stubs and temp config/state folders. + +bats_require_minimum_version 1.5.0 +bats_load_library bats-support +bats_load_library bats-assert + +SCRIPT=$BATS_TEST_DIRNAME/../update-notify + +setup_file() { + # A kernel release whose vmlinuz exists, so the reboot check stays quiet unless a test fakes one + local f + for f in /usr/lib/modules/*/vmlinuz; do + [[ -e $f ]] || continue + f=${f%/vmlinuz} + export INSTALLED_KERNEL=${f##*/} + return + done + echo "no /usr/lib/modules/*/vmlinuz found" >&2 + return 1 +} + +setup() { + export STUB_DATA=$BATS_TEST_TMPDIR/data + export XDG_CONFIG_HOME=$BATS_TEST_TMPDIR/config + export XDG_STATE_HOME=$BATS_TEST_TMPDIR/state + export STUB_KERNEL=$INSTALLED_KERNEL + STATE=$XDG_STATE_HOME/update-notify + BIN=$BATS_TEST_TMPDIR/bin + mkdir -p "$STUB_DATA" "$XDG_CONFIG_HOME/update-notify" "$BIN" + # PATH holds only the stubs and the real tools the script needs, so a removed stub is a missing command + local tool + for tool in mkdir mv rm id timeout vercmp; do + ln -s "$(command -v "$tool")" "$BIN/$tool" + done + for tool in checkupdates aur pacman pacdiff uname notify-send; do + enable_stub "$tool" + done +} + +enable_stub() { ln -s "$BATS_TEST_DIRNAME/stubs/$1" "$BIN/$1"; } +disable_stub() { rm "$BIN/$1"; } +config() { printf '%s\n' "$@" >> "$XDG_CONFIG_HOME/update-notify/config"; } +repo_updates() { printf '%s\n' "$@" > "$STUB_DATA/checkupdates"; } +aur_updates() { printf '%s\n' "$@" > "$STUB_DATA/aur"; } +installed() { printf '%s\n' "$@" > "$STUB_DATA/installed"; } +pacnew_files() { printf '%s\n' "$@" > "$STUB_DATA/pacdiff"; } + +# Runs the script; notifications and stub calls are logged from this run only +run_script() { + rm -f "$STUB_DATA/notify" "$STUB_DATA/calls" + run /usr/bin/env PATH="$BIN" "$SCRIPT" "$@" +} + +# Args: title, then body lines +assert_notification() { + local actual expected + actual=$(<"$STUB_DATA/notify") || fail "no notification sent" + printf -v expected '%s\n' "$@" -- + assert_equal "$actual" "${expected%$'\n'}" +} + +assert_no_notification() { + [[ ! -e $STUB_DATA/notify ]] || fail "unexpected notification: $(<"$STUB_DATA/notify")" +} + +assert_pending() { + local expected + printf -v expected '%s\n' "$@" + assert_equal "$(<"$STATE/pending")" "${expected%$'\n'}" +} + +assert_called() { + grep -qxF -- "$1" "$STUB_DATA/calls" || fail "not called: $1" +} + +# --- Threshold and arguments --- + +@test "no updates: no notification, pending file has only the threshold" { + run_script + assert_success + assert_no_notification + assert_pending "threshold 30" +} + +@test "below the threshold: no notification, updates listed in the pending file" { + repo_updates "foo 1-1 -> 2-1" + aur_updates "bar 1-1 -> 1.1-1" + run_script + assert_success + assert_no_notification + assert_pending "threshold 30" "repo foo 2-1" "aur bar 1.1-1" +} + +@test "threshold reached: notifies the counts" { + config THRESHOLD=2 + repo_updates "foo 1-1 -> 2-1" + aur_updates "bar 1-1 -> 1.1-1" + run_script + assert_success + assert_notification "Updates available" "Arch: 1, AUR: 1" +} + +@test "threshold argument overrides the config, pending file keeps the config value" { + config THRESHOLD=5 + repo_updates "foo 1-1 -> 2-1" "baz 1-1 -> 2-1" + run_script 2 + assert_success + assert_notification "Updates available" "Arch: 2, AUR: 0" + assert_pending "threshold 5" "repo foo 2-1" "repo baz 2-1" +} + +@test "threshold with a leading zero is read as decimal" { + repo_updates "a 1 -> 2" "b 1 -> 2" "c 1 -> 2" "d 1 -> 2" "e 1 -> 2" "f 1 -> 2" "g 1 -> 2" "h 1 -> 2" "i 1 -> 2" + run_script 010 # octal would be 8 + assert_success + assert_no_notification +} + +@test "non-numeric threshold argument fails before touching the state" { + run_script abc + assert_failure 1 + assert_output "Threshold must be a number" + [[ ! -e $STATE ]] +} + +@test "non-numeric threshold in the config fails" { + config THRESHOLD=many + run_script + assert_failure 1 + assert_output "Threshold must be a number" +} + +# --- Gathering updates --- + +@test "checkupdates failure exits without touching the state" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + run_script + assert_notification "Updates available" "Arch: 1, AUR: 0" + cp -r "$STATE" "$BATS_TEST_TMPDIR/before" + + repo_updates "foo 1-1 -> 2-1" "baz 1-1 -> 2-1" + CHECKUPDATES_RC=1 run_script + assert_failure 1 + assert_output --partial "checkupdates failed" + assert_no_notification + diff -r "$BATS_TEST_TMPDIR/before" "$STATE" +} + +@test "aur repo -u failure warns and counts no AUR updates" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + aur_updates "bar 1-1 -> 1.1-1" + AUR_RC=1 run_script + assert_success + assert_output --partial "aur repo -u failed" + assert_notification "Updates available" "Arch: 1, AUR: 0" + assert_pending "threshold 1" "repo foo 2-1" +} + +@test "aur missing: AUR updates count 0 without a warning" { + disable_stub aur + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + run_script + assert_success + assert_output "" + assert_notification "Updates available" "Arch: 1, AUR: 0" +} + +@test "aur is queried for the configured repo" { + config AUR_REPO=myrepo + run_script + assert_success + assert_called "aur repo -u -d myrepo" +} + +# --- Kernel check --- + +@test "stock kernel update notifies the rebuild below the threshold" { + installed "linux 6.1.1-1" "linux-custom 6.1.1-1" + repo_updates "linux 6.1.1-1 -> 6.1.2-1" + run_script + assert_success + assert_notification "Kernel update available" "Rebuild linux-custom (linux 6.1.2-1)" + assert_pending "threshold 30" "repo linux 6.1.2-1" "kernel linux-custom 6.1.2-1" +} + +@test "kernel rebuild joins the counts over the threshold" { + config THRESHOLD=1 + installed "linux 6.1.2-1" "linux-custom 6.1.1-1" + run_script 0 + assert_success + assert_notification "Updates available" "Arch: 0, AUR: 0" "Rebuild linux-custom (linux 6.1.2-1)" +} + +@test "kernel check ignores pkgrel" { + installed "linux 6.1.1-1" "linux-custom 6.1.1-1" + repo_updates "linux 6.1.1-1 -> 6.1.1-2" + run_script + assert_success + assert_no_notification +} + +@test "custom kernel already built into the repo isn't flagged" { + installed "linux 6.1.1-1" "linux-custom 6.1.1-1" + repo_updates "linux 6.1.1-1 -> 6.1.2-1" "linux-custom 6.1.1-1 -> 6.1.2-1" + run_script + assert_success + assert_no_notification +} + +@test "kernels without a custom build installed are skipped" { + installed "linux 6.1.1-1" + repo_updates "linux 6.1.1-1 -> 6.1.2-1" + run_script + assert_success + assert_no_notification +} + +@test "custom kernel map comes from the config" { + config "KERNELS=([linux-hardened]=linux-hardened-mine)" + installed "linux 6.1.1-1" "linux-custom 6.1.1-1" "linux-hardened 6.1.2-1" "linux-hardened-mine 6.1.1-1" + repo_updates "linux 6.1.1-1 -> 6.1.2-1" + run_script + assert_success + assert_notification "Kernel update available" "Rebuild linux-hardened-mine (linux-hardened 6.1.2-1)" +} + +# --- Notification state --- + +@test "same updates don't notify again, a change does" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + run_script + assert_notification "Updates available" "Arch: 1, AUR: 0" + + run_script + assert_success + assert_no_notification + + repo_updates "foo 1-1 -> 2-1" "baz 1-1 -> 2-1" + run_script + assert_notification "Updates available" "Arch: 2, AUR: 0" +} + +@test "updates gone: the next ones notify again" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + run_script + rm "$STUB_DATA/checkupdates" + run_script + assert_no_notification + [[ ! -e $STATE/updates ]] + + repo_updates "foo 1-1 -> 2-1" + run_script + assert_notification "Updates available" "Arch: 1, AUR: 0" +} + +@test "failed notify-send isn't remembered, the next run retries" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + pacnew_files /etc/a.pacnew + NOTIFY_RC=1 run_script + [[ ! -e $STATE/updates && ! -e $STATE/maintenance ]] + + run_script + assert_notification "Updates available" "Arch: 1, AUR: 0" "Config files to merge with pacdiff: 1" +} + +# --- Quiet runs (path unit) --- + +@test "--quiet writes the pending file but leaves a new notification to the timer" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + run_script --quiet + assert_success + assert_no_notification + assert_pending "threshold 1" "repo foo 2-1" + [[ ! -e $STATE/updates ]] + + run_script + assert_notification "Updates available" "Arch: 1, AUR: 0" +} + +@test "--quiet tracks changes to a shown notification, so the timer doesn't repeat them" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + run_script + repo_updates "foo 1-1 -> 2-1" "baz 1-1 -> 2-1" + run_script -q + assert_no_notification + + run_script + assert_no_notification +} + +@test "--quiet leaves new maintenance items to the timer" { + pacnew_files /etc/a.pacnew + run_script --quiet + assert_no_notification + [[ ! -e $STATE/maintenance ]] + + run_script + assert_notification "System maintenance" "Config files to merge with pacdiff: 1" +} + +@test "--quiet takes a threshold argument after it" { + repo_updates "foo 1-1 -> 2-1" + run_script --quiet 1 + assert_success + assert_no_notification + assert_pending "threshold 30" "repo foo 2-1" +} + +# --- Maintenance reminders --- + +@test "pacnew files notify alone only when new" { + pacnew_files /etc/a.pacnew /etc/b.pacsave + run_script + assert_success + assert_notification "System maintenance" "Config files to merge with pacdiff: 2" + assert_pending "threshold 30" "pacnew /etc/a.pacnew" "pacnew /etc/b.pacsave" + + run_script + assert_no_notification + + pacnew_files /etc/a.pacnew /etc/b.pacsave /etc/c.pacnew + run_script + assert_notification "System maintenance" "Config files to merge with pacdiff: 3" + + rm "$STUB_DATA/pacdiff" + run_script + assert_no_notification + [[ ! -e $STATE/maintenance ]] +} + +@test "maintenance lines join every update notification" { + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + pacnew_files /etc/a.pacnew + run_script + assert_notification "Updates available" "Arch: 1, AUR: 0" "Config files to merge with pacdiff: 1" + + repo_updates "foo 1-1 -> 2-1" "baz 1-1 -> 2-1" + run_script + assert_notification "Updates available" "Arch: 2, AUR: 0" "Config files to merge with pacdiff: 1" +} + +@test "reboot reminder when the running kernel's vmlinuz is gone" { + STUB_KERNEL=0.0.0-gone run_script + assert_success + assert_notification "System maintenance" "Reboot to load the new kernel (running 0.0.0-gone)" + assert_pending "threshold 30" "reboot 0.0.0-gone" +} + +@test "REBOOT_CHECK=0 disables the reboot reminder" { + config REBOOT_CHECK=0 + STUB_KERNEL=0.0.0-gone run_script + assert_success + assert_no_notification +} + +@test "PACNEW_CHECK=0 skips pacdiff" { + config PACNEW_CHECK=0 + pacnew_files /etc/a.pacnew + run_script + assert_success + assert_no_notification + run ! grep -q '^pacdiff' "$STUB_DATA/calls" +} + +# --- Reminders over the threshold --- + +@test "git-monitor and firmware reminders over the threshold" { + enable_stub git-monitor + enable_stub fwupdmgr + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + FWUPD_RC=0 run_script + assert_success + assert_notification "Updates available" "Arch: 1, AUR: 0" \ + "Also check: git-monitor, fwupdmgr (firmware updates available)" + assert_called "fwupdmgr get-updates --no-metadata-check" +} + +@test "no firmware reminder without firmware updates" { + enable_stub git-monitor + enable_stub fwupdmgr + config THRESHOLD=1 + repo_updates "foo 1-1 -> 2-1" + run_script + assert_notification "Updates available" "Arch: 1, AUR: 0" "Also check: git-monitor" +} + +@test "no reminders below the threshold" { + enable_stub git-monitor + enable_stub fwupdmgr + installed "linux 6.1.2-1" "linux-custom 6.1.1-1" + run_script + assert_notification "Kernel update available" "Rebuild linux-custom (linux 6.1.2-1)" + run ! grep -q '^fwupdmgr' "$STUB_DATA/calls" +}