ci: add the per-crate test-count ratchet (#8404)

Quick Checks now fails any PR that lowers the number of test attributes
in a workspace crate or adds #[ignore], measured per crate against the
exact rows of .config/test-count-baseline.txt (rustfs/backlog#2734
section 1 rule 3). The counter widens the spec's literal
#[(tokio::)?test] to the tokio attributes that carry arguments and both
spellings of #[test_case(...)], and anchors at line start so
commented-out attributes do not count. The initial baseline covers 52
crates, 21077 tests and 164 ignored.

Refs rustfs/backlog#2738
Refs rustfs/backlog#2734
This commit is contained in:
Chris
2026-10-08 06:25:42 -07:00
committed by GitHub
parent f2a59073ca
commit c9c16efbb6
6 changed files with 287 additions and 2 deletions
+5
View File
@@ -70,6 +70,11 @@ s3s-footprint-check: ## Check the s3s dependency footprint ratchet stays frozen
@echo "📦 Checking s3s footprint ratchet..."
./scripts/check_s3s_footprint.sh
.PHONY: test-count-ratchet-check
test-count-ratchet-check: ## Check no workspace crate lost tests or gained #[ignore] against the committed baseline
@echo "🧪 Checking per-crate test-count ratchet..."
./scripts/check_test_count_ratchet.sh
.PHONY: fips-wording-check
fips-wording-check: ## Check docs and crates/kms do not over-claim crypto capabilities
@echo "📣 Checking cryptographic capability wording guard..."
+2 -2
View File
@@ -20,11 +20,11 @@ planning-docs-check: ## Check that no planning-type documents are committed
./scripts/check_no_planning_docs.sh
.PHONY: pre-commit
pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check quick-check ## Run fast pre-commit checks without clippy/full tests
pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check test-count-ratchet-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check quick-check ## Run fast pre-commit checks without clippy/full tests
@echo "✅ All pre-commit checks passed!"
.PHONY: pre-pr
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check log-analyzer-rules-check offline-enrollment-e2e-check clippy-check test ## Run full pre-PR checks with clippy and tests
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check error-other-ratchet-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check test-count-ratchet-check fips-wording-check embedded-secrets-check test-wiring-check doc-paths-check planning-docs-check log-analyzer-rules-check offline-enrollment-e2e-check clippy-check test ## Run full pre-PR checks with clippy and tests
@echo "✅ All pre-PR checks passed!"
.PHONY: dev-check
+63
View File
@@ -0,0 +1,63 @@
# Per-crate test-count baseline consumed by scripts/check_test_count_ratchet.sh
# (rustfs/backlog#2734 section 1 rule 3; T0.3 in rustfs/backlog#2738).
#
# One row per workspace member: <package>, tests, ignored, tab-separated.
# tests counts #[test], #[tokio::test], #[tokio::test(...)] and #[test_case(...)]
# attribute lines under the package directory; ignored counts #[ignore] and
# #[ignore = "..."]. Both columns are exact: CI fails when a crate's tests fall
# below or rise above its row, or when its ignored count changes. Raise a row in
# the PR that adds tests; lowering one is a reviewed statement that tests were
# intentionally removed. Regenerate with:
# scripts/check_test_count_ratchet.sh --update
e2e_test 870 18
rustfs 5475 62
rustfs-audit 70 0
rustfs-checksums 26 0
rustfs-common 23 0
rustfs-concurrency 27 0
rustfs-config 43 0
rustfs-connect-symbol-catalog 6 0
rustfs-credentials 24 0
rustfs-crypto 67 0
rustfs-data-usage 54 0
rustfs-ecstore 6203 1
rustfs-extension-schema 23 0
rustfs-filemeta 306 0
rustfs-heal 836 3
rustfs-heal-contracts 6 0
rustfs-iam 357 0
rustfs-io-core 49 0
rustfs-io-metrics 123 0
rustfs-keystone 29 0
rustfs-kms 715 25
rustfs-license 2 0
rustfs-lifecycle 152 0
rustfs-lock 135 0
rustfs-log-analyzer 87 0
rustfs-madmin 209 0
rustfs-notify 136 0
rustfs-object-capacity 92 0
rustfs-object-data-cache 140 0
rustfs-obs 362 0
rustfs-policy 663 0
rustfs-protocols 733 1
rustfs-protos 52 0
rustfs-replication 200 0
rustfs-rio 198 0
rustfs-rio-v2 34 6
rustfs-s3-client 65 0
rustfs-s3-ops 8 0
rustfs-s3-types 21 0
rustfs-s3select-api 206 0
rustfs-s3select-query 188 2
rustfs-scanner 968 20
rustfs-scanner-metrics 75 0
rustfs-security-governance 20 0
rustfs-signer 30 0
rustfs-storage-api 31 0
rustfs-targets 603 26
rustfs-test-utils 1 0
rustfs-tls-runtime 20 0
rustfs-trusted-proxies 68 0
rustfs-utils 216 0
rustfs-zip 30 0
+4
View File
@@ -86,6 +86,10 @@ runs:
shell: bash
run: ./scripts/check_s3s_footprint.sh
- name: Check per-crate test-count ratchet
shell: bash
run: ./scripts/check_test_count_ratchet.sh
- name: Check cryptographic capability wording
shell: bash
run: ./scripts/check_fips_wording.sh
+1
View File
@@ -36,6 +36,7 @@ their issue closes.
| `check_no_planning_docs.sh` | ci-gate | Blocks committed planning-type documents | ci.yml Quick Checks; `make pre-commit` |
| `check_no_tokio_io_uring.sh` | ci-gate | Keeps tokio's io-uring backend disabled | ci.yml Quick Checks |
| `check_s3s_footprint.sh` | ci-gate | Lower-only ratchet freezing the direct s3s surface ahead of the s3gate migration | ci.yml Quick Checks; `make pre-commit` |
| `check_test_count_ratchet.sh` | ci-gate | Exact per-crate test-count and `#[ignore]` ratchet against `.config/test-count-baseline.txt`; no crate may lose tests or gain ignores | ci.yml Quick Checks; `make pre-commit` |
| `check_unsafe_code_allowances.sh` | ci-gate | Unsafe-code allowance ledger guard | ci.yml Quick Checks |
| `layer-dependency-baseline.txt` | ci-gate (data) | Committed baseline consumed by `check_layer_dependencies.sh` | [Architecture guard troubleshooting](../docs/operations/architecture-guard-troubleshooting.md) |
| `static.sh` | ci-gate | Static-build helper executed inside image builds | `Dockerfile.source`, `Dockerfile.decommission-local` |
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env bash
# Per-crate test-count ratchet for the gateway migration (rustfs/backlog#2734
# section 1 rule 3; task T0.3 in rustfs/backlog#2738).
#
# No PR may lower the number of tests in any workspace crate, and no PR may
# add #[ignore]: "delete the test to get green" is rejected in CI. The counter
# is deliberately per crate, never a workspace total — a drop in one crate
# must not hide behind an addition in another.
#
# Workspace members come from `cargo metadata --no-deps` (the same source that
# `Cargo.toml` [workspace].members feeds), so a renamed or added crate is seen
# without a hand-kept list. For each member two counters are taken over every
# .rs file under its package directory (src/, tests/, benches/, examples/):
#
# tests lines matching TEST_PATTERN: #[test], #[tokio::test],
# #[tokio::test(flavor = ...)], and each #[test_case(...)] or
# #[test_case::test_case(...)] case. The spec's literal
# `#\[(tokio::)?test\]` misses the tokio attributes that carry
# arguments and every test_case case (792 and 477 lines when this
# ratchet landed), so the pattern is widened here on purpose.
# ignored lines matching IGNORE_PATTERN: #[ignore] and #[ignore = "..."].
#
# Both patterns are anchored at line start. rustfmt (enforced by Quick Checks)
# keeps attributes on their own line, so the anchor costs nothing and keeps
# doc comments that mention `#[test]` out of the count.
#
# The counts are compared row by row with .config/test-count-baseline.txt
# (`<package>\t<tests>\t<ignored>`, one row per member). The baseline is an
# EXACT value, not a floor, in both columns:
#
# - tests below the row fails: `crate <name>: <actual> < baseline <n>`.
# Restore the tests. If the removal is intentional, lower the row in the
# same PR and justify it in the PR description, so the reduction is visible
# in the diff instead of disappearing into a green check.
# - tests above the row fails as well, telling the author to raise the row in
# the same PR. An exact baseline cannot drift upward silently, so the next
# deletion is always measured against the true count.
# - ignored above the row fails (the line contains `ignored`); adding
# #[ignore] is not accepted on gateway/integration. Ignored below the row
# fails too, asking for the row to be lowered so that ratchet stays tight.
# - a row whose crate is no longer a member fails (deleting a crate deletes
# its tests), and a member without a row fails.
#
# `--update` rewrites the baseline from the current tree. Review the diff it
# produces before committing it; it is the only sanctioned way to change the
# file other than editing the single row a PR touched.
#
# Usage:
# scripts/check_test_count_ratchet.sh # compare against the baseline
# scripts/check_test_count_ratchet.sh --update # regenerate the baseline
set -euo pipefail
cd "$(dirname "$0")/.."
BASELINE_FILE=".config/test-count-baseline.txt"
TEST_PATTERN='^[[:space:]]*#\[((tokio::)?test([]]|\()|(test_case::)?test_case\()'
IGNORE_PATTERN='^[[:space:]]*#\[ignore([]]|[[:space:]]*=)'
mode="check"
case "${1:-}" in
"") ;;
--update) mode="update" ;;
*)
echo "usage: $0 [--update]" >&2
exit 2
;;
esac
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
# One `<package>\t<dir>` line per workspace member, sorted. python3 is already
# a Quick Checks dependency (check_test_wiring.py); jq is not.
cargo metadata --no-deps --format-version 1 --offline |
python3 -c '
import json, os, sys
meta = json.load(sys.stdin)
members = set(meta["workspace_members"])
root = meta["workspace_root"]
for pkg in meta["packages"]:
if pkg["id"] in members:
pkg_dir = os.path.relpath(os.path.dirname(pkg["manifest_path"]), root)
print(pkg["name"] + "\t" + pkg_dir)
' | LC_ALL=C sort >"$TMP_DIR/packages"
if ! grep -q . "$TMP_DIR/packages"; then
echo "error: cargo metadata reported no workspace members" >&2
exit 1
fi
# grep exits 1 on zero matches (a legitimate count of 0 for a crate without
# tests) and >1 on real errors; only the latter may abort the check.
count_matching_lines() {
local pattern="$1" dir="$2" status=0
grep -rE --include='*.rs' -- "$pattern" "$dir" >"$TMP_DIR/matches" || status=$?
if ((status > 1)); then
echo "error: 'grep -rE $pattern $dir' failed with status $status" >&2
exit 1
fi
grep -c . "$TMP_DIR/matches" || true
}
while IFS=$'\t' read -r name dir; do
if [[ ! -d "$dir" ]]; then
echo "error: package $name has no directory at $dir" >&2
exit 1
fi
tests="$(count_matching_lines "$TEST_PATTERN" "$dir")"
ignored="$(count_matching_lines "$IGNORE_PATTERN" "$dir")"
printf '%s\t%s\t%s\n' "$name" "$tests" "$ignored"
done <"$TMP_DIR/packages" >"$TMP_DIR/actual"
crate_count="$(grep -c . "$TMP_DIR/actual")"
total_tests="$(awk -F'\t' '{sum += $2} END {print sum + 0}' "$TMP_DIR/actual")"
total_ignored="$(awk -F'\t' '{sum += $3} END {print sum + 0}' "$TMP_DIR/actual")"
# Sanity assertion: every crate counting 0 means the counter broke (a grep
# that searched the wrong tree or a pattern no rustfmt-formatted attribute
# matches), not that the workspace has no tests. Fail hard rather than
# writing or comparing against an all-zero table.
if ((total_tests == 0)); then
echo "error: counted 0 tests across $crate_count crates; the counter itself is broken" >&2
exit 1
fi
if [[ "$mode" == "update" ]]; then
{
echo "# Per-crate test-count baseline consumed by scripts/check_test_count_ratchet.sh"
echo "# (rustfs/backlog#2734 section 1 rule 3; T0.3 in rustfs/backlog#2738)."
echo "#"
echo "# One row per workspace member: <package>, tests, ignored, tab-separated."
echo "# tests counts #[test], #[tokio::test], #[tokio::test(...)] and #[test_case(...)]"
echo "# attribute lines under the package directory; ignored counts #[ignore] and"
echo "# #[ignore = \"...\"]. Both columns are exact: CI fails when a crate's tests fall"
echo "# below or rise above its row, or when its ignored count changes. Raise a row in"
echo "# the PR that adds tests; lowering one is a reviewed statement that tests were"
echo "# intentionally removed. Regenerate with:"
echo "# scripts/check_test_count_ratchet.sh --update"
cat "$TMP_DIR/actual"
} >"$BASELINE_FILE"
echo "wrote $BASELINE_FILE: $crate_count crates, $total_tests tests, $total_ignored ignored"
exit 0
fi
if [[ ! -f "$BASELINE_FILE" ]]; then
echo "error: $BASELINE_FILE is missing; create it with '$0 --update' and commit it" >&2
exit 1
fi
grep -Ev '^[[:space:]]*(#|$)' "$BASELINE_FILE" >"$TMP_DIR/baseline" || true
if ! grep -q . "$TMP_DIR/baseline"; then
echo "error: $BASELINE_FILE has no baseline rows" >&2
exit 1
fi
if grep -nEv $'^[^\t]+\t[0-9]+\t[0-9]+$' "$TMP_DIR/baseline" >"$TMP_DIR/malformed"; then
printf 'error: malformed rows in %s (expected <package>\\t<tests>\\t<ignored>):\n' "$BASELINE_FILE" >&2
sed 's/^/ line /' "$TMP_DIR/malformed" >&2
exit 1
fi
if cut -f1 "$TMP_DIR/baseline" | LC_ALL=C sort | uniq -d | grep . >"$TMP_DIR/duplicates"; then
echo "error: duplicate rows in $BASELINE_FILE for:" >&2
sed 's/^/ /' "$TMP_DIR/duplicates" >&2
exit 1
fi
# Every violation is one line. The `<actual> < baseline <n>` and `ignored`
# shapes are the acceptance contract of rustfs/backlog#2738; keep them.
awk -F'\t' '
NR == FNR {
baseline_tests[$1] = $2
baseline_ignored[$1] = $3
next
}
{
seen[$1] = 1
if (!($1 in baseline_tests)) {
printf "crate %s: no baseline row (%d tests, %d ignored); add it in this PR\n", $1, $2, $3
next
}
if ($2 + 0 < baseline_tests[$1] + 0) {
printf "crate %s: %d < baseline %d\n", $1, $2, baseline_tests[$1]
} else if ($2 + 0 > baseline_tests[$1] + 0) {
printf "crate %s: %d > baseline %d; raise the baseline row to %d in this PR\n", $1, $2, baseline_tests[$1], $2
}
if ($3 + 0 > baseline_ignored[$1] + 0) {
printf "crate %s: %d ignored > baseline %d; adding #[ignore] is not accepted\n", $1, $3, baseline_ignored[$1]
} else if ($3 + 0 < baseline_ignored[$1] + 0) {
printf "crate %s: %d ignored < baseline %d; lower the baseline row to %d in this PR\n", $1, $3, baseline_ignored[$1], $3
}
}
END {
for (name in baseline_tests) {
if (!(name in seen)) {
printf "crate %s: not a workspace member (baseline %d tests); remove the row in this PR if the crate was intentionally deleted\n", name, baseline_tests[name]
}
}
}
' "$TMP_DIR/baseline" "$TMP_DIR/actual" | LC_ALL=C sort >"$TMP_DIR/violations"
if grep -q . "$TMP_DIR/violations"; then
cat "$TMP_DIR/violations" >&2
echo "error: test-count ratchet violated (rustfs/backlog#2734 section 1 rule 3)." >&2
echo " A crate below its baseline lost tests: restore them. If the removal is" >&2
echo " intentional, lower that row in $BASELINE_FILE in the same PR and" >&2
echo " justify it in the PR description. A crate above its baseline raises its" >&2
echo " row in the same PR ('$0 --update' rewrites the file; review the diff)." >&2
exit 1
fi
echo "test-count ratchet OK: $crate_count crates, $total_tests tests, $total_ignored ignored match $BASELINE_FILE"