mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
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:
parent
3338033cf2
commit
bca70b67b5
@ -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."
|
||||
}
|
||||
@ -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."
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
|
||||
106
linux_configuration/scripts/single_use/features/lib/tests/features_harness.sh
Executable file
106
linux_configuration/scripts/single_use/features/lib/tests/features_harness.sh
Executable 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 ]]
|
||||
}
|
||||
27
linux_configuration/scripts/single_use/features/lib/tests/run_all.sh
Executable file
27
linux_configuration/scripts/single_use/features/lib/tests/run_all.sh
Executable 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'
|
||||
@ -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"
|
||||
@ -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
|
||||
|
||||
44
meta/scripts/lib/shell_coverage_bar.sh
Executable file
44
meta/scripts/lib/shell_coverage_bar.sh
Executable 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
|
||||
}
|
||||
100
meta/scripts/lib/shell_coverage_jail_setup.sh
Executable file
100
meta/scripts/lib/shell_coverage_jail_setup.sh
Executable 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"
|
||||
}
|
||||
203
meta/scripts/shell_coverage_jail.sh
Executable file
203
meta/scripts/shell_coverage_jail.sh
Executable 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 "$@"
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user