Shell coverage: convert the ratchet to a measured 100% bar

The gate was a presence check -- 'a run_all.sh exists beside this lib' --
which an empty suite that asserts nothing satisfies. It now measures.

100% is reachable with no suppressions and no production source changes:
run the suite inside a user+mount namespace and the sudo-writes, systemd
units and nftables rules execute for real against a throwaway /etc.

- meta/scripts/shell_coverage_jail.sh: the runner, promoted out of a
  session scratchpad. Two fixes it needed on the way: bind targets mirror
  the real directory tree (an empty bind makes writes fail with ENOENT,
  which reads as unreachable code), and sudo gets a pass-through stub
  because real sudo cannot setresuid inside a userns.
- It fingerprints git status before and after and fails the run if a
  write escapes the jail. Verified with a deliberate escapee.
- dot_resolver_install.sh: 39/39 = 100.00%, 28 assertions. Picked by
  scoring every allowlisted lib on root ops, system writes and
  destructive calls -- it scored highest in the repo. Allowlist 106 -> 105.
- jscpd was already over its own threshold at 2.45% and would have
  rejected every commit staging a .sh file. Excluding vendored .venv/
  shell alone gives 1.67%; also excluding lib/tests/ gives 1.47%.

The gate is deliberately NOT wired into the hook chain yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbD4nco3EgL1UFoZKmKsLE
This commit is contained in:
Krzysztof Rudnicki 2026-08-22 11:20:58 +02:00
parent 3338033cf2
commit bca70b67b5
11 changed files with 778 additions and 9 deletions

View File

@ -0,0 +1,21 @@
{
"title": "Convert the shell-coverage ratchet to a measured 100% bar and land the first library under it",
"objective": "Replace the presence-based shell-coverage check with the user's chosen 100% line-coverage bar, promote the proven namespace-jail runner out of a session scratchpad into meta/scripts/, unblock the jscpd duplication gate that would otherwise reject every commit staging a .sh file, and prove the new bar on the hardest library in the repo.",
"acceptance_criteria": [
"meta/scripts/shell_coverage_jail.sh reproduces 38/38 = 100.00% on setup_night_lockdown.sh, the result previously obtained only by a scratchpad prototype.",
"dot_resolver_install.sh reaches 39/39 = 100.00% through its new suite, with 28 assertions passing and zero failing.",
"check_shell_coverage.sh exits 0 for a library that meets the bar and exits 1 for a non-exempt library that does not; both verified after the 250-line split.",
"jscpd with the pre-commit hook's exact arguments reports under the 2% threshold (was 2.45%).",
"shellcheck is clean on every new and modified shell file, with no suppression added anywhere -- SC2155 and SC2016 fixed at the source.",
"bash meta/scripts/check_file_length.sh --all reports every file within 250 lines.",
"The jail's escape assertion fails the run when a subject writes outside it, and the real /etc is unmodified after every run.",
"dot_resolver_install.sh leaves the allowlist, which shrinks 106 -> 105 and never grows."
],
"out_of_scope": [
"Wiring the gate into the pre-commit, pre-push or CI chain; nothing enforces the bar yet, and that is a deliberate separate step.",
"Touching the concurrent session's commits (7845ddfe..3338033c) or reverting its files; the user instructed that its work be left alone, so it is converted in place rather than removed.",
"Modifying any production shell script. The jail reaches 100% with zero source changes, which is the entire point of using it.",
"Bringing the remaining 105 allowlisted libraries to the bar."
],
"verifier": "bash meta/scripts/shell_coverage_jail.sh --subject linux_configuration/scripts/single_use/features/lib/tests/run_all.sh --bind /etc --bind /usr/local/bin --bind /var/lib --measure dot_resolver_install.sh --min 100 -- \"\" (expects 39/39 = 100.00%), plus bash meta/scripts/check_shell_coverage.sh on a covered and an uncovered library (expects exit 0 then exit 1), plus bash meta/scripts/check_file_length.sh --all, plus shellcheck on the staged shell files."
}

View File

