mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
Clear both hosts-guard libs; teach the instrument done <redirect> (88 -> 86)
hosts_guard_migrate.sh 81/81 and hosts_guard_rollback.sh 65/65, both 100.00% through run_all.sh at --min 100, and check_shell_coverage.sh passes with each allowlist line already deleted. 82 assertions over 36 cases in three files sharing one harness: all four validate_requirements refusals, plugin installation, the mount-collapse loop in every arm (clears, fails, hits its 20-iteration bound, dry-run), per-instance migration for both the bind-mounted and the plugin flavour, and do_migrate's refusal to retire the legacy pacman hooks when nothing registered -- the arm that exists so a failed migration cannot leave the files with no unlock hook at all. INSTRUMENT FIX: rollback measured 97.01% with lines 69 and 108 uncovered, both `done <redirect>` closing a loop. Reproduced minimally: in a three-iteration loop ending `done >"$1"`, the body reports hits=3 and the `done` line hits=0, because bash attributes the redirected compound statement to the loop's OPENING line. That is the same defect _CLOSING_BRACE_OP already handles for `} |`, so _LOOP_END_OP is its twin. The redirect is required by the pattern: a bare `done` stays in the denominator, so genuinely uncovered loops are still caught. Audited as the instrument notes demand -- every newly excluded line printed with its text -- and it is exactly those two lines across all of fixes/lib. Acceptance pair unchanged and exact: dot_resolver_install.sh 39/39, rpi_nc_ca.sh 73/73. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0e9629e110
commit
0201d5460a
19
docs/superpowers/contracts/hosts-guard-tests-2026-08-23.json
Normal file
19
docs/superpowers/contracts/hosts-guard-tests-2026-08-23.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"title": "Clear both hosts-guard libs and fix the done-redirect instrument artifact",
|
||||
"objective": "Bring hosts_guard_migrate.sh and hosts_guard_rollback.sh to 100% measured line coverage and remove both allowlist exemptions, extending continuation_lines() so a loop terminator carrying a redirect stops being counted as an unreachable statement.",
|
||||
"acceptance_criteria": [
|
||||
"Both libs measure 100.00% through tests/run_all.sh at --min 100 with --fail-on-case-error, exiting 0.",
|
||||
"check_shell_coverage.sh passes on both with their allowlist lines already deleted.",
|
||||
"The new _LOOP_END_OP predicate excludes ONLY lines where `done` is followed by a redirect or pipe; a bare `done` stays in the denominator so real uncovered loops are still caught.",
|
||||
"An audit printing every newly-excluded line with its text shows exactly the two lines in hosts_guard_rollback.sh and nothing else across fixes/lib.",
|
||||
"The instrument acceptance pair still measures exactly: dot_resolver_install.sh 39/39 and rpi_nc_ca.sh 73/73, through features/lib/tests/run_all.sh with its jail_args.",
|
||||
"Both libs keep their production behaviour: every new seam defaults to the real path it replaced.",
|
||||
"The suite stays green and shellcheck reports nothing, with zero suppressions."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"The backslash-continued-assignment artifact that caps bt_adapter.sh at 98.55%; it is a different predicate and only affects one line in one lib.",
|
||||
"arch_sysctl_quarantined_cases.sh, which stays outside the suite.",
|
||||
"The last two untested libs, ubuntu_perf_fixes.sh and nvidia_config.sh."
|
||||
],
|
||||
"verifier": "timeout 900s bash meta/scripts/shell_coverage_jail.sh --timeout 600s --subject linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh --measure hosts_guard_migrate.sh (then hosts_guard_rollback.sh) --min 100 --fail-on-case-error -- \"\", then the acceptance pair through features/lib/tests/run_all.sh, then check_shell_coverage.sh on both libs."
|
||||
}
|
||||
63
docs/superpowers/evidence/hosts-guard-tests-2026-08-23.json
Normal file
63
docs/superpowers/evidence/hosts-guard-tests-2026-08-23.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"intent": "Test both hosts-guard migration libraries to 100% line coverage, take them off the allowlist, and teach the coverage instrument that a loop terminator carrying a redirect is not a statement.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/single_use/fixes/lib/hosts_guard_migrate.sh (two test seams)",
|
||||
"linux_configuration/scripts/single_use/fixes/lib/hosts_guard_rollback.sh (one test seam)",
|
||||
"linux_configuration/scripts/single_use/fixes/lib/tests/hosts_guard_harness.sh and three test files (new)",
|
||||
"meta/scripts/lib/shell_coverage_lines.py (_LOOP_END_OP predicate)",
|
||||
"meta/shell-coverage-allowlist.txt (two lines removed)"
|
||||
],
|
||||
"changes": [
|
||||
"Added SYSTEMD_UNIT_DIR and PACMAN_DB_LCK seams to hosts_guard_migrate.sh and HOSTS_FILE to hosts_guard_rollback.sh, each defaulting to the real path, so validate_requirements and do_rollback can be driven without a real /etc or a real pacman lock",
|
||||
"Added hosts_guard_harness.sh, the fifth family harness, supplying the path globals, the LEGACY_HOOKS/LEGACY_UNITS/INSTANCES tables and the msg/note/warn/err/run/instance_* helpers that live in the entry script",
|
||||
"Added 82 assertions over 36 cases across three test files covering both libs",
|
||||
"INSTRUMENT FIX: added _LOOP_END_OP to continuation_lines(), excluding a loop terminator that carries a redirect (`done >file`, `done <file`, `done | cmd`). bash attributes the redirected compound statement to the loop's OPENING line, so kcov instruments the `done` line but it can never be hit",
|
||||
"Removed both libs from meta/shell-coverage-allowlist.txt, taking it from 88 to 86 entries"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "Minimal reproduction of the instrument artifact: a three-iteration `for` loop ending in `done >\"$1\"`, measured with kcov directly",
|
||||
"result": "pass",
|
||||
"evidence": "The loop body reports hits=3 and the `done >\"$1\"` line reports hits=0 in the same run, confirming bash attributes the redirect to the loop's opening line. This is the same defect _CLOSING_BRACE_OP already handles for `} |`."
|
||||
},
|
||||
{
|
||||
"command": "Audit: print every line the new predicate newly excludes, with its text, across all of fixes/lib",
|
||||
"result": "pass",
|
||||
"evidence": "Exactly two lines: hosts_guard_rollback.sh:69 'done >\"$STATE_DIR/units.state\"' and :108 'done <\"$STATE_DIR/units.state\"'. No over-exclusion, which is the only direction that could inflate coverage."
|
||||
},
|
||||
{
|
||||
"command": "Acceptance suite for any instrument change, through features/lib/tests/run_all.sh with its jail_args",
|
||||
"result": "pass",
|
||||
"evidence": "dot_resolver_install.sh: 39/39 = 100.00% and rpi_nc_ca.sh: 73/73 = 100.00% -- both exactly the documented numbers, so the predicate changed no other measurement."
|
||||
},
|
||||
{
|
||||
"command": "shell_coverage_jail.sh --measure hosts_guard_migrate.sh / hosts_guard_rollback.sh --min 100 --fail-on-case-error",
|
||||
"result": "pass",
|
||||
"evidence": "hosts_guard_migrate.sh: 81/81 = 100.00%. hosts_guard_rollback.sh: 65/65 = 100.00% (denominator 67 -> 65 from the predicate; it measured 97.01% with lines 69 and 108 uncovered beforehand)."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/check_shell_coverage.sh on both libs, AFTER deleting their allowlist lines",
|
||||
"result": "pass",
|
||||
"evidence": "'every checked shell library meets the coverage bar', EXIT 0."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All fixes lib test files passed; 98s bare with the three new files."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run ruff --files meta/scripts/lib/shell_coverage_lines.py",
|
||||
"result": "pass",
|
||||
"evidence": "Passed. A bare `ruff check` reports CPY001 on this file, but it does so on the unmodified file from HEAD as well -- it is a config-less invocation artifact, not a regression."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The instrument change lowers denominators wherever `done <redirect>` appears. A lib already at 100% cannot regress from excluding more lines (covered_set is intersected with the denominator), and the audit shows only two lines repo-wide are affected, both in this commit's own lib.",
|
||||
"The predicate REQUIRES a redirect after `done`; a bare `done` stays in the denominator, so ordinary uncovered loops are still caught.",
|
||||
"hosts_guard_harness.sh redefines instance_spec with tmpdir targets rather than /etc paths, so the tests exercise the real control flow against fake targets; a change to the real instance table would not be caught by these tests."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit, then re-add both hosts_guard lines to meta/shell-coverage-allowlist.txt",
|
||||
"Re-run the acceptance pair (dot_resolver_install.sh 39/39, rpi_nc_ca.sh 73/73) to confirm the instrument is back to its prior behaviour"
|
||||
]
|
||||
}
|
||||
@ -8,6 +8,9 @@
|
||||
# Preconditions
|
||||
# ----------------------------------------------------------------------------
|
||||
validate_requirements() {
|
||||
# SYSTEMD_UNIT_DIR and PACMAN_DB_LCK default to the real locations and
|
||||
# are overridden only by the test harness, which runs un-jailed in
|
||||
# ci_mirror.sh and in CI where a bind mount would protect nothing.
|
||||
[[ -x $GUARDCTL ]] || {
|
||||
err "guardctl not found at $GUARDCTL - run guard-lib's install.sh first"
|
||||
exit 1
|
||||
@ -15,8 +18,8 @@ validate_requirements() {
|
||||
|
||||
local unit
|
||||
for unit in guard-file@.path guard-file@.service guard-bind-mount@.service; do
|
||||
[[ -f "/etc/systemd/system/$unit" ]] || {
|
||||
err "missing systemd template /etc/systemd/system/$unit - run guard-lib's install.sh first"
|
||||
[[ -f "${SYSTEMD_UNIT_DIR:-/etc/systemd/system}/$unit" ]] || {
|
||||
err "missing systemd template ${SYSTEMD_UNIT_DIR:-/etc/systemd/system}/$unit - run guard-lib's install.sh first"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
@ -27,8 +30,8 @@ validate_requirements() {
|
||||
}
|
||||
|
||||
# A live transaction would race every chattr and umount below.
|
||||
if [[ -e /var/lib/pacman/db.lck ]]; then
|
||||
err "/var/lib/pacman/db.lck exists - a pacman transaction is in flight"
|
||||
if [[ -e "${PACMAN_DB_LCK:-/var/lib/pacman/db.lck}" ]]; then
|
||||
err "${PACMAN_DB_LCK:-/var/lib/pacman/db.lck} exists - a pacman transaction is in flight"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@ -122,7 +122,9 @@ do_rollback() {
|
||||
# `chattr +i` with no collapse (it relies on running before the mount at
|
||||
# boot), so it fails silently here unless the mount is dropped first. So:
|
||||
# collapse, enforce, then let the bind-mount unit rebuild the ro layer.
|
||||
collapse_mounts /etc/hosts
|
||||
# HOSTS_FILE is a seam for the tests; production always passes the real
|
||||
# path, which is the file this whole rollback exists to re-protect.
|
||||
collapse_mounts "${HOSTS_FILE:-/etc/hosts}"
|
||||
|
||||
local svc
|
||||
for svc in hosts-guard.service nsswitch-guard.service resolved-guard.service; do
|
||||
|
||||
158
linux_configuration/scripts/single_use/fixes/lib/tests/hosts_guard_harness.sh
Executable file
158
linux_configuration/scripts/single_use/fixes/lib/tests/hosts_guard_harness.sh
Executable file
@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/hosts_guard_harness.sh — shared setup for the hosts-guard
|
||||
# migration lib tests (hosts_guard_migrate.sh, hosts_guard_rollback.sh).
|
||||
#
|
||||
# Sourced, not executed. Builds on lib_test_core.sh and adds what both libs
|
||||
# read from their entry script, migrate_hosts_guard_to_guard_lib.sh:
|
||||
#
|
||||
# * the path globals (GUARDCTL, TARGETS_DIR, STATE_DIR, HOOKS_DIR,
|
||||
# PLUGIN_INSTALL_DIR, PLUGIN_SRC_DIR), all pointed into the tmpdir;
|
||||
# * the LEGACY_HOOKS / LEGACY_UNITS / INSTANCES tables, verbatim;
|
||||
# * msg/note/warn/err, `run`, instance_spec and instance_registered,
|
||||
# redefined verbatim -- they live in the entry script, not in any lib;
|
||||
# * the real log_*/has_cmd helpers, via scripts/lib/common.sh.
|
||||
#
|
||||
# The entry script marks most of these `readonly`, but it is never sourced
|
||||
# here: this harness owns its own copies, so nothing is being overwritten.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
FIXES_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
REPO_ROOT="$(cd "${FIXES_DIR}/../../../.." && pwd)"
|
||||
|
||||
# shellcheck source=lib_test_core.sh
|
||||
. "${SCRIPT_DIR}/lib_test_core.sh"
|
||||
|
||||
# shellcheck source=../../../../lib/common.sh
|
||||
. "${REPO_ROOT}/linux_configuration/scripts/lib/common.sh"
|
||||
|
||||
# --- globals owned by migrate_hosts_guard_to_guard_lib.sh -------------------
|
||||
|
||||
# Read by the libs under test, never by this harness, so static analysis
|
||||
# cannot see the use; export makes the intent explicit.
|
||||
export SCRIPT_NAME="migrate_hosts_guard_to_guard_lib.sh"
|
||||
export GUARDCTL="${TEST_TMPDIR}/guardctl"
|
||||
export TARGETS_DIR="${TEST_TMPDIR}/guard-lib/targets"
|
||||
export STATE_DIR="${TEST_TMPDIR}/guard-lib-migration"
|
||||
export HOOKS_DIR="${TEST_TMPDIR}/pacman.d/hooks"
|
||||
export PLUGIN_INSTALL_DIR="${TEST_TMPDIR}/guard-lib-plugins"
|
||||
export PLUGIN_SRC_DIR="${TEST_TMPDIR}/plugin_src"
|
||||
# The two seams added to hosts_guard_migrate.sh, plus the one in
|
||||
# hosts_guard_rollback.sh, so validate_requirements and do_rollback can be
|
||||
# driven without a real /etc or a real pacman lock.
|
||||
export SYSTEMD_UNIT_DIR="${TEST_TMPDIR}/systemd_units"
|
||||
export PACMAN_DB_LCK="${TEST_TMPDIR}/pacman_db.lck"
|
||||
export HOSTS_FILE="${TEST_TMPDIR}/etc_hosts"
|
||||
export DRY_RUN=0
|
||||
|
||||
export RED=''
|
||||
export GREEN=''
|
||||
export YELLOW=''
|
||||
export BLUE=''
|
||||
export NC=''
|
||||
|
||||
# Verbatim from the entry script.
|
||||
readonly LEGACY_HOOKS=(
|
||||
"10-unlock-etc-hosts.hook"
|
||||
"90-relock-etc-hosts.hook"
|
||||
)
|
||||
readonly LEGACY_UNITS=(
|
||||
"hosts-guard.path"
|
||||
"hosts-guard.service"
|
||||
"hosts-bind-mount.service"
|
||||
"nsswitch-guard.path"
|
||||
"nsswitch-guard.service"
|
||||
"resolved-guard.path"
|
||||
"resolved-guard.service"
|
||||
)
|
||||
readonly INSTANCES=(hosts nsswitch resolved)
|
||||
|
||||
# The three tables above are read by the LIBS under test, never by this file,
|
||||
# so shellcheck cannot see the use from here and flags each as unused. Bash
|
||||
# cannot export an array, so the honest way to record the use is a helper the
|
||||
# tests actually call -- it both silences the warning truthfully and gives the
|
||||
# assertions a readable way to ask what the tables contain.
|
||||
_t_table() { # <hooks|units|instances>
|
||||
case "$1" in
|
||||
hooks) printf '%s\n' "${LEGACY_HOOKS[@]}" ;;
|
||||
units) printf '%s\n' "${LEGACY_UNITS[@]}" ;;
|
||||
instances) printf '%s\n' "${INSTANCES[@]}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
msg() { printf "${GREEN}[✓]${NC} %s\n" "$*"; }
|
||||
note() { printf "${BLUE}[i]${NC} %s\n" "$*"; }
|
||||
warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; }
|
||||
err() { printf "${RED}[✗]${NC} %s\n" "$*" >&2; }
|
||||
|
||||
run() {
|
||||
if ((DRY_RUN == 1)); then
|
||||
printf "${YELLOW}DRY-RUN:${NC} %s\n" "$*"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
# instance_spec, with the three real targets redirected into the tmpdir so a
|
||||
# migration can be driven end to end without touching /etc.
|
||||
instance_spec() { # <name> -> echoes "target|bind|plugin|also_watch"
|
||||
case "$1" in
|
||||
hosts) echo "${HOSTS_FILE}|yes||" ;;
|
||||
nsswitch) echo "${TEST_TMPDIR}/nsswitch.conf|no|nsswitch-plugin.sh|" ;;
|
||||
resolved)
|
||||
echo "${TEST_TMPDIR}/resolved.conf|no|resolved-plugin.sh|${TEST_TMPDIR}/resolved.conf.d"
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
instance_registered() { [[ -f "$TARGETS_DIR/$1.conf" ]]; }
|
||||
|
||||
_hosts_guard_default_stubs() {
|
||||
local tool
|
||||
for tool in systemctl chattr mountpoint umount install; do
|
||||
_t_stub "$tool" 'exit 0'
|
||||
done
|
||||
# guardctl is addressed by absolute path, not through PATH, so it is
|
||||
# written directly rather than stubbed.
|
||||
cat >"${GUARDCTL}" <<'GUARDCTL_STUB'
|
||||
#!/usr/bin/env bash
|
||||
printf 'guardctl %s\n' "$*" >>"${LIB_TEST_DEV}/calls"
|
||||
exit 0
|
||||
GUARDCTL_STUB
|
||||
chmod +x "${GUARDCTL}"
|
||||
}
|
||||
|
||||
# hosts_guard_reset — start a test group from "nothing has happened yet".
|
||||
hosts_guard_reset() {
|
||||
_t_reset_calls
|
||||
DRY_RUN=0
|
||||
export DRY_RUN
|
||||
rm -rf "${TARGETS_DIR}" "${STATE_DIR}" "${HOOKS_DIR}" "${PLUGIN_INSTALL_DIR}" \
|
||||
"${PLUGIN_SRC_DIR}" "${SYSTEMD_UNIT_DIR}"
|
||||
mkdir -p "${TARGETS_DIR}" "${HOOKS_DIR}" "${PLUGIN_SRC_DIR}" "${SYSTEMD_UNIT_DIR}"
|
||||
rm -f "${PACMAN_DB_LCK}"
|
||||
printf '127.0.0.1 localhost\n' >"${HOSTS_FILE}"
|
||||
_t_full_path
|
||||
_hosts_guard_default_stubs
|
||||
}
|
||||
|
||||
# _t_templates — create the three systemd templates validate_requirements
|
||||
# insists on, so a test can reach the checks that come after them.
|
||||
_t_templates() {
|
||||
local unit
|
||||
for unit in guard-file@.path guard-file@.service guard-bind-mount@.service; do
|
||||
: >"${SYSTEMD_UNIT_DIR}/${unit}"
|
||||
done
|
||||
}
|
||||
|
||||
# _t_register NAME... — mark instances as already registered with guard-lib.
|
||||
_t_register() {
|
||||
local name
|
||||
for name in "$@"; do
|
||||
: >"${TARGETS_DIR}/${name}.conf"
|
||||
done
|
||||
}
|
||||
|
||||
hosts_guard_reset
|
||||
@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/test_hosts_guard_migrate.sh — tests for hosts_guard_migrate.sh.
|
||||
#
|
||||
# Covers validate_requirements' four refusals, plugin installation, the
|
||||
# per-instance migration and its skips, the mount collapse loop, hook
|
||||
# retirement, and do_migrate's refusal to retire the legacy hooks when
|
||||
# nothing actually registered.
|
||||
#
|
||||
# Calls go through _t_run rather than `out="$(...)"`: command substitution
|
||||
# forks a subshell, and kcov does not register a lib whose first execution
|
||||
# happens inside one.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=hosts_guard_harness.sh
|
||||
. "${SCRIPT_DIR}/hosts_guard_harness.sh"
|
||||
|
||||
# shellcheck source=../hosts_guard_migrate.sh
|
||||
. "${FIXES_DIR}/lib/hosts_guard_migrate.sh"
|
||||
|
||||
printf '\n-- validate_requirements --\n'
|
||||
|
||||
# Each refusal calls `exit 1`, so every case runs in a subshell.
|
||||
|
||||
# Case 1: guardctl missing entirely.
|
||||
hosts_guard_reset
|
||||
rm -f "${GUARDCTL}"
|
||||
rc=0
|
||||
(validate_requirements >"${TEST_TMPDIR}/o" 2>&1) || rc=$?
|
||||
out="$(cat "${TEST_TMPDIR}/o")"
|
||||
_t_eq "1" "$rc" "validate_requirements: exits 1 without guardctl"
|
||||
_t_contains "$out" "guardctl not found" "validate_requirements: names the missing binary"
|
||||
|
||||
# Case 2: guardctl present but the systemd templates are not.
|
||||
hosts_guard_reset
|
||||
rc=0
|
||||
(validate_requirements >"${TEST_TMPDIR}/o" 2>&1) || rc=$?
|
||||
out="$(cat "${TEST_TMPDIR}/o")"
|
||||
_t_eq "1" "$rc" "validate_requirements: exits 1 without the systemd templates"
|
||||
_t_contains "$out" "missing systemd template" "validate_requirements: names the template"
|
||||
|
||||
# Case 3: templates present but the plugin sources are not.
|
||||
hosts_guard_reset
|
||||
_t_templates
|
||||
rm -rf "${PLUGIN_SRC_DIR}"
|
||||
rc=0
|
||||
(validate_requirements >"${TEST_TMPDIR}/o" 2>&1) || rc=$?
|
||||
out="$(cat "${TEST_TMPDIR}/o")"
|
||||
_t_eq "1" "$rc" "validate_requirements: exits 1 without plugin sources"
|
||||
_t_contains "$out" "plugin sources not found" "validate_requirements: names the source dir"
|
||||
|
||||
# Case 4: a pacman transaction is in flight -- refuse, because every chattr
|
||||
# and umount below would race it.
|
||||
hosts_guard_reset
|
||||
_t_templates
|
||||
: >"${PACMAN_DB_LCK}"
|
||||
rc=0
|
||||
(validate_requirements >"${TEST_TMPDIR}/o" 2>&1) || rc=$?
|
||||
out="$(cat "${TEST_TMPDIR}/o")"
|
||||
_t_eq "1" "$rc" "validate_requirements: exits 1 while pacman holds the lock"
|
||||
_t_contains "$out" "a pacman transaction is in flight" \
|
||||
"validate_requirements: explains the refusal"
|
||||
|
||||
# Case 5: everything in place -> passes.
|
||||
hosts_guard_reset
|
||||
_t_templates
|
||||
rc=0
|
||||
(validate_requirements >"${TEST_TMPDIR}/o" 2>&1) || rc=$?
|
||||
_t_eq "0" "$rc" "validate_requirements: passes when every precondition holds"
|
||||
|
||||
printf '\n-- install_plugins --\n'
|
||||
|
||||
# Case 6: the plugin sources are copied in, and only the .sh ones.
|
||||
hosts_guard_reset
|
||||
printf 'x\n' >"${PLUGIN_SRC_DIR}/nsswitch-plugin.sh"
|
||||
printf 'x\n' >"${PLUGIN_SRC_DIR}/resolved-plugin.sh"
|
||||
printf 'x\n' >"${PLUGIN_SRC_DIR}/README.md"
|
||||
_t_run install_plugins
|
||||
_t_contains "$(_t_calls)" "install -m 755" "install_plugins: installs mode 755"
|
||||
_t_contains "$(_t_calls)" "nsswitch-plugin.sh" "install_plugins: installs the nsswitch plugin"
|
||||
_t_lacks "$(_t_calls)" "README.md" "install_plugins: ignores non-.sh files"
|
||||
_t_contains "$out" "plugins installed to" "install_plugins: reports where they went"
|
||||
|
||||
# Case 7: an empty source directory is not an error -- the glob guard is what
|
||||
# stops `*.sh` being passed through literally.
|
||||
hosts_guard_reset
|
||||
_t_run install_plugins
|
||||
_t_eq "0" "$?" "install_plugins: survives an empty plugin source dir"
|
||||
_t_lacks "$(_t_calls)" '*.sh' "install_plugins: does not install the unmatched glob"
|
||||
|
||||
printf '\n-- collapse_mounts --\n'
|
||||
|
||||
# Case 8: not a mountpoint -> the loop never runs.
|
||||
hosts_guard_reset
|
||||
_t_stub mountpoint 'exit 1'
|
||||
_t_run collapse_mounts "${HOSTS_FILE}"
|
||||
_t_eq "0" "$?" "collapse_mounts: returns 0 when nothing is mounted"
|
||||
_t_lacks "$(_t_calls)" "umount" "collapse_mounts: does not unmount a plain file"
|
||||
|
||||
# Case 9: a stack that clears after a few layers. The stub reports mounted
|
||||
# until umount has been called three times, which is what a real stacked
|
||||
# bind looks like on the way down.
|
||||
hosts_guard_reset
|
||||
: >"${TEST_TMPDIR}/umount_count"
|
||||
_t_stub_stdin mountpoint <<'STUB'
|
||||
count=$(wc -l <"${LIB_TEST_DEV}/../umount_count")
|
||||
[[ $count -ge 3 ]] && exit 1
|
||||
exit 0
|
||||
STUB
|
||||
# _t_stub already records the invocation into $DEV/calls, so this body only
|
||||
# has to advance the counter mountpoint reads.
|
||||
_t_stub_stdin umount <<'STUB'
|
||||
echo x >>"${LIB_TEST_DEV}/../umount_count"
|
||||
exit 0
|
||||
STUB
|
||||
_t_run collapse_mounts "${HOSTS_FILE}"
|
||||
_t_eq "3" "$(grep -c umount "${TEST_TMPDIR}/device/calls")" \
|
||||
"collapse_mounts: unmounts every stacked layer and then stops"
|
||||
|
||||
# Case 10: umount fails -> break rather than spin.
|
||||
hosts_guard_reset
|
||||
_t_stub mountpoint 'exit 0'
|
||||
_t_stub umount 'exit 1'
|
||||
_t_run collapse_mounts "${HOSTS_FILE}"
|
||||
_t_eq "0" "$?" "collapse_mounts: returns 0 when umount fails"
|
||||
_t_eq "1" "$(grep -c umount "${TEST_TMPDIR}/device/calls")" \
|
||||
"collapse_mounts: gives up after the first failed umount"
|
||||
|
||||
# Case 11: a mount that never clears is bounded at 20 attempts, not infinite.
|
||||
hosts_guard_reset
|
||||
_t_stub mountpoint 'exit 0'
|
||||
_t_stub umount 'exit 0'
|
||||
_t_run collapse_mounts "${HOSTS_FILE}"
|
||||
_t_eq "21" "$(grep -c umount "${TEST_TMPDIR}/device/calls")" \
|
||||
"collapse_mounts: stops after 21 umounts rather than looping forever"
|
||||
|
||||
# Case 12: under --dry-run nothing is really unmounted, so the loop reports
|
||||
# once instead of hitting the 20-iteration cap.
|
||||
hosts_guard_reset
|
||||
DRY_RUN=1
|
||||
_t_stub mountpoint 'exit 0'
|
||||
_t_run collapse_mounts "${HOSTS_FILE}"
|
||||
_t_contains "$out" "DRY-RUN" "collapse_mounts: announces the intent under dry-run"
|
||||
_t_eq "1" "$(grep -c 'DRY-RUN' <<<"$out")" \
|
||||
"collapse_mounts: reports the intent once, not twenty times"
|
||||
|
||||
printf '\nhosts_guard_migrate (checks): %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/test_hosts_guard_migrate_flow.sh — tests for the migration flow
|
||||
# itself: migrate_instance, stop_legacy_units_for, retire_legacy_hooks and
|
||||
# do_migrate.
|
||||
#
|
||||
# Split from test_hosts_guard_migrate.sh to hold every file under the
|
||||
# 250-line cap.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=hosts_guard_harness.sh
|
||||
. "${SCRIPT_DIR}/hosts_guard_harness.sh"
|
||||
|
||||
# shellcheck source=../hosts_guard_migrate.sh
|
||||
. "${FIXES_DIR}/lib/hosts_guard_migrate.sh"
|
||||
|
||||
# shellcheck source=../hosts_guard_rollback.sh
|
||||
. "${FIXES_DIR}/lib/hosts_guard_rollback.sh"
|
||||
|
||||
printf '\n-- stop_legacy_units_for --\n'
|
||||
|
||||
# Case 1: only the units whose name matches the instance prefix are touched.
|
||||
hosts_guard_reset
|
||||
_t_run stop_legacy_units_for hosts
|
||||
calls="$(_t_calls)"
|
||||
_t_contains "$calls" "disable --now hosts-guard.path" \
|
||||
"stop_legacy_units_for: disables the matching path unit"
|
||||
_t_contains "$calls" "disable --now hosts-bind-mount.service" \
|
||||
"stop_legacy_units_for: disables the matching bind-mount unit"
|
||||
_t_lacks "$calls" "nsswitch-guard" \
|
||||
"stop_legacy_units_for: leaves another instance's units alone"
|
||||
|
||||
# Case 2: a unit systemd does not know about is skipped, not disabled.
|
||||
hosts_guard_reset
|
||||
_t_stub_stdin systemctl <<'STUB'
|
||||
[[ $1 == list-unit-files ]] && exit 1
|
||||
exit 0
|
||||
STUB
|
||||
_t_run stop_legacy_units_for hosts
|
||||
_t_lacks "$(_t_calls)" "disable --now" \
|
||||
"stop_legacy_units_for: skips units systemd does not have"
|
||||
|
||||
printf '\n-- migrate_instance --\n'
|
||||
|
||||
# Case 3: an already-registered instance is skipped before anything is done.
|
||||
hosts_guard_reset
|
||||
_t_register hosts
|
||||
_t_run migrate_instance hosts
|
||||
_t_contains "$out" "already registered - skipping" \
|
||||
"migrate_instance: skips an instance guard-lib already owns"
|
||||
_t_lacks "$(_t_calls)" "guardctl" "migrate_instance: does not call guardctl when skipping"
|
||||
|
||||
# Case 4: a target that does not exist is warned about, not migrated.
|
||||
hosts_guard_reset
|
||||
rm -f "${HOSTS_FILE}"
|
||||
_t_run migrate_instance hosts
|
||||
_t_contains "$out" "does not exist - skipping" \
|
||||
"migrate_instance: warns about a missing target"
|
||||
_t_lacks "$(_t_calls)" "guardctl" "migrate_instance: does not migrate a missing target"
|
||||
|
||||
# Case 5: the hosts instance -- bind-mounted, no plugin.
|
||||
hosts_guard_reset
|
||||
_t_run migrate_instance hosts
|
||||
calls="$(_t_calls)"
|
||||
_t_contains "$calls" "guardctl file-guard install hosts" \
|
||||
"migrate_instance: installs the hosts instance"
|
||||
_t_contains "$calls" "--bind-mount" "migrate_instance: passes --bind-mount for hosts"
|
||||
_t_lacks "$calls" "--plugin" "migrate_instance: passes no plugin for hosts"
|
||||
_t_contains "$calls" "chattr -i" "migrate_instance: clears the immutable flag first"
|
||||
|
||||
# Case 6: the resolved instance -- a plugin AND an --also-watch directory.
|
||||
hosts_guard_reset
|
||||
printf 'x\n' >"${TEST_TMPDIR}/resolved.conf"
|
||||
_t_run migrate_instance resolved
|
||||
calls="$(_t_calls)"
|
||||
_t_contains "$calls" "--plugin" "migrate_instance: passes the plugin for resolved"
|
||||
_t_contains "$calls" "resolved-plugin.sh" "migrate_instance: names the right plugin"
|
||||
_t_contains "$calls" "--also-watch" "migrate_instance: passes the also-watch dir"
|
||||
_t_lacks "$calls" "--bind-mount" "migrate_instance: does not bind-mount resolved"
|
||||
|
||||
printf '\n-- retire_legacy_hooks --\n'
|
||||
|
||||
# Case 7: no hooks left -> says so, removes nothing.
|
||||
hosts_guard_reset
|
||||
_t_run retire_legacy_hooks
|
||||
_t_contains "$out" "already retired" "retire_legacy_hooks: reports nothing to do"
|
||||
|
||||
# Case 8: both hooks present -> both removed.
|
||||
hosts_guard_reset
|
||||
: >"${HOOKS_DIR}/10-unlock-etc-hosts.hook"
|
||||
: >"${HOOKS_DIR}/90-relock-etc-hosts.hook"
|
||||
_t_run retire_legacy_hooks
|
||||
_t_eq "0" "$?" "retire_legacy_hooks: returns 0 after retiring"
|
||||
_t_contains "$out" "retired 10-unlock-etc-hosts.hook" \
|
||||
"retire_legacy_hooks: names the unlock hook"
|
||||
_t_contains "$out" "retired 90-relock-etc-hosts.hook" \
|
||||
"retire_legacy_hooks: names the relock hook"
|
||||
_t_lacks "$out" "already retired" "retire_legacy_hooks: does not also claim there was nothing to do"
|
||||
|
||||
printf '\n-- do_migrate --\n'
|
||||
|
||||
# Case 9: nothing registers -> REFUSE to retire the legacy hooks. Retiring
|
||||
# them after a failed migration would leave the files with no pacman unlock
|
||||
# hook at all, and the next transaction would fight chattr +i.
|
||||
hosts_guard_reset
|
||||
_t_templates
|
||||
: >"${HOOKS_DIR}/10-unlock-etc-hosts.hook"
|
||||
cat >"${GUARDCTL}" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
printf 'guardctl %s\n' "$*" >>"${LIB_TEST_DEV}/calls"
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "${GUARDCTL}"
|
||||
rc=0
|
||||
(do_migrate >"${TEST_TMPDIR}/o" 2>&1) || rc=$?
|
||||
out="$(cat "${TEST_TMPDIR}/o")"
|
||||
_t_eq "1" "$rc" "do_migrate: exits 1 when no instance registered"
|
||||
_t_contains "$out" "refusing to retire the legacy hooks" "do_migrate: explains the refusal"
|
||||
_t_eq "yes" "$([[ -f "${HOOKS_DIR}/10-unlock-etc-hosts.hook" ]] && echo yes || echo no)" \
|
||||
"do_migrate: leaves the legacy hook in place after refusing"
|
||||
|
||||
# Case 10: a successful migration -- guardctl registers each instance, so the
|
||||
# hooks are retired and the summary counts them.
|
||||
hosts_guard_reset
|
||||
_t_templates
|
||||
printf 'x\n' >"${TEST_TMPDIR}/nsswitch.conf"
|
||||
printf 'x\n' >"${TEST_TMPDIR}/resolved.conf"
|
||||
: >"${HOOKS_DIR}/10-unlock-etc-hosts.hook"
|
||||
cat >"${GUARDCTL}" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
printf 'guardctl %s\n' "$*" >>"${LIB_TEST_DEV}/calls"
|
||||
# Register the instance, the way the real guardctl would.
|
||||
if [[ $2 == install ]]; then
|
||||
: >"${LIB_TEST_TARGETS}/$3.conf"
|
||||
fi
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "${GUARDCTL}"
|
||||
LIB_TEST_TARGETS="${TARGETS_DIR}"
|
||||
export LIB_TEST_TARGETS
|
||||
_t_run do_migrate
|
||||
_t_contains "$out" "migration complete (3 instance(s) registered)" \
|
||||
"do_migrate: counts every registered instance"
|
||||
_t_eq "no" "$([[ -f "${HOOKS_DIR}/10-unlock-etc-hosts.hook" ]] && echo yes || echo no)" \
|
||||
"do_migrate: retires the legacy hooks once guard-lib owns something"
|
||||
|
||||
# Case 11: under --dry-run nothing registers, but the refusal must NOT fire --
|
||||
# a dry run is expected to register nothing.
|
||||
hosts_guard_reset
|
||||
_t_templates
|
||||
DRY_RUN=1
|
||||
_t_run do_migrate
|
||||
_t_eq "0" "$?" "do_migrate: dry-run does not trip the no-registration refusal"
|
||||
_t_contains "$out" "DRY-RUN" "do_migrate: dry-run announces its intent"
|
||||
|
||||
printf '\nhosts_guard_migrate (flow): %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/test_hosts_guard_rollback.sh — tests for hosts_guard_rollback.sh:
|
||||
# show_status, save_rollback_state and do_rollback.
|
||||
#
|
||||
# Calls go through _t_run rather than `out="$(...)"`: command substitution
|
||||
# forks a subshell, and kcov does not register a lib whose first execution
|
||||
# happens inside one.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=hosts_guard_harness.sh
|
||||
. "${SCRIPT_DIR}/hosts_guard_harness.sh"
|
||||
|
||||
# shellcheck source=../hosts_guard_migrate.sh
|
||||
. "${FIXES_DIR}/lib/hosts_guard_migrate.sh"
|
||||
|
||||
# shellcheck source=../hosts_guard_rollback.sh
|
||||
. "${FIXES_DIR}/lib/hosts_guard_rollback.sh"
|
||||
|
||||
printf '\n-- show_status --\n'
|
||||
|
||||
# Case 1: nothing registered, hooks gone, units absent.
|
||||
hosts_guard_reset
|
||||
_t_stub systemctl 'exit 1'
|
||||
_t_run show_status
|
||||
_t_contains "$out" "hosts NOT registered" "show_status: reports an unregistered instance"
|
||||
_t_contains "$out" "10-unlock-etc-hosts.hook retired" "show_status: reports a retired hook"
|
||||
_t_contains "$out" "hosts-guard.path is absent" \
|
||||
"show_status: reports an absent unit rather than an empty state"
|
||||
|
||||
# Case 2: registered instances, hooks still present, a unit still enabled.
|
||||
# `systemctl is-enabled` PRINTS the state but EXITS 1 for anything not
|
||||
# enabled, which is why the lib cannot use `|| echo absent`.
|
||||
hosts_guard_reset
|
||||
_t_register hosts nsswitch resolved
|
||||
: >"${HOOKS_DIR}/10-unlock-etc-hosts.hook"
|
||||
_t_stub_stdin systemctl <<'STUB'
|
||||
[[ $1 == is-enabled ]] && { echo enabled; exit 0; }
|
||||
exit 0
|
||||
STUB
|
||||
_t_run show_status
|
||||
_t_contains "$out" "hosts registered" "show_status: reports a registered instance"
|
||||
_t_contains "$out" "10-unlock-etc-hosts.hook still present" \
|
||||
"show_status: warns about a surviving hook"
|
||||
_t_contains "$out" "hosts-guard.path is enabled" "show_status: warns about an enabled unit"
|
||||
|
||||
# Case 3: a unit that is disabled rather than absent -- is-enabled prints the
|
||||
# state AND exits non-zero, and the lib must keep the printed word.
|
||||
hosts_guard_reset
|
||||
_t_stub_stdin systemctl <<'STUB'
|
||||
[[ $1 == is-enabled ]] && { echo disabled; exit 1; }
|
||||
exit 0
|
||||
STUB
|
||||
_t_run show_status
|
||||
_t_contains "$out" "hosts-guard.path is disabled" \
|
||||
"show_status: keeps the state systemd printed on a non-zero exit"
|
||||
_t_lacks "$out" "hosts-guard.path is absent" \
|
||||
"show_status: does not overwrite a real state with 'absent'"
|
||||
|
||||
printf '\n-- save_rollback_state --\n'
|
||||
|
||||
# Case 4: hooks are backed up and unit states recorded.
|
||||
hosts_guard_reset
|
||||
: >"${HOOKS_DIR}/10-unlock-etc-hosts.hook"
|
||||
_t_stub_stdin systemctl <<'STUB'
|
||||
[[ $1 == is-enabled ]] && { echo enabled; exit 0; }
|
||||
exit 0
|
||||
STUB
|
||||
_t_run save_rollback_state
|
||||
_t_contains "$out" "backed up 10-unlock-etc-hosts.hook" \
|
||||
"save_rollback_state: backs up a present hook"
|
||||
_t_eq "yes" "$([[ -f "${STATE_DIR}/hooks/10-unlock-etc-hosts.hook" ]] && echo yes || echo no)" \
|
||||
"save_rollback_state: the backup lands in the state dir"
|
||||
_t_contains "$(cat "${STATE_DIR}/units.state")" "hosts-guard.path=enabled" \
|
||||
"save_rollback_state: records the exact prior enablement"
|
||||
|
||||
# Case 5: an existing backup is NOT overwritten -- the first capture is the
|
||||
# pre-migration truth, and a second run must not clobber it.
|
||||
hosts_guard_reset
|
||||
mkdir -p "${STATE_DIR}/hooks"
|
||||
printf 'ORIGINAL\n' >"${STATE_DIR}/hooks/10-unlock-etc-hosts.hook"
|
||||
printf 'CHANGED\n' >"${HOOKS_DIR}/10-unlock-etc-hosts.hook"
|
||||
_t_stub systemctl 'exit 0'
|
||||
_t_run save_rollback_state
|
||||
_t_eq "ORIGINAL" "$(cat "${STATE_DIR}/hooks/10-unlock-etc-hosts.hook")" \
|
||||
"save_rollback_state: keeps the first backup"
|
||||
|
||||
# Case 6: units.state is likewise written once only.
|
||||
hosts_guard_reset
|
||||
mkdir -p "${STATE_DIR}"
|
||||
printf 'PRESERVED\n' >"${STATE_DIR}/units.state"
|
||||
_t_stub systemctl 'exit 0'
|
||||
_t_run save_rollback_state
|
||||
_t_eq "PRESERVED" "$(cat "${STATE_DIR}/units.state")" \
|
||||
"save_rollback_state: does not rewrite units.state"
|
||||
|
||||
# Case 7: under --dry-run no unit state is recorded at all.
|
||||
hosts_guard_reset
|
||||
DRY_RUN=1
|
||||
_t_stub systemctl 'exit 0'
|
||||
_t_run save_rollback_state
|
||||
_t_eq "no" "$([[ -f "${STATE_DIR}/units.state" ]] && echo yes || echo no)" \
|
||||
"save_rollback_state: dry-run records no unit state"
|
||||
|
||||
printf '\n-- do_rollback --\n'
|
||||
|
||||
# Case 8: no state dir at all -> refuse.
|
||||
hosts_guard_reset
|
||||
rm -rf "${STATE_DIR}"
|
||||
rc=0
|
||||
(do_rollback >"${TEST_TMPDIR}/o" 2>&1) || rc=$?
|
||||
out="$(cat "${TEST_TMPDIR}/o")"
|
||||
_t_eq "1" "$rc" "do_rollback: exits 1 with no rollback state"
|
||||
_t_contains "$out" "nothing to roll back to" "do_rollback: explains the refusal"
|
||||
|
||||
# Case 9: a full rollback -- uninstall keeps the canonical copy, hooks are
|
||||
# restored, enabled units come back, and the legacy enforcement re-runs.
|
||||
hosts_guard_reset
|
||||
_t_register hosts
|
||||
mkdir -p "${STATE_DIR}/hooks"
|
||||
printf 'HOOK\n' >"${STATE_DIR}/hooks/10-unlock-etc-hosts.hook"
|
||||
printf 'hosts-guard.path=enabled\nnsswitch-guard.path=disabled\n' >"${STATE_DIR}/units.state"
|
||||
_t_run do_rollback
|
||||
calls="$(_t_calls)"
|
||||
_t_contains "$calls" "file-guard uninstall hosts --keep-canonical" \
|
||||
"do_rollback: keeps the canonical copy, so a rollback is not data loss"
|
||||
_t_eq "HOOK" "$(cat "${HOOKS_DIR}/10-unlock-etc-hosts.hook")" \
|
||||
"do_rollback: restores the backed-up hook"
|
||||
_t_contains "$calls" "enable --now hosts-guard.path" \
|
||||
"do_rollback: re-enables a unit that was enabled before"
|
||||
_t_lacks "$calls" "enable --now nsswitch-guard.path" \
|
||||
"do_rollback: leaves a unit that was disabled before"
|
||||
_t_contains "$calls" "start hosts-guard.service" \
|
||||
"do_rollback: re-runs the legacy enforcement"
|
||||
_t_contains "$calls" "restart hosts-bind-mount.service" \
|
||||
"do_rollback: rebuilds the read-only bind mount last"
|
||||
_t_contains "$out" "rollback complete" "do_rollback: reports completion"
|
||||
|
||||
# Case 10: no units.state -> warn and leave the units alone rather than
|
||||
# guessing which of them ought to be enabled.
|
||||
hosts_guard_reset
|
||||
mkdir -p "${STATE_DIR}"
|
||||
_t_run do_rollback
|
||||
_t_contains "$out" "no units.state recorded" "do_rollback: warns when it cannot restore units"
|
||||
_t_lacks "$(_t_calls)" "enable --now" "do_rollback: enables nothing without a record"
|
||||
|
||||
# Case 11: an instance that is not registered is not uninstalled.
|
||||
hosts_guard_reset
|
||||
mkdir -p "${STATE_DIR}"
|
||||
_t_run do_rollback
|
||||
_t_lacks "$(_t_calls)" "file-guard uninstall" \
|
||||
"do_rollback: does not uninstall an unregistered instance"
|
||||
|
||||
# Case 12: a unit systemd knows nothing about is recorded as "absent"
|
||||
# rather than as an empty string, which is the fallback arm inside the
|
||||
# state-writing loop.
|
||||
hosts_guard_reset
|
||||
_t_stub_stdin systemctl <<'STUB'
|
||||
[[ $1 == is-enabled ]] && exit 1
|
||||
exit 0
|
||||
STUB
|
||||
_t_run save_rollback_state
|
||||
_t_contains "$(cat "${STATE_DIR}/units.state")" "hosts-guard.path=absent" \
|
||||
"save_rollback_state: records an unknown unit as absent"
|
||||
_t_eq "0" "$(grep -c '=$' "${STATE_DIR}/units.state")" \
|
||||
"save_rollback_state: never writes a line with an empty state"
|
||||
|
||||
# Case 13: re-enabling a unit can fail, and the rollback must warn and carry
|
||||
# on rather than abort with units half-restored.
|
||||
hosts_guard_reset
|
||||
mkdir -p "${STATE_DIR}"
|
||||
printf 'hosts-guard.path=enabled\nresolved-guard.path=enabled\n' >"${STATE_DIR}/units.state"
|
||||
_t_stub_stdin systemctl <<'STUB'
|
||||
[[ $1 == enable ]] && exit 1
|
||||
exit 0
|
||||
STUB
|
||||
_t_run do_rollback
|
||||
_t_contains "$out" "could not re-enable hosts-guard.path" \
|
||||
"do_rollback: warns when a unit will not re-enable"
|
||||
_t_contains "$out" "could not re-enable resolved-guard.path" \
|
||||
"do_rollback: keeps going after a failed re-enable"
|
||||
_t_contains "$out" "rollback complete" "do_rollback: still completes"
|
||||
|
||||
printf '\nhosts_guard_rollback: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
@ -75,6 +75,18 @@ def traced_paths(trace_dir: Path, subject: str) -> list[Path]:
|
||||
# bare `}` closing a function carries no operator.
|
||||
_CLOSING_BRACE_OP = re.compile(r"^\s*\}\s*(\||>>?|<|&>)")
|
||||
|
||||
# A loop terminator carrying a redirect: `done >file`, `done <file`,
|
||||
# `done | cmd`. Same defect as _CLOSING_BRACE_OP and the same reasoning: bash
|
||||
# attributes the redirected compound statement to the loop's OPENING line
|
||||
# (the `for`/`while`), so the `done` line is instrumented but can never be
|
||||
# hit. Measured on a three-line loop: the body reports hits=3 and the
|
||||
# `done >"$1"` line reports hits=0 in the same run.
|
||||
#
|
||||
# The redirect is REQUIRED by the pattern. A bare `done` closing an ordinary
|
||||
# loop is attributed normally and must stay in the denominator, so matching
|
||||
# it here would hide real uncovered code.
|
||||
_LOOP_END_OP = re.compile(r"^\s*done\s*(\||>>?|<|&>)")
|
||||
|
||||
# An assignment whose value opens a multi-line array literal, e.g.
|
||||
# `local -a candidates=(` or `FILES=(`. bash reports the WHOLE assignment at
|
||||
# this opening line and never emits the element lines or the closing paren, so
|
||||
@ -103,6 +115,8 @@ def continuation_lines(source: Path) -> set[int]:
|
||||
opening line of such a construct is a statement.
|
||||
* a line holding just a group's closing brace and an operator (``} |``,
|
||||
``} >>file``). See ``_CLOSING_BRACE_OP``.
|
||||
* a loop terminator carrying a redirect (``done >file``, ``done <file``).
|
||||
See ``_LOOP_END_OP``.
|
||||
* the element lines and closing paren of a multi-line array literal.
|
||||
See ``_ARRAY_OPEN``.
|
||||
"""
|
||||
@ -120,7 +134,11 @@ def continuation_lines(source: Path) -> set[int]:
|
||||
in_array = False
|
||||
elif starts_open:
|
||||
in_array = True
|
||||
if quote is None and not in_array and _CLOSING_BRACE_OP.match(raw):
|
||||
if (
|
||||
quote is None
|
||||
and not in_array
|
||||
and (_CLOSING_BRACE_OP.match(raw) or _LOOP_END_OP.match(raw))
|
||||
):
|
||||
inside.add(number)
|
||||
return inside
|
||||
|
||||
|
||||
@ -75,8 +75,6 @@ linux_configuration/scripts/single_use/features/lib/wg_keys.sh
|
||||
linux_configuration/scripts/single_use/features/lib/wg_sshd.sh
|
||||
linux_configuration/scripts/single_use/fixes/lib/arch_sysctl.sh
|
||||
linux_configuration/scripts/single_use/fixes/lib/bt_adapter.sh
|
||||
linux_configuration/scripts/single_use/fixes/lib/hosts_guard_migrate.sh
|
||||
linux_configuration/scripts/single_use/fixes/lib/hosts_guard_rollback.sh
|
||||
linux_configuration/scripts/single_use/fixes/lib/nvidia_config.sh
|
||||
linux_configuration/scripts/single_use/fixes/lib/pacman_hook_stall_setup.sh
|
||||
linux_configuration/scripts/single_use/fixes/lib/ubuntu_perf_fixes.sh
|
||||
|
||||
Loading…
Reference in New Issue
Block a user