243 lines
9.6 KiB
Bash
Executable File
243 lines
9.6 KiB
Bash
Executable File
#!/bin/sh
|
|
# scripts/feature-census --- measure which tests a feature flag HIDES.
|
|
#
|
|
# scripts/feature-census <config-a> <config-b> [--covers <test-name>]
|
|
#
|
|
# Both arguments are `--features` strings, applied with
|
|
# `--no-default-features`. Config A is the baseline (typically CI's
|
|
# flags); config B is the richer one. The script reports every test that
|
|
# exists under B and NOT under A --- the "dark" set.
|
|
#
|
|
# scripts/feature-census luajit luajit,crdt
|
|
# scripts/feature-census luajit luajit,crdt --covers crdt_undo_keeps_invariant
|
|
#
|
|
# WHY THIS EXISTS. A cargo feature is a COMPILE-TIME gate, so a test
|
|
# behind one is not skipped or filtered --- it does not exist. Nothing in
|
|
# a test run says so: the suite reports `ok`, the counts look plausible,
|
|
# and the absent tests are absent from the report too. `.github/
|
|
# workflows/ci.yml` never enabled `crdt` for the project's whole life,
|
|
# and 279 tests --- including 186 library tests behind a REQUIRED
|
|
# CLAUDE.md gate --- had never executed in CI. Nobody was ignoring a red
|
|
# signal; there was no signal.
|
|
#
|
|
# docs/active-work.md therefore says the figure "moves with every merge
|
|
# and must be re-measured, not quoted." That instruction had no tool, so
|
|
# every re-measurement was a hand-rolled `--list` pipeline. This is that
|
|
# tool.
|
|
#
|
|
# EXIT STATUS: 0 when the census completes (and, with --covers, the
|
|
# claim holds); 1 when a --covers claim FAILS; 2 on usage errors; 3 when
|
|
# either configuration FAILS TO BUILD.
|
|
#
|
|
# Exit 3 is the important one, and it is fail-closed on purpose. A
|
|
# configuration that does not compile produces no test list, which is
|
|
# indistinguishable by counting from "this configuration contains no
|
|
# tests" --- and would render as a spectacular, entirely false, "every
|
|
# test is dark under A." A census whose baseline did not build is not a
|
|
# small error; it is a number that will be quoted.
|
|
#
|
|
# PARSING NOTES, each one a mistake made while writing this:
|
|
#
|
|
# * libtest prints `some::path::name: test` with NO SPACE before the
|
|
# colon. A filter written as `/ : test$/` matches nothing, reports
|
|
# zero targets, and looks like a clean run. Cost: one silently empty
|
|
# census. The pattern below is anchored to end-of-line instead.
|
|
#
|
|
# * `--list` also emits `: benchmark` lines. Only `: test` is counted.
|
|
#
|
|
# * Target attribution comes from cargo's `Running` lines, which have
|
|
# TWO shapes: `Running unittests src/lib.rs (...)` for a lib/bin
|
|
# target and `Running tests/foo.rs (...)` for an integration test.
|
|
# Reading a fixed field number handles one and mangles the other.
|
|
#
|
|
# * A target with zero tests still prints its `Running` line and then
|
|
# nothing. Counting only test lines DROPS such targets entirely, so
|
|
# they never appear in the diff --- which is precisely the finding
|
|
# worth surfacing (eight binaries reported `ok` with nothing in
|
|
# them). They are tracked from the `Running` line, not inferred from
|
|
# having tests.
|
|
#
|
|
# * `--list` includes #[ignore]d tests. They are dark in the same
|
|
# sense but are NOT recovered by adding the feature to an ordinary
|
|
# test job --- that needs `--ignored`. Conflating the two overstates
|
|
# what a fix delivers, so ignored tests are counted separately via a
|
|
# second `--list --ignored` pass.
|
|
#
|
|
# SCOPE LIMIT, and it is structural rather than an omission: this
|
|
# censuses the workspace DEFAULT MEMBER only (`pmacs`), because that is
|
|
# what a bare `cargo test --all-targets` builds. Sibling crates
|
|
# --- pmacs-protocol, pmacs-gpu --- are invisible to it no matter what
|
|
# configs are passed.
|
|
#
|
|
# That blind spot is not hypothetical. pmacs-protocol has its own
|
|
# `crdt` feature which gates NO tests, so a census of it would report
|
|
# zero dark either way --- yet the feature changes
|
|
# `cfg!(feature = "crdt")` expressions inside the capability defaults,
|
|
# so its 17 tests exercise different runtime values under it. A feature
|
|
# can therefore matter to a crate this script would score as
|
|
# unaffected. Check sibling crates by hand.
|
|
#
|
|
# CARGO_TERM_COLOR is pinned off so escape codes cannot break the
|
|
# anchored matches, the same precaution and for the same reason as
|
|
# scripts/bite.
|
|
|
|
set -eu
|
|
|
|
usage() {
|
|
echo "usage: scripts/feature-census <config-a> <config-b> [--covers <test-name>]" >&2
|
|
exit 2
|
|
}
|
|
|
|
[ "$#" -ge 2 ] || usage
|
|
|
|
config_a=$1
|
|
config_b=$2
|
|
shift 2
|
|
|
|
covers=""
|
|
while [ "$#" -gt 0 ]; do
|
|
case $1 in
|
|
--covers)
|
|
[ "$#" -ge 2 ] || usage
|
|
covers=$2
|
|
shift 2
|
|
;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
|
|
export CARGO_TERM_COLOR=never
|
|
|
|
work=$(mktemp -d "${TMPDIR:-/tmp}/feature-census.XXXXXX")
|
|
trap 'rm -rf -- "$work"' EXIT INT TERM
|
|
|
|
# Emit "<count> <target>" per target. Targets with zero tests are
|
|
# emitted with a count of 0 rather than omitted.
|
|
parse_list() {
|
|
awk '
|
|
/^ *Running /{
|
|
t = $2
|
|
if (t == "unittests") t = $3
|
|
sub(/.*\//, "", t)
|
|
sub(/\.rs$/, "", t)
|
|
seen[t] = 1
|
|
next
|
|
}
|
|
/: test$/ { c[t]++ }
|
|
END { for (k in seen) printf "%d %s\n", c[k] + 0, k }
|
|
' | sort -k2
|
|
}
|
|
|
|
# $1 = features, $2 = output basename, $3... = extra libtest args
|
|
run_list() {
|
|
features=$1
|
|
out=$2
|
|
shift 2
|
|
if ! cargo test --all-targets --no-default-features --features "$features" \
|
|
-- --list "$@" > "$work/$out.raw" 2>&1; then
|
|
echo "feature-census: CONFIGURATION FAILED TO BUILD: --features $features" >&2
|
|
echo "feature-census: refusing to report a census against a config that does not" >&2
|
|
echo "feature-census: compile --- every test would read as absent. Last output:" >&2
|
|
tail -n 20 "$work/$out.raw" >&2
|
|
exit 3
|
|
fi
|
|
parse_list < "$work/$out.raw" > "$work/$out.counts"
|
|
}
|
|
|
|
echo "feature-census: listing A (--features $config_a)" >&2
|
|
run_list "$config_a" a
|
|
echo "feature-census: listing B (--features $config_b)" >&2
|
|
run_list "$config_b" b
|
|
# BOTH sides need an ignored pass. A test that is #[ignore]d under A as
|
|
# well as B is not "dark and ignored" --- it was already there. Counting
|
|
# only B's ignored set attributes pre-existing ignores to the feature
|
|
# and overstates the untouchable remainder.
|
|
echo "feature-census: listing A ignored" >&2
|
|
run_list "$config_a" a_ignored --ignored
|
|
echo "feature-census: listing B ignored" >&2
|
|
run_list "$config_b" b_ignored --ignored
|
|
|
|
total_a=$(awk '{s+=$1} END {print s+0}' "$work/a.counts")
|
|
total_b=$(awk '{s+=$1} END {print s+0}' "$work/b.counts")
|
|
targets_a=$(wc -l < "$work/a.counts" | tr -d ' ')
|
|
targets_b=$(wc -l < "$work/b.counts" | tr -d ' ')
|
|
|
|
echo
|
|
echo " config A: --features $config_a"
|
|
echo " config B: --features $config_b"
|
|
echo
|
|
|
|
# One pass over all four count files, keyed by target, so every derived
|
|
# figure comes from the same table rather than a chain of joins whose
|
|
# defaulting rules have to agree.
|
|
awk '
|
|
FILENAME ~ /a\.counts$/ { a[$2] = $1; seen[$2] = 1; next }
|
|
FILENAME ~ /b\.counts$/ { b[$2] = $1; seen[$2] = 1; next }
|
|
FILENAME ~ /a_ignored\.counts$/{ ai[$2] = $1; next }
|
|
FILENAME ~ /b_ignored\.counts$/{ bi[$2] = $1; next }
|
|
END {
|
|
for (t in seen) {
|
|
d = b[t] - a[t]
|
|
if (d <= 0) continue
|
|
# A dark test is ignored only if B ignores MORE than A does.
|
|
di = bi[t] - ai[t]
|
|
if (di < 0) di = 0
|
|
if (di > d) di = d
|
|
printf "row %6d %6d %6d %6d %s\n", d, a[t], b[t], di, t
|
|
dark += d; darkt++; darkig += di
|
|
}
|
|
printf "sum %d %d %d\n", dark + 0, darkt + 0, darkig + 0
|
|
}
|
|
' "$work/a.counts" "$work/b.counts" "$work/a_ignored.counts" "$work/b_ignored.counts" \
|
|
> "$work/table"
|
|
|
|
awk '$1 == "row" { printf "%6d dark | %6d A | %6d B | %s%s\n", $2, $3, $4, ($5 > 0 ? "(" $5 " ignored) " : ""), $6 }' \
|
|
"$work/table" | sort -rn
|
|
|
|
read_sum() { awk -v f="$1" '$1 == "sum" { print $(f + 1) }' "$work/table"; }
|
|
dark_total=$(read_sum 1)
|
|
dark_targets=$(read_sum 2)
|
|
dark_ignored=$(read_sum 3)
|
|
|
|
# Targets with zero tests under A. These build, run, and report `ok`
|
|
# with nothing in them. Split by whether B gives them any, because the
|
|
# two mean different things: one is a target the feature unlocks, the
|
|
# other is a binary that simply has no tests at all.
|
|
empty_a=$(awk '$1 == 0 { n++ } END { print n+0 }' "$work/a.counts")
|
|
empty_a_filled_by_b=$(awk '
|
|
FILENAME ~ /a\.counts$/ { a[$2] = $1; next }
|
|
FILENAME ~ /b\.counts$/ { if (a[$2] == 0 && $1 > 0) n++ }
|
|
END { print n+0 }
|
|
' "$work/a.counts" "$work/b.counts")
|
|
|
|
echo
|
|
echo " A: $total_a tests across $targets_a targets"
|
|
echo " B: $total_b tests across $targets_b targets"
|
|
echo " DARK: $dark_total tests across $dark_targets targets"
|
|
echo " of which #[ignore]d under B but not A: $dark_ignored"
|
|
echo " recovered by adding B's features to a plain test job: $((dark_total - dark_ignored))"
|
|
echo " (the remainder needs an --ignored invocation, not just the feature)"
|
|
echo
|
|
echo " $empty_a target(s) run under A with ZERO tests --- they report ok with nothing in them."
|
|
echo " $empty_a_filled_by_b of those gain tests under B; the rest have no tests in either."
|
|
|
|
if [ -n "$covers" ]; then
|
|
echo
|
|
in_b=no
|
|
in_a=no
|
|
grep -q "^${covers}: test$\|::${covers}: test$" "$work/b.raw" && in_b=yes
|
|
grep -q "^${covers}: test$\|::${covers}: test$" "$work/a.raw" && in_a=yes
|
|
echo " --covers $covers"
|
|
echo " present under B: $in_b"
|
|
echo " present under A: $in_a"
|
|
if [ "$in_b" = yes ] && [ "$in_a" = no ]; then
|
|
echo " => B-only. A configuration running only A cannot see this test."
|
|
elif [ "$in_b" = no ]; then
|
|
echo " => CLAIM FAILED: not present under B at all (misspelled?)." >&2
|
|
exit 1
|
|
else
|
|
echo " => CLAIM FAILED: present under A too, so B is not load-bearing for it." >&2
|
|
exit 1
|
|
fi
|
|
fi
|