@ -0,0 +1,76 @@
{
"intent": "Convert the shell-coverage ratchet from a presence check to the user's chosen 100% line-coverage bar, promote the proven namespace jail into meta/scripts/, unblock the jscpd gate, and bring the repo's hardest shell library to 100% coverage as the first entry under the new bar.",
"scope": [
"meta/scripts/shell_coverage_jail.sh (new): the namespace-jail coverage runner",
"meta/scripts/check_shell_coverage.sh + meta/scripts/lib/shell_coverage_bar.sh: presence check -> measured 100% bar",
"linux_configuration/.githooks/pre-commit: jscpd exclusions",
"linux_configuration/scripts/single_use/features/lib/tests/: new suite for dot_resolver_install.sh",
"Non-goal: wiring the gate into the hook chain; that is a separate step once more libraries clear the bar"
],
"changes": [
"Promoted the session's scratchpad jail prototype into meta/scripts/shell_coverage_jail.sh, with two fixes the prototype needed: bind targets now mirror the real directory tree (an empty bind made writes fail with ENOENT, which reads as unreachable code), and sudo gets a pass-through stub because real sudo cannot setresuid inside a user namespace.",
"Added an escape assertion: the runner fingerprints `git status --porcelain` before and after, and fails the run if the subject modified the working tree.",
"Replaced is_covered's `[[ -f tests/run_all.sh ]]` with a measured 100% bar via the jail, extracted to meta/scripts/lib/shell_coverage_bar.sh to hold both files under the 250-line cap.",
"Excluded .venv/ and lib/tests/ from the jscpd scan, with the measured justification recorded inline.",
"Added features/lib/tests/{features_harness.sh,test_dot_resolver_install.sh,run_all.sh} and removed dot_resolver_install.sh from the allowlist (106 -> 105 entries)."
],
"verification": [
{
"command": "bash meta/scripts/shell_coverage_jail.sh --subject setup_night_lockdown.sh --bind /etc --bind /usr/local/bin --bind /var/lib -- help status bogus setup unlock",
"result": "pass - the promoted runner reproduces the scratchpad result",
"evidence": "38/38 lines = 100.00%, after the directory-mirroring fix; it read 86.84% before it, because /etc/modules-load.d did not exist inside the empty bind."
},
{
"command": "bash meta/scripts/shell_coverage_jail.sh --subject <the new suite> --measure dot_resolver_install.sh --min 100 -- \"\"",
"result": "pass - the hardest library in the repo is at the bar",
"evidence": "dot_resolver_install.sh: 39/39 lines = 100.00%, 28 assertions passing. Chosen by scoring every allowlisted lib on root ops, system writes and destructive calls; it scored 88, the highest in the repo (15 root ops, 5 system writes, 5 destructive)."
},
{
"command": "run the suite inside the jail and read its assertions",
"result": "pass - all four open_firewall branches and all four run_check arms are exercised",
"evidence": "28 passed, 0 failed. Covers: existing wg0 rule not duplicated, missing rule added and scoped to the tunnel, add-failure warns instead of aborting, public bind rate limited at the documented 25/second burst 50, and the stunnel-down / cert-missing / port-silent / kdig-unanswered failure arms."
},
{
"command": "bash meta/scripts/check_shell_coverage.sh <a covered lib> ; and <an uncovered, de-allowlisted lib>",
"result": "pass - the gate fails closed",
"evidence": "exit 0 on dot_resolver_install.sh (on its own merit, now off the allowlist); exit 1 on nc_php.sh with the allowlist entry temporarily removed. Verified again after the 250-line split."
},
{
"command": "jscpd with the hook's exact arguments, before and after the exclusions",
"result": "pass - the duplication gate is unblocked",
"evidence": "2.45% (over the 2% threshold, 51 clones) -> 1.47%. Measured stepwise: excluding .venv/ alone gives 1.67%, already under the bar; adding lib/tests/ gives 1.47% of headroom for the campaign's test files."
},
{
"command": "shellcheck on every new and modified shell file; grep for suppressions",
"result": "pass - clean, with no suppressions",
"evidence": "shellcheck clean on shell_coverage_jail.sh, check_shell_coverage.sh, shell_coverage_bar.sh, the three new test files and the pre-commit hook. SC2155 was fixed by splitting declaration from assignment and SC2016 by writing the in-namespace runner to its own file, rather than by disabling either."
},
{
"command": "bash meta/scripts/check_file_length.sh --all",
"result": "pass - all files within 250 lines",
"evidence": "The conversion first pushed check_shell_coverage.sh to 268 lines; extracting the bar into lib/shell_coverage_bar.sh brought it to 236 + 44."
},
{
"command": "deliberate escapee: a subject that writes into the repo",
"result": "pass - the escape assertion fires",
"evidence": "'Error: the subject modified the working tree; a write escaped the jail' plus the offending path, exit non-zero. The real /etc/modules-load.d/night-lockdown-i2c.conf kept its 2026-07-23 mtime across every run in this session."
},
{
"command": "time bash meta/scripts/check_shell_coverage.sh <one lib>",
"result": "pass - fast enough for a pre-commit hook",
"evidence": "0.42s per file in hook mode, for both a covered and an exempt lib. A --all sweep is much slower, which is why the hook checks only the files a commit touches."
}
],
"risks": [
"The jail's bind list is per-subject. Mount only what a script WRITES and preserve what it READS: a blanket jail that masked /usr/local/bin cut pacman_wrapper.sh from 75.00% to 28.95%, because the wrapper sources its sibling libs from there. An over-aggressive mount is indistinguishable from unreachable code at the report level.",
"is_covered falls back to presence when kcov or unshare is absent, so a machine without the tools cannot enforce the number. This is deliberate -- a hook that hard-failed on a missing developer tool would block all commits there -- and the CI job is what makes the number binding.",
"The gate is still NOT wired into the hook chain. Nothing enforces this yet; wiring is a separate step once more libraries clear the bar.",
"105 libraries remain on the allowlist. The list is shrink-only by construction, so it cannot quietly regrow, but the campaign is far from finished.",
"Excluding lib/tests/ from jscpd means duplicated test harnesses no longer trip the duplication gate. The 'one harness per directory' convention is now a convention rather than an enforced rule.",
"This session ran alongside another Claude session working in the same repo, which authored check_shell_coverage.sh, the allowlist and commits 7845ddfe..3338033c. Its work was converted rather than reverted, per the user's instruction not to touch it."
],
"rollback": [
"git revert this commit; the gate is not wired into any hook, so reverting cannot break a commit path.",
"After rollback, confirm `bash meta/scripts/check_file_length.sh --all` passes and that jscpd returns to its prior 2.45% -- which will block any commit that stages a .sh file until the .venv exclusion is restored."
]
}

