testsAndMisc/.github/workflows/shell-tests.yml
Krzysztof kuhy Rudnicki 170f843481
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Make the jailed lib/tests gate able to fail, without breaking measurement
The jail recorded each case's exit code but reported only timeouts,
discarding every other non-zero status. A suite with a failing assertion
exited 0 through the push gate -- the same blind spot that let an earlier
session ship 2 failing assertions in the nc_php suite. Found by negative
control, not by reading: a deliberately failing assertion still exited 0.

Propagating that status unconditionally was wrong, and caught before landing:
the jail's own usage example passes a `bogus` case that exits non-zero to
cover an error path, so a clean 5/5 = 100.00% run started failing. That is
the mode the 105-lib campaign runs in.

So the behaviour sits behind --fail-on-case-error, default off, passed only
by ci_mirror.sh and shell-tests.yml. Verified in all four quadrants:
measurement + error case = 0, gate + error case = 1, gate + clean suite = 0,
gate + broken assertion = 1.

Both gates also pass --min 0: with no --measure the jail was measuring
run_all.sh itself against its default --min 100 and failing the push at
54.55%, a coverage verdict neither gate asked for. ci_mirror.sh now prints
the jail's output on failure, so a red gate can be diagnosed.

The added flag pushed the jail over the 250-line cap, so the in-namespace
case runner moved to lib/shell_coverage_case_runner.sh, following the
existing lib/shell_coverage_jail_setup.sh split. All four quadrants were
re-run after the extraction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
2026-08-22 12:16:18 +02:00

163 lines
6.3 KiB
YAML

name: Shell tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
shell-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# kcov + unshare back shell_coverage_jail.sh, which the jail_args-marked
# lib/tests suites below run under. util-linux (unshare) is preinstalled;
# kcov is not.
- name: Install bats and kcov
run: sudo apt-get update && sudo apt-get install -y bats kcov
- name: Run bats suites
run: bats linux_configuration/tests/*.bats
# Only the side-effect-free tests run here. The rest of
# linux_configuration/tests/ drives pacman, /etc, systemd or adb and
# cannot run on an Ubuntu runner without root and an Arch container --
# see the accompanying evidence artifacts for the full list.
- name: Run side-effect-free shell tests
run: |
set -euo pipefail
tests=(
test_android_guardian_post_fs_data.sh
test_android_guardian_service.sh
test_i3blocks_persist_common.sh
test_music_parallelism.sh
test_setup_dns_blocker.sh
test_usage_monitoring_installer_efficiency.sh
)
failed=0
for t in "${tests[@]}"; do
printf '\n=== %s ===\n' "$t"
if bash "linux_configuration/tests/$t"; then
printf 'PASS %s\n' "$t"
else
printf 'FAIL %s\n' "$t"
failed=$((failed + 1))
fi
done
if [ "$failed" -gt 0 ]; then
printf '\n%d shell test(s) failed\n' "$failed"
exit 1
fi
# The lib/tests/ suites that came out of the 250-line-cap splits. These
# are DISCOVERED rather than listed: every split adds another
# lib/tests/run_all.sh, and a hardcoded list silently leaves each new
# suite running only on the author's machine. Four such runners already
# existed and none of them ran in CI until this step was added.
#
# Each runner is invoked directly, not as `bash <runner>`: the same
# invocation the coverage wrapper needs, and it keeps the shebang
# authoritative.
- name: Run lib/tests suites
run: |
set -euo pipefail
mapfile -t runners < <(find . -path ./node_modules -prune -o \
-path '*/lib/tests/run_all.sh' -print | sort)
if [ "${#runners[@]}" -eq 0 ]; then
printf 'No lib/tests/run_all.sh found — the discovery glob is wrong\n'
exit 1
fi
failed=0
for r in "${runners[@]}"; do
printf '\n=== %s ===\n' "$r"
# A suite that writes to real system paths ships a `jail_args`
# file and REFUSES to run bare; run it through the namespace jail
# with the flags it declares. Empty marker = configuration error.
args_file="${r%/run_all.sh}/jail_args"
if [ -f "$args_file" ]; then
mapfile -t jail_args < <(grep -v '^#\|^$' "$args_file" || true)
if [ "${#jail_args[@]}" -eq 0 ]; then
printf 'FAIL %s (jail_args present but empty)\n' "$r"
failed=$((failed + 1))
continue
fi
# --min 0: this step gates on the ASSERTIONS, not on coverage.
# The jail's default --min 100 would otherwise measure the
# runner itself and fail on a number this step never asked for;
# the coverage bar belongs to check_shell_coverage.sh.
cmd=(bash meta/scripts/shell_coverage_jail.sh \
--subject "$r" "${jail_args[@]}" --min 0 --fail-on-case-error -- "")
else
cmd=("$r")
fi
if "${cmd[@]}"; then
printf 'PASS %s\n' "$r"
else
printf 'FAIL %s\n' "$r"
failed=$((failed + 1))
fi
done
if [ "$failed" -gt 0 ]; then
printf '\n%d lib/tests suite(s) failed\n' "$failed"
exit 1
fi
# Tests that assume Arch: they exercise the pacman wrapper, makepkg wrapper
# and hosts-guard hooks, so they need an Arch userland rather than Ubuntu.
# They run as root here, which is what lets them touch /etc safely — the
# container is disposable.
arch-tests:
runs-on: ubuntu-latest
container:
image: archlinux:base-devel
# test_i3blocks_efficiency.sh counts execve calls with strace, which needs
# ptrace. Without these the trace file is never produced.
options: --cap-add=SYS_PTRACE --security-opt seccomp=unconfined
steps:
# base-devel ships none of these: git (actions/checkout needs it), strace
# (execve counting), or jq (the claude_usage block parses its cache with
# it, and without jq that block degrades to "no data" and its assertions
# fail).
- name: Install dependencies
run: pacman -Sy --noconfirm --needed git strace jq
- uses: actions/checkout@v4
# The complete Arch-dependent set. All eight verified locally in
# `docker run --cap-add=SYS_PTRACE archlinux:base-devel`.
#
# test_security_hardening.sh inspects a configured machine (chattr flags,
# systemd guard units, /usr/local payloads). It detects that it is not on
# one and reports those checks as skipped, while still running its
# repository checks — so it is meaningful here AND on the real host,
# where it continues to fail loudly if a guard unit is down.
- name: Run Arch-specific shell tests
run: |
set -euo pipefail
tests=(
test_hosts_file_monitor.sh
test_hosts_guard_pacman_integration.sh
test_i3blocks_efficiency.sh
test_makepkg_capped.sh
test_makepkg_wrapper.sh
test_pacman_wrapper_security.sh
test_security_hardening.sh
test_shutdown_timer_monitor.sh
)
failed=0
for t in "${tests[@]}"; do
printf '\n=== %s ===\n' "$t"
if bash "linux_configuration/tests/$t"; then
printf 'PASS %s\n' "$t"
else
printf 'FAIL %s\n' "$t"
failed=$((failed + 1))
fi
done
if [ "$failed" -gt 0 ]; then
printf '\n%d shell test(s) failed\n' "$failed"
exit 1
fi