testsAndMisc/linux_configuration/periodic_background/lib/tests/services_harness.sh
Krzysztof kuhy Rudnicki 4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.

Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.

The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:

  - 52 relative `source` lines for lib/common.sh and lib/android.sh. The
    correct prefix differs per file: 40 were ../../ and 12 were ../../../,
    so a blanket replace would have fixed one set and broken the other.
  - REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
    each walking up four levels from a directory now two levels shallower.
    This caused a real regression -- the fixes suite went red with
    "log_warn: command not found" -- caught by running it and comparing
    against the pre-move baseline worktree.
  - conftest.py's sys.path entries, which took the pytest gate down with
    14 collection errors, and the `# shellcheck source=` directives, which
    took shellcheck down with SC1091.

16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.

Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.

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

251 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# lib/tests/services_harness.sh — shared fixture for the check_and_enable_services
# split.
#
# Sourced, not executed. Every command the checks reach for — systemctl,
# guardctl, lsattr, chattr, logger, sudo, find, wc — is a PATH shim recording
# its invocation into $DEV/calls, so a test asserts on what the code TRIED to
# do rather than on the state of this machine. Nothing here touches a real unit,
# a real /etc file, or a real installer.
#
# The paths the checks probe (/usr/bin/pacman, /etc/hosts, ...) are prefixed
# with $SYSROOT in every lib, so this harness points SERVICES_ROOT at a fixture
# tree under the tmpdir. A test declares "pretend /etc/hosts has 200 lines" by
# actually creating $SYSROOT/etc/hosts with 200 lines -- the checks then run
# their real logic against a filesystem this process owns.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=services_assert.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/services_assert.sh"
# --- fake external tools ----------------------------------------------------
# systemctl: `is-enabled`/`is-active <unit>` succeed only when $DEV/enabled or
# $DEV/active lists the unit. Every other subcommand (restart, ...) just
# records. `--user --machine=...` is the user_systemctl shape and is answered
# from the same two files, prefixed "user:".
cat >"${FAKE_BIN}/systemctl" <<'SYSTEMCTLSHIM'
#!/usr/bin/env bash
set -euo pipefail
DEV="${SERVICES_TEST_DEV}"
printf '%s\n' "systemctl $*" >>"${DEV}/calls"
prefix=""
args=()
for a in "$@"; do
case "$a" in
--user) prefix="user:" ;;
--machine=*) ;;
*) args+=("$a") ;;
esac
done
verb="${args[0]:-}"
unit="${args[1]:-}"
case "$verb" in
is-enabled) grep -qxF "${prefix}${unit}" "${DEV}/enabled" 2>/dev/null ;;
is-active) grep -qxF "${prefix}${unit}" "${DEV}/active" 2>/dev/null ;;
*) exit 0 ;;
esac
SYSTEMCTLSHIM
chmod +x "${FAKE_BIN}/systemctl"
# guardctl file-guard status <name>: prints the two lines
# guard_lib_instance_healthy greps for, but only for instances listed in
# $DEV/guard_healthy. An instance in $DEV/guard_degraded prints a status whose
# path unit is inactive; anything else exits 1 (unregistered).
cat >"${FAKE_BIN}/guardctl" <<'GUARDCTLSHIM'
#!/usr/bin/env bash
set -euo pipefail
DEV="${SERVICES_TEST_DEV}"
printf '%s\n' "guardctl $*" >>"${DEV}/calls"
name="${3:-}"
if grep -qxF "$name" "${DEV}/guard_healthy" 2>/dev/null; then
printf 'path unit: active\ntarget attrs: ----i---------e---- /etc/%s\n' "$name"
exit 0
fi
if grep -qxF "$name" "${DEV}/guard_degraded" 2>/dev/null; then
printf 'path unit: inactive\ntarget attrs: ------------------ /etc/%s\n' "$name"
exit 0
fi
exit 1
GUARDCTLSHIM
chmod +x "${FAKE_BIN}/guardctl"
# lsattr: prints an attribute string for paths listed in $DEV/immutable,
# otherwise the all-dashes "no attributes" form.
cat >"${FAKE_BIN}/lsattr" <<'LSATTRSHIM'
#!/usr/bin/env bash
set -euo pipefail
DEV="${SERVICES_TEST_DEV}"
target="${1:-}"
if grep -qxF "$target" "${DEV}/immutable" 2>/dev/null; then
printf '%s %s\n' '----i---------e----' "$target"
else
printf '%s %s\n' '------------------e----' "$target"
fi
exit 0
LSATTRSHIM
chmod +x "${FAKE_BIN}/lsattr"
for tool in chattr logger sudo systemd-resolved; do
cat >"${FAKE_BIN}/${tool}" <<SIMPLESHIM
#!/usr/bin/env bash
printf '%s\n' "${tool} \$*" >>"\${SERVICES_TEST_DEV}/calls"
exit 0
SIMPLESHIM
chmod +x "${FAKE_BIN}/${tool}"
done
# shellcheck source=services_path.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/services_path.sh"
present_command() { # <name>...
local name
for name in "$@"; do
printf '#!/usr/bin/env bash\nexit 0\n' >"${FAKE_BIN}/${name}"
chmod +x "${FAKE_BIN}/${name}"
done
}
absent_command() { # <name>...
local name
for name in "$@"; do
rm -f "${FAKE_BIN}/${name}"
done
}
export SERVICES_TEST_DEV="${DEV}"
export PATH="${FAKE_BIN}:${REAL_BIN}"
# --- fixture filesystem -----------------------------------------------------
# Every absolute path the check libs probe is prefixed with $SYSROOT, which they
# read from SERVICES_ROOT. Pointing it at the tmpdir is what makes the
# file-existence branches reachable: the checks run their real logic, against a
# tree this process created. SERVICES_ROOT is deliberately un-defaulted in the
# libs, so a test file that forgets this line dies instead of touching /etc.
readonly SYSROOT_DIR="${TEST_TMPDIR}/sysroot"
export SERVICES_ROOT="${SYSROOT_DIR}"
mkdir -p \
"${SYSROOT_DIR}/usr/bin" \
"${SYSROOT_DIR}/usr/local/bin" \
"${SYSROOT_DIR}/etc/systemd/system" \
"${SYSROOT_DIR}/etc/pacman.d/hooks" \
"${SYSROOT_DIR}/var/lib"
# Create a file under the fixture root, making its parent as needed.
# `sysfile etc/hosts 200` writes 200 filler lines, which is how the
# StevenBlack-list line-count check is driven.
sysfile() { # <relative-path> [line-count]
local target="${SYSROOT_DIR}/$1"
mkdir -p "$(dirname "$target")"
if [[ -n "${2:-}" ]]; then
seq 1 "$2" >"$target"
else
: >"$target"
fi
}
# Remove a file or directory from the fixture root.
sysrm() { # <relative-path>...
local rel
for rel in "$@"; do
rm -rf "${SYSROOT_DIR:?}/${rel}"
done
}
# --- globals the libs read --------------------------------------------------
RED=$'\033[0;31m'
GREEN=$'\033[0;32m'
YELLOW=$'\033[0;33m'
BLUE=$'\033[0;34m'
CYAN=$'\033[0;36m'
NC=$'\033[0m'
DRY_RUN=0
STATUS_ONLY=0
ISSUES_FOUND=0
FIXES_APPLIED=0
declare -A SERVICE_STATUS=()
declare -a MISSING_SCRIPTS=()
# Installer paths point into the tmpdir so a test controls whether the repair
# path finds its script (present) or reports it missing (absent).
#
# Exported because the check libs that read them are sourced by the individual
# test files rather than by this harness, so standalone shellcheck sees only
# the write here and reports SC2034. They genuinely cross a process boundary
# too (the require_root probe runs as a child), so export is the accurate
# declaration rather than a way to quiet the warning.
export PACMAN_WRAPPER_INSTALL="${TEST_TMPDIR}/install_pacman_wrapper.sh"
export MAKEPKG_WRAPPER_INSTALL="${TEST_TMPDIR}/install_makepkg_wrapper.sh"
export PACMAN_WRAPPER_MANIFEST="${TEST_TMPDIR}/pacman-source.sha256"
export MAKEPKG_WRAPPER_MANIFEST="${TEST_TMPDIR}/makepkg-source.sha256"
export MIDNIGHT_SHUTDOWN_SCRIPT="${TEST_TMPDIR}/setup_midnight_shutdown.sh"
export STARTUP_MONITOR_SCRIPT="${TEST_TMPDIR}/setup_pc_startup_monitor.sh"
export PERIODIC_SYSTEM_SCRIPT="${TEST_TMPDIR}/setup_periodic_system.sh"
export HOSTS_INSTALL_SCRIPT="${TEST_TMPDIR}/hosts_install.sh"
export GUARD_LIB_MIGRATE_SCRIPT="${TEST_TMPDIR}/migrate_hosts_guard.sh"
export COMPULSIVE_BLOCK_SCRIPT="${TEST_TMPDIR}/block_compulsive_opening.sh"
export LEECHBLOCK_SCRIPT="${TEST_TMPDIR}/install_leechblock.sh"
export REMOVE_GUEST_MODE_SCRIPT="${TEST_TMPDIR}/remove_guest_mode.sh"
export VBOX_HOSTS_SCRIPT="${TEST_TMPDIR}/enforce_vbox_hosts.sh"
export WORKOUT_LOCKER_INSTALL_SCRIPT="${TEST_TMPDIR}/install_systemd.sh"
export WORKOUT_LOCKER_SCRIPT="${TEST_TMPDIR}/screen_lock.py"
# Create an installer stub that records being run, so "did the repair path fire"
# is observable without any real installation happening.
make_installer() { # <path>
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "ran %s $*" >>"%s/calls"\nexit 0\n' \
"$(basename "$1")" "${DEV}" >"$1"
chmod +x "$1"
}
# shellcheck source=../services_common.sh
. "${LIB_DIR}/services_common.sh"
# Stage a fully healthy blocking stack: a long hosts file, the guard-lib trio
# active, both pacman hooks, and a resolved.conf that reads /etc/hosts.
stage_hosts_ok() {
sysfile etc/hosts 200
sysfile etc/hosts.stevenblack 200
printf '%s\n' "${SERVICES_ROOT}/etc/hosts" >"${DEV}/immutable"
printf 'hosts: files resolve dns\n' >"${SERVICES_ROOT}/etc/nsswitch.conf"
printf '[Resolve]\nReadEtcHosts=yes\n' >"${SERVICES_ROOT}/etc/systemd/resolved.conf"
printf '%s\n' hosts nsswitch resolved >"${DEV}/guard_healthy"
sysfile etc/pacman.d/hooks/10-guard-lib-unlock-all.hook
sysfile etc/pacman.d/hooks/90-guard-lib-relock-all.hook
}
# reset_state — return every mutable global and every $DEV fact to "nothing has
# happened yet", so each test group starts from a known machine.
reset_state() {
rm -f "${DEV:?}"/* 2>/dev/null || true
rm -rf "${SYSROOT_DIR:?}"
mkdir -p \
"${SYSROOT_DIR}/usr/bin" \
"${SYSROOT_DIR}/usr/local/bin" \
"${SYSROOT_DIR}/etc/systemd/system" \
"${SYSROOT_DIR}/etc/pacman.d/hooks" \
"${SYSROOT_DIR}/var/lib"
: >"${DEV}/calls"
DRY_RUN=0
STATUS_ONLY=0
ISSUES_FOUND=0
FIXES_APPLIED=0
SERVICE_STATUS=()
MISSING_SCRIPTS=()
# Staged fake executables are part of the machine a test describes, so they
# reset too. Without this a `present_command discord` leaks into every later
# group, which makes a forgetful test pass for the wrong reason.
rm -f "${FAKE_BIN:?}"/beeper "${FAKE_BIN}"/signal-desktop "${FAKE_BIN}"/discord \
"${FAKE_BIN}"/thorium-browser "${FAKE_BIN}"/chromium \
"${FAKE_BIN}"/google-chrome "${FAKE_BIN}"/brave-browser \
"${FAKE_BIN}"/VBoxManage
}
reset_state