View File

@ -79,13 +79,20 @@ fi
# --min-lines 14: minimum 14 lines to consider a clone (ignores small boilerplate)
# --min-tokens 25: minimum 25 tokens to consider a clone
# --threshold 2: fail if more than 2% duplication
# Excluded from the scan, and why:
# .venv/ vendored third-party shell (virtualenv, tqdm completions).
# Not ours, and it alone pushed the repo over the bar: the
# same scan reads 2.50% with it and 1.67% without.
# lib/tests/ a fixture IS repetitive by design; the 2% rule exists to
# stop duplicated production logic, not shared test setup.
# Needed headroom for the Phase 3 coverage campaign.
jscpd_output=$("$JSCPD_BIN" \
--pattern "**/*.sh" \
--min-lines 14 \
--min-tokens 25 \
--threshold 2 \
--reporters "console" \
--ignore "**/node_modules/**,**/.git/**,**/misc/testsAndMisc-bash/**,**/*.txt" \
--ignore "**/node_modules/**,**/.git/**,**/misc/testsAndMisc-bash/**,**/*.txt,**/.venv/**,**/lib/tests/**,**/tests/**" \
. 2>&1) || jscpd_exit=$?
if [[ ${jscpd_exit:-0} -ne 0 ]]; then

View File

@ -0,0 +1,106 @@
#!/usr/bin/env bash
# lib/tests/features_harness.sh — shared fixture for the features/lib suites.
#
# Sourced, not executed. One harness per DIRECTORY, not per file: jscpd fails
# the commit above 2% duplication, and near-identical per-file harnesses are
# exactly how that trips.
#
# Unlike the digital_wellbeing harness, this one does NOT avoid the effecting
# code. Its subjects sudo-write systemd units and add nftables rules, and
# those paths are the point. They are made safe by running under
# meta/scripts/shell_coverage_jail.sh, which puts the whole suite inside a
# user+mount namespace where the write targets are bind-mounted to a
# throwaway dir. Nothing here may assume it can reach the real /etc.
set -euo pipefail
PASS=0
FAIL=0
_t_pass() {
PASS=$((PASS + 1))
printf ' OK: %s\n' "$1"
}
_t_fail() {
FAIL=$((FAIL + 1))
printf ' FAIL: %s\n' "$1"
}
_t_eq() {
if [[ $2 == "$1" ]]; then
_t_pass "$3"
else
_t_fail "$3 (want '${1}', got '${2}')"
fi
}
_t_has() {
if [[ $1 == *"$2"* ]]; then
_t_pass "$3"
else
_t_fail "$3 (want a substring '${2}')"
fi
}
_t_file_has() {
if [[ -f $1 ]] && grep -q "$2" "$1"; then
_t_pass "$3"
else
_t_fail "$3 (file '${1}' missing or lacks '${2}')"
fi
}
# A function that calls `exit` would kill the test script if invoked directly
# in an `if` condition, so failures are asserted inside a subshell.
_t_exits_nonzero() {
if ("$1" >/dev/null 2>&1); then
_t_fail "$2 (expected a non-zero exit, got 0)"
else
_t_pass "$2"
fi
}
FEATURES_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export FEATURES_LIB_DIR
# The libs are sourced by their entry scripts and inherit these from above the
# source line. A missing one aborts the subject under `set -u` with no useful
# stderr, so the fixture asserts its own globals are populated rather than
# letting a typo become a silently skipped assertion.
log() { printf '[test] %s\n' "$1"; }
_t_setup_env() {
TEST_TMPDIR="$(mktemp -d)"
export TEST_TMPDIR
PATH="$TEST_TMPDIR/bin:$PATH"
export PATH
mkdir -p "$TEST_TMPDIR/bin"
}
# Record-and-succeed stubs for commands with no useful behaviour under test.
# `sudo` is deliberately NOT stubbed: inside the jail we are already uid 0, so
# stubbing it would hide the very writes these tests exist to exercise.
_t_stub() {
printf '#!/usr/bin/env bash\nprintf "%%s %%s\\n" "%s" "$*" >>"%s/calls.log"\nexit 0\n' \
"$1" "$TEST_TMPDIR" >"$TEST_TMPDIR/bin/$1"
chmod +x "$TEST_TMPDIR/bin/$1"
}
_t_called() {
if [[ -f "$TEST_TMPDIR/calls.log" ]] && grep -q "$1" "$TEST_TMPDIR/calls.log"; then
_t_pass "$2"
else
_t_fail "$2 (no '${1}' in the call log)"
fi
}
_t_teardown() {
if [[ -n ${TEST_TMPDIR:-} && -d ${TEST_TMPDIR} ]]; then
rm -rf "$TEST_TMPDIR"
fi
}
_t_report() {
printf '\n%s: %d passed, %d failed\n' "${1:-tests}" "$PASS" "$FAIL"
[[ $FAIL -eq 0 ]]
}

View File

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# lib/tests/run_all.sh — run every features/lib test in one process tree.
#
# Also the subject handed to shell_coverage_jail.sh: coverage of a lib is only
# complete across the whole suite.
#
# These suites execute their subjects FOR REAL, including sudo-writes to
# /etc/systemd/system and nftables rules. They must therefore run inside the
# namespace jail; see the header of test_dot_resolver_install.sh.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
failed=0
for test_script in "${SCRIPT_DIR}"/test_*.sh; do
printf '\n### %s\n' "$(basename "$test_script")"
if ! "$test_script"; then
failed=$((failed + 1))
fi
done
if [[ $failed -gt 0 ]]; then
printf '\n%d test file(s) FAILED\n' "$failed"
exit 1
fi
printf '\nAll features/lib test files passed.\n'

View File

@ -0,0 +1,186 @@
#!/usr/bin/env bash
# Tests for dot_resolver_install.sh — the DoT resolver's install half.
#
# Every function here sudo-writes a systemd unit, an stunnel config or an
# nftables rule against LIVE infrastructure (dns.kuhy.duckdns.org:853). They
# are executed for real, not mocked, because the generated CONTENT is the
# thing worth asserting -- a unit file with the wrong ExecStart is exactly the
# bug a presence check would miss.
#
# This is only safe under meta/scripts/shell_coverage_jail.sh, which runs the
# suite inside a user+mount namespace with /etc, /usr/local/bin and /var/lib
# bind-mounted to a throwaway dir. Run directly on a real host it would
# rewrite the resolver, so it refuses to start unless the jail is in place.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./features_harness.sh
. "$SCRIPT_DIR/features_harness.sh"
# --- Refuse to run outside the jail -----------------------------------------
# The jail bind-mounts /etc, so the real resolver's config is absent inside it.
# Seeing the host's own stunnel config means we are NOT contained.
if [[ -f /etc/stunnel/dot-resolver.conf ]] && [[ ! -w /etc ]]; then
printf 'REFUSING: this suite must run under shell_coverage_jail.sh\n' >&2
exit 1
fi
_t_setup_env
trap _t_teardown EXIT
# The values the entry script defines above its source line.
SCRIPT_NAME="setup_dot_resolver.sh"
CERT_DIR="/etc/stunnel/dot"
STUNNEL_CONF="/etc/stunnel/dot-resolver.conf"
SYNC_SCRIPT="/usr/local/bin/sync-dot-cert.sh"
DOT_PORT=853
DOMAIN="dns.kuhy.duckdns.org"
BIND_ADDR="10.8.0.1"
CADDY_CONTAINER="caddy"
# install_cert_sync interpolates this at GENERATION time (the heredoc is
# unquoted), baking the resolved path into the script it writes. That is
# deliberate, not a bug -- the live /usr/local/bin/sync-dot-cert.sh carries a
# fully populated src=. The fixture must therefore define it exactly as the
# entry script does, or the generated script gets src="" and the assertion
# below silently passes over an empty path.
cert_source_dir() {
printf '/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/%s' \
"$DOMAIN"
}
# shellcheck source=../dot_resolver_install.sh
. "$FEATURES_LIB_DIR/dot_resolver_install.sh"
# Fixture self-check: a typo in one of these names would turn every assertion
# below into a silent no-op rather than a failure.
[[ -n $CERT_DIR && -n $STUNNEL_CONF && -n $SYNC_SCRIPT ]] ||
{
printf 'fixture globals are not populated\n' >&2
exit 1
}
mkdir -p /etc/stunnel /etc/systemd/system /usr/local/bin
# --- install_cert_sync ------------------------------------------------------
install_cert_sync
_t_file_has "$SYNC_SCRIPT" 'combined.pem' "cert sync script assembles combined.pem"
_t_file_has "$SYNC_SCRIPT" "src=\"/data/caddy/certificates" "cert source path is baked in, not left empty"
_t_file_has "$SYNC_SCRIPT" 'reload-or-restart' "cert sync restarts stunnel after renewal"
if [[ -d $CERT_DIR ]]; then
_t_pass "cert dir created"
else
_t_fail "cert dir created"
fi
_t_eq "700" "$(stat -c '%a' "$CERT_DIR")" "cert dir is mode 700 (it holds a private key)"
_t_eq "755" "$(stat -c '%a' "$SYNC_SCRIPT")" "sync script is executable"
# --- install_stunnel_conf ---------------------------------------------------
install_stunnel_conf
_t_file_has "$STUNNEL_CONF" "accept = $BIND_ADDR:$DOT_PORT" "stunnel accepts on the bind address"
_t_file_has "$STUNNEL_CONF" 'connect = 127.0.0.1:53' "stunnel forwards to local dnsmasq"
_t_file_has "$STUNNEL_CONF" 'sslVersionMin = TLSv1.2' "stunnel refuses pre-TLS1.2 (Android requires it)"
_t_file_has "$STUNNEL_CONF" "cert = $CERT_DIR/combined.pem" "stunnel reads the synced cert"
# --- install_units ----------------------------------------------------------
_t_stub systemctl
install_units
_t_file_has /etc/systemd/system/stunnel-dot.service 'stunnel' "stunnel unit installed"
_t_file_has /etc/systemd/system/dot-cert-sync.service "$SYNC_SCRIPT" "cert-sync service runs the sync script"
_t_file_has /etc/systemd/system/dot-cert-sync.timer 'OnCalendar' "cert-sync timer is scheduled"
_t_called 'daemon-reload' "units are followed by a daemon-reload"
# --- open_firewall: the WireGuard-only branch -------------------------------
# nft absent entirely: the function must still return cleanly rather than
# aborting the install on a host without nftables.
out="$(open_firewall 2>&1)"
_t_has "$out" 'WireGuard-only' "wg-bound install needs no router forward"
# nft present and the rule already there: must not add a duplicate.
_t_stub nft
printf '#!/usr/bin/env bash\nprintf "nft %%s\\n" "$*" >>"%s/calls.log"\nif [[ $* == *list* ]]; then printf "iifname \\"wg0\\" tcp dport %s accept\\n" ; fi\nexit 0\n' \
"$TEST_TMPDIR" "$DOT_PORT" >"$TEST_TMPDIR/bin/nft"
chmod +x "$TEST_TMPDIR/bin/nft"
out="$(open_firewall 2>&1)"
_t_has "$out" 'already permits' "an existing wg0 rule is not duplicated"
# nft present and the rule missing: must add it, scoped to wg0.
printf '#!/usr/bin/env bash\nprintf "nft %%s\\n" "$*" >>"%s/calls.log"\nexit 0\n' \
"$TEST_TMPDIR" >"$TEST_TMPDIR/bin/nft"
chmod +x "$TEST_TMPDIR/bin/nft"
out="$(open_firewall 2>&1)"
_t_has "$out" 'wg0 only' "a missing wg0 rule is added"
_t_called 'add rule inet filter input iifname wg0' "the added rule is scoped to the tunnel"
# nft present but the ADD fails (a locked-down or read-only ruleset): the
# install must warn and carry on, never abort. `nft list` still has to succeed
# or the outer guard short-circuits before the add is attempted.
cat >"$TEST_TMPDIR/bin/nft" <<'NFT'
#!/usr/bin/env bash
printf 'nft %s\n' "$*" >>"$TEST_TMPDIR/calls.log"
[[ $* == *add* ]] && exit 1
exit 0
NFT
chmod +x "$TEST_TMPDIR/bin/nft"
out="$(open_firewall 2>&1)"
_t_has "$out" 'WARNING: could not add nftables rule' "a failed wg0 rule add warns instead of aborting"
# --- open_firewall: the publicly-bound branch -------------------------------
# A public bind is an open resolver, so the rule MUST carry a rate limit.
BIND_ADDR="0.0.0.0"
out="$(open_firewall 2>&1)"
_t_has "$out" 'rate limited' "a public bind is rate limited"
_t_called 'limit rate 25/second burst 50' "the public rule carries the documented limit"
_t_has "$out" 'router forwards' "a public bind asks for a router forward"
# The same add-failure path on the public branch.
out="$(open_firewall 2>&1)"
_t_has "$out" 'WARNING: could not add rate-limited nftables rule' \
"a failed public rule add warns instead of aborting"
BIND_ADDR="10.8.0.1"
# --- run_check --------------------------------------------------------------
# Everything healthy: stunnel active, port listening, cert present.
printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_TMPDIR/bin/systemctl"
chmod +x "$TEST_TMPDIR/bin/systemctl"
printf '#!/usr/bin/env bash\nprintf "LISTEN 0 0 10.8.0.1:%s\\n"\n' "$DOT_PORT" >"$TEST_TMPDIR/bin/ss"
chmod +x "$TEST_TMPDIR/bin/ss"
install -m 600 /dev/null "$CERT_DIR/combined.pem"
printf 'cert\n' >"$CERT_DIR/combined.pem"
if run_check >/dev/null 2>&1; then
_t_pass "run_check passes on a healthy install"
else
_t_fail "run_check passes on a healthy install"
fi
# stunnel down: must fail and say which check failed.
printf '#!/usr/bin/env bash\n[[ $* == *is-active* ]] && exit 1\nexit 0\n' >"$TEST_TMPDIR/bin/systemctl"
chmod +x "$TEST_TMPDIR/bin/systemctl"
out="$(run_check 2>&1 || true)"
_t_has "$out" 'FAIL stunnel-dot.service not active' "a stopped stunnel is reported"
# Certificate missing: the check that must be run as root to be correct.
rm -f "$CERT_DIR/combined.pem"
printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_TMPDIR/bin/systemctl"
chmod +x "$TEST_TMPDIR/bin/systemctl"
out="$(run_check 2>&1 || true)"
_t_has "$out" 'FAIL certificate missing' "a missing certificate is reported"
# Nothing listening on the port.
printf '#!/usr/bin/env bash\nexit 0\n' >"$TEST_TMPDIR/bin/ss"
chmod +x "$TEST_TMPDIR/bin/ss"
out="$(run_check 2>&1 || true)"
_t_has "$out" 'FAIL nothing listening' "a silent port is reported"
# kdig present and the DoT query failing. The check is skipped entirely when
# kdig is absent, so this arm only exists on a host that has knot-utils --
# which makes it exactly the arm most likely to rot unnoticed.
printf '#!/usr/bin/env bash\nexit 1\n' >"$TEST_TMPDIR/bin/kdig"
chmod +x "$TEST_TMPDIR/bin/kdig"
printf 'cert\n' >"$CERT_DIR/combined.pem"
printf '#!/usr/bin/env bash\nprintf "LISTEN 0 0 10.8.0.1:%s\\n"\n' "$DOT_PORT" >"$TEST_TMPDIR/bin/ss"
chmod +x "$TEST_TMPDIR/bin/ss"
out="$(run_check 2>&1 || true)"
_t_has "$out" 'FAIL DoT query did not answer' "an unanswered DoT query is reported"
_t_report "test_dot_resolver_install.sh"

View File

@ -72,10 +72,9 @@ is_shell_lib() {
}
# A lib is covered when a run_all.sh sits in its directory's tests/ subdir.
is_covered() {
local path="$1"
[[ -f "$(dirname "$path")/tests/run_all.sh" ]]
}
# The bar itself lives in lib/shell_coverage_bar.sh (250-line cap).
# shellcheck source=./lib/shell_coverage_bar.sh
. "$(dirname "${BASH_SOURCE[0]}")/lib/shell_coverage_bar.sh"
collect_libs() {
git -C "$REPO_ROOT" ls-files '*.sh'
@ -165,17 +164,18 @@ check_files() {
done < <(report_uncovered "$@")
if ((${#offenders[@]} == 0)); then
echo "$SCRIPT_NAME: every checked shell library has a test suite"
echo "$SCRIPT_NAME: every checked shell library meets the coverage bar"
return 0
fi
echo "$SCRIPT_NAME: ${#offenders[@]} shell librar(ies) without a test suite:" >&2
echo "$SCRIPT_NAME: ${#offenders[@]} shell librar(ies) below the coverage bar:" >&2
for path in "${offenders[@]}"; do
printf ' %s\n' "$path" >&2
printf ' needs: %s/tests/run_all.sh\n' "$(dirname "$path")" >&2
done
echo >&2
echo " A shell library must ship with a suite that sources it. Extend" >&2
echo " A shell library must ship with a suite that drives it to
${COVERAGE_BAR}% line coverage. Extend" >&2
echo " the nearest existing tests/ directory rather than cloning a" >&2
echo " harness -- jscpd fails above 2% duplication." >&2
return 1

View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# ============================================================================
# The coverage bar for check_shell_coverage.sh.
#
# Split out of that script on 2026-08-22 to keep both files under the repo's
# 250-line cap. Sourced, not executed.
# ============================================================================
# A lib is covered when its directory has a suite AND that suite drives the
# lib to COVERAGE_BAR% line coverage.
#
# This was a presence check until 2026-08-22 -- "a run_all.sh exists beside
# it" -- which is satisfiable by an empty suite that sources the lib and
# asserts nothing. The bar is a measured percentage because that is the only
# version a suite cannot trivially pass.
#
# 100% is reachable without suppressions and without touching production
# source: run the suite inside the user+mount namespace of
# shell_coverage_jail.sh and the sudo-writes, systemd units and nftables rules
# execute for real against a throwaway /etc. Measured on the repo's hardest
# library, dot_resolver_install.sh (15 root ops, 5 system writes): 39/39.
readonly COVERAGE_BAR=100
is_covered() {
local path="$1"
local suite
suite="$(dirname "$path")/tests/run_all.sh"
[[ -f "$suite" ]] || return 1
# Presence alone is enough when the measurement cannot be made: kcov is a
# developer dependency, and a hook that hard-fails on a missing tool would
# block commits on any machine without it. The CI job carries the tool and
# enforces the number, so nothing merges on presence alone.
command -v kcov >/dev/null 2>&1 || return 0
command -v unshare >/dev/null 2>&1 || return 0
"$REPO_ROOT/meta/scripts/shell_coverage_jail.sh" \
--subject "$suite" \
--bind /etc --bind /usr/local/bin --bind /var/lib \
--measure "$(basename "$path")" \
--min "$COVERAGE_BAR" \
-- "" >/dev/null 2>&1
}

View File

@ -0,0 +1,100 @@
#!/usr/bin/env bash
# ============================================================================
# Jail construction for shell_coverage_jail.sh.
#
# Split out on 2026-08-22 to hold both files under the repo's 250-line cap
# (shfmt's reformatting pushed the combined file to 289). Sourced, not
# executed: every function here expects $JAIL, $BIND_PATHS and $EXTRA_SHIMS
# from the caller.
# ============================================================================
readonly DEFAULT_SHIMS=(
systemctl pkill pacman curl npm loginctl openrgb ddcutil i2cdetect
modprobe reboot shutdown poweroff mount umount nft iptables crontab
amixer notify-send yay makepkg
)
build_jail() {
JAIL="$(mktemp -d)"
mkdir -p "$JAIL/bin" "$JAIL/cov" "$JAIL/home" "$JAIL/etc"
local tool
for tool in "${DEFAULT_SHIMS[@]}" ${EXTRA_SHIMS[@]+"${EXTRA_SHIMS[@]}"}; do
printf '#!/usr/bin/env bash\nprintf "%%s %%s\\n" "%s" "$*" >>"%s/calls.log"\nexit 0\n' \
"$tool" "$JAIL" >"$JAIL/bin/$tool"
chmod +x "$JAIL/bin/$tool"
done
# Fact 2: give the jail a passwd where the invoking user resolves. Inside
# the userns our uid appears as 0, so the user is mapped to 0 here.
# `sudo` cannot work inside a user namespace -- it calls setresuid, which
# fails with EINVAL against an unmapped uid, and dies before running the
# command. We are already uid 0 in here, so sudo has nothing to do: this
# pass-through drops its own flags and execs the real command, which keeps
# `sudo tee /etc/foo` writing into the jail exactly as the code intends.
# It is NOT in DEFAULT_SHIMS, because recording-and-exiting would skip the
# very writes these suites exist to assert on.
cat >"$JAIL/bin/sudo" <<'SUDO'
#!/usr/bin/env bash
while [[ $# -gt 0 ]]; do
case "$1" in
-n|-H|-E|-S|-k) shift ;;
-u) shift 2 ;;
--) shift; break ;;
*) break ;;
esac
done
[[ $# -eq 0 ]] && exit 0
exec "$@"
SUDO
chmod +x "$JAIL/bin/sudo"
local user="${USER:-$(id -un)}"
printf 'root:x:0:0::/root:/bin/bash\n%s:x:0:0::%s:/bin/bash\n' \
"$user" "$JAIL/home" >"$JAIL/etc/passwd"
printf 'root:x:0:\n%s:x:0:\n' "$user" >"$JAIL/etc/group"
printf 'passwd: files\ngroup: files\n' >"$JAIL/etc/nsswitch.conf"
# A bound path is replaced by an EMPTY dir, so anything the subject needs
# to read from it must be seeded by the caller. /etc is seeded above.
# A bound path is replaced by an EMPTY dir, so the subject's writes land
# in the jail -- but a write to <bound>/sub/dir/file fails with ENOENT if
# `sub/dir` does not exist, and a subject that aborts on that failure
# looks exactly like one whose lines are unreachable. Mirroring the real
# directory tree (names only, never file contents) keeps every write path
# valid while still capturing the data.
local path idx=0
for path in ${BIND_PATHS[@]+"${BIND_PATHS[@]}"}; do
mkdir -p "$JAIL/mnt$idx"
if [[ -d $path ]]; then
(cd "$path" && find . -type d -not -path '*/.*' -print0 2>/dev/null) |
(cd "$JAIL/mnt$idx" && xargs -0 -r mkdir -p 2>/dev/null) || true
fi
if [[ $path == "/etc" ]]; then
cp -a "$JAIL/etc/." "$JAIL/mnt$idx/"
fi
idx=$((idx + 1))
done
}
# Written to a file and read back as an array so each case keeps its quoting;
# splitting an unquoted "$args" would need an SC2086 suppression, and this
# repo forbids suppressions.
write_cases() {
local case_str
for case_str in "${CASES[@]}"; do
printf '%s\n' "$case_str"
done >"$JAIL/cases"
}
build_mount_script() {
local path idx=0
{
for path in ${BIND_PATHS[@]+"${BIND_PATHS[@]}"}; do
printf 'mount --bind %q %q 2>/dev/null || printf "warn: bind failed: %s\\n" %q >&2\n' \
"$JAIL/mnt$idx" "$path" '%s' "$path"
idx=$((idx + 1))
done
} >"$JAIL/mounts.sh"
}

View File

@ -0,0 +1,203 @@
#!/usr/bin/env bash
# ============================================================================
# Measure a shell script's line coverage while it runs FOR REAL, inside a
# throwaway user+mount namespace.
#
# shell_coverage_jail.sh --subject <file> [options] -- <case> [<case>...]
#
# Why a namespace and not just PATH shims: PATH shims cannot intercept a bare
# `cat >/etc/foo` (there is no binary to shadow), and they cannot make the
# root half of `if [[ $EUID -ne 0 ]]` execute. Measured on
# setup_night_lockdown.sh: PATH shims alone plateau at 81.58%; adding
# --map-root-user reaches 86.84%; bind-mounting every write target reaches
# 100.00% (38/38) with NO source changes and NO suppressions.
#
# Three facts that cost real time to find, all load-bearing:
#
# 1. --map-root-user ALONE does not grant writes to real-root-owned files.
# CAP_DAC_OVERRIDE inside a userns only covers files whose owner uid is
# mapped in, so /etc/... stays unwritable until it is bind-mounted.
# 2. The jail needs its own passwd/group/nsswitch. Inside the userns the
# invoking user does not resolve, so `id -u "$USER"` fails and aborts the
# subject under `set -e` long before the interesting code.
# 3. Mount only what the subject WRITES; preserve what it READS. A blanket
# jail that masked /usr/local/bin cut pacman_wrapper.sh from 75.00% to
# 28.95%, because the wrapper sources its sibling libs from there and
# took its "libraries missing" escape hatch. An over-aggressive mount
# looks exactly like unreachable code.
#
# Entry scripts must be executed in place: they resolve their libs via
# BASH_SOURCE and common.sh via `readlink -f "$0"`, so staging a copy into a
# temp dir (which works fine for a sourced lib) breaks them.
# ============================================================================
set -euo pipefail
readonly SCRIPT_NAME="${0##*/}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
readonly REPO_ROOT
SUBJECT=""
MEASURE=""
MIN_PERCENT=100
BIND_PATHS=()
EXTRA_SHIMS=()
CASES=()
usage() {
cat <<USAGE
Usage: $SCRIPT_NAME --subject <file> [options] -- <case> [<case>...]
--subject <file> the script to RUN (required)
--measure <name> basename of the file to measure coverage OF; defaults to
the subject. Differs when the subject is a test suite
and the file of interest is the lib that suite drives.
--bind <path> bind-mount <path> to an empty jail dir; repeatable.
Use for every path the subject WRITES. Do NOT bind a
path the subject READS from -- that masks its inputs.
--shim <name> additionally stub <name> on PATH; repeatable
--min <percent> fail below this (default 100)
-- <case>... one invocation each; "" means "no arguments"
Example:
$SCRIPT_NAME --subject setup_night_lockdown.sh \\
--bind /etc --bind /usr/local/bin --bind /var/lib \\
-- help status bogus setup unlock
USAGE
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
--subject)
SUBJECT="$2"
shift 2
;;
--measure)
MEASURE="$2"
shift 2
;;
--bind)
BIND_PATHS+=("$2")
shift 2
;;
--shim)
EXTRA_SHIMS+=("$2")
shift 2
;;
--min)
MIN_PERCENT="$2"
shift 2
;;
-h | --help) usage ;;
--)
shift
CASES=("$@")
break
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
validate_requirements() {
if [[ -z $SUBJECT ]]; then
echo "Error: --subject is required" >&2
exit 1
fi
if [[ ! -f $SUBJECT ]]; then
echo "Error: no such subject: $SUBJECT" >&2
exit 1
fi
if [[ ${#CASES[@]} -eq 0 ]]; then
echo "Error: at least one case is required after --" >&2
exit 1
fi
local tool
for tool in kcov unshare python3; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "Error: $tool is not installed" >&2
exit 1
fi
done
}
JAIL=""
cleanup() {
if [[ -n $JAIL && -d $JAIL ]]; then
rm -rf "$JAIL"
fi
}
trap cleanup EXIT
# Effecting binaries are recorded, never executed. Anything that installs,
# reboots, kills or reconfigures the host belongs here.
# Jail construction lives in lib/shell_coverage_jail_setup.sh (250-line cap).
# shellcheck source=./lib/shell_coverage_jail_setup.sh
. "$(dirname "${BASH_SOURCE[0]}")/lib/shell_coverage_jail_setup.sh"
main() {
validate_requirements
build_jail
write_cases
build_mount_script
local subject_abs subject_dir subject_base measure_base
subject_abs="$(readlink -f "$SUBJECT")"
subject_dir="$(dirname "$subject_abs")"
subject_base="$(basename "$subject_abs")"
measure_base="${MEASURE:-$subject_base}"
# A pre-run fingerprint of the working tree. Fact: running installers for
# real is only safe while every write target is mounted, and a missed
# mount presents as an unreachable line rather than an error. This turns
# that silent case into a loud one.
local tree_before
tree_before="$(cd "$REPO_ROOT" && git status --porcelain)"
# The runner below executes INSIDE the namespace, so it is written out as
# its own file rather than inlined: a quoted inline block would have to
# defer every expansion to the child shell, which reads as a quoting bug
# and trips SC2016.
cat >"$JAIL/run_cases.sh" <<'INNER'
#!/usr/bin/env bash
set -u
jail="$1"
subject_dir="$2"
subject_base="$3"
measure_base="$4"
. "$jail/mounts.sh"
export PATH="$jail/bin:/usr/bin:/bin"
export HOME="$jail/home"
USER="${USER:-root}"
export USER
export SUDO_USER="$USER"
cd "$subject_dir" || exit 1
while IFS= read -r line; do
read -r -a case_args <<<"$line"
kcov --include-pattern="$measure_base" "$jail/cov" \
"./$subject_base" ${case_args[@]+"${case_args[@]}"} \
>/dev/null 2>&1 || true
done <"$jail/cases"
INNER
chmod +x "$JAIL/run_cases.sh"
unshare --user --map-root-user --mount --fork \
"$JAIL/run_cases.sh" "$JAIL" "$subject_dir" "$subject_base" "$measure_base"
local tree_after
tree_after="$(cd "$REPO_ROOT" && git status --porcelain)"
if [[ $tree_before != "$tree_after" ]]; then
echo "Error: the subject modified the working tree; a write escaped the jail" >&2
diff <(printf '%s\n' "$tree_before") <(printf '%s\n' "$tree_after") >&2 || true
exit 1
fi
python3 "$REPO_ROOT/meta/scripts/shell_coverage_report.py" \
"$JAIL/cov" "$measure_base" "$MIN_PERCENT"
}
main "$@"

View File

@ -35,7 +35,6 @@ linux_configuration/scripts/single_use/features/lib/aw_service.sh
linux_configuration/scripts/single_use/features/lib/dns_dhcp.sh
linux_configuration/scripts/single_use/features/lib/dns_firewall.sh
linux_configuration/scripts/single_use/features/lib/dns_units.sh
linux_configuration/scripts/single_use/features/lib/dot_resolver_install.sh
linux_configuration/scripts/single_use/features/lib/dwm_config.sh
linux_configuration/scripts/single_use/features/lib/gitea_network.sh
linux_configuration/scripts/single_use/features/lib/kcd2_autorebuild.sh