Skip to content

test(abc): the test module is ungated, so a minimal-feature build reports ~2,600 spurious failures #1472

Description

@dekobon

Summary

src/metrics/abc.rs's mod tests carries no feature gate, so 2,598 of
3,118
tests in it fail under cargo test -p big-code-analysis --no-default-features --features rust. Every per-language test in the module
is compiled regardless of whether its grammar is enabled.

Why it matters

.claude/rules/testing.md requires a #[cfg(any(feature = ...))] union gate
on any test whose fixture table is feature-dependent, so a build enabling
none of its rows drops the test rather than failing it. Individual modules
inside abc.rs now honour that — the newer ones added across 2026-09 carry
both the union gate and a non-vacuity guard. The enclosing module does
not, so the discipline is being applied test-by-test inside a container that
defeats it.

The practical effect is that a minimal-feature build cannot be used to verify
anything about ABC: the signal is buried under ~2,600 unrelated failures that
are bootstrap artifacts rather than regressions — the same class of
misleading-red as the missing-corpora case #1171 fixed.

A second trap in the same area

A workspace-wide cargo test --no-default-features --features X does not
isolate features at all: big-code-analysis-cli and big-code-analysis-web
depend on the library with default features, and cargo unifies them, so
everything is silently re-enabled. Three different feature sets reported an
identical 4 passed; 3428 filtered out before this was noticed — the
uniform-number tell from .claude/rules/testing.md.

Only -p big-code-analysis isolates. Any feature-gate verification run
without it proves nothing, and proves it while looking green. Worth a line in
.claude/rules/testing.md alongside the existing gate guidance.

Suggested shape

Gate mod tests on the union of every language feature its contents name, or
split the per-language sections into already-gated child modules and gate the
parent on their union. Verify against a subset enabling none of them — note
rust,typescript is not such a subset for any table with a TypeScript or TSX
row, so a single non-listed language (--features go) is the reproducer.

Related


RESOLVED

All three items below are done on fix/1472-gate-per-language-tests
(12 commits, not pushed). Detail in the closing comment. In short:

  1. The 2,594 ungated tests — re-measured as 3,025 across src/,
    big-code-analysis-ast/src/ and tests/; 3,235 markers now, all but
    a handful derived.
  2. The gate that derives the markerutils/check-test-lang-gates.py,
    wired into make pre-commit / make ci. It reproduces all 167 gates
    written by hand in this tree, which is what licensed generating the
    rest.
  3. Subject.carries_tests — fixed, with the matcher on the
    qualified module path.

The feature-matrix CI legs now run the suite rather than only
compiling it, which is what #1285 was waiting for.

One correction to the body below: the "30 of 3,482" figure does not
reproduce. 80 of 3,469 src/ tests carried a feature cfg of their
own.


Update — the CI half is closed; three items remain

PR #1476 closed the CI half of this issue. .github/workflows/ci.yml's
feature-matrix job now documents that it runs clippy and not the suite,
and a new utils/check-feature-gates.py runs in each leg asserting that a
union-gated test subject is absent under a feature set enabling none of
its rows. What remains:

1. The 2,594 ungated per-language lib tests (the original issue)

Unchanged. Measured on fix/batch-2026-09-14: --no-default-features --features rust fails 2,642 of 3,238 tests, 2,594 of them in the lib
target. Root cause is a contract violation, not a missing cfg in the
abstract:

panicked at big-code-analysis-ast/src/node.rs:84:
invariant: the caller checked LANG::is_enabled, or reached this through
AnyParser: LanguageDisabled(Python)

ParserTrait::new's own doc says the same and directs callers to check
LANG::is_enabled first. Only 30 of 3,482 #[test] fns in src/ do.
By module (halved for nextest's double-reporting): abc 525, loc 363,
cognitive 346, halstead 211, cyclomatic 207, npm 187, npa 180, wmc 167,
nargs 136, nexits 111, nom 77, tokens 36, mi 7.

Two approaches were considered and rejected during #1476; record them so
they are not re-proposed:

  • Gate each test. Semantically right, ~2,600 attributes. The hazard is
    that a cross-language parity test needs all(feature = …) where a
    per-language test needs one feature, and a mechanical rewrite that gets
    that backwards produces a test that silently stops running.
  • Make the helpers self-skip on is_enabled(). Only 3 of ~13 helpers
    take a runtime LANG; the rest take a type parameter, and ParserTrait
    exposes no language accessor, so it would need an additive change to a
    published trait. The disqualifier is independent of that: it would turn
    ~2,600 tests into silent no-ops under partial features, which
    .claude/rules/testing.md treats as worse than no test.

2. check-feature-gates.py cannot see a missing gate

A scanner of declared markers reports a clean tree when the marker is
deleted, because deleting it deletes the subject. That is the
hidden_literal_supertypes shape (fixed in 0e15c49c on #1476), and only
its compile half — unused imports at -D warnings — was caught, by clippy.

The gate catches the defeated direction (a union present but weakened,
e.g. a non-feature disjunct), verified both ways during #1476. Catching the
missing direction needs a different gate: derive what the marker should
be from the union of the item's inner #[cfg(feature = …)] rows and compare
it against what is declared. That is the natural companion to item 1, since
the same derivation would drive a mechanical gating pass.

3. A latent false pass in Subject.carries_tests

utils/check-feature-gates.py:265 classifies a mod as test-bearing only
when it carries its own bare cfg(test) predicate:

return (self.kind == "mod" and self.is_cfg_test) or (
    self.kind == "fn" and self.is_test_fn
)

A union-gated mod nested inside an already-#[cfg(test)] parent has no
test in its own attribute, so it is classified compile-time only and
never compared against the nextest listing — a false pass in exactly the
#1220 shape the gate exists to catch. No current subject hits it (all 34 are
top-level #[cfg(test)] mod), so it is latent.

The naive repair is wrong: subject_matcher anchors on the bare name
((?:^|::)name(?:::|$)), so admitting any mod makes a subject named
tests match every ::tests:: path in the crate and fail every disjoint
leg. Narrowing it needs the subject's full module path rather than its bare
name.

Also noticed, adjacent

src/test_support.rs:321assert_csharp_fixture_spells carries no
#[cfg(feature = …)] while its sibling assert_perl_fixture_spells (:329)
carries #[cfg(feature = "perl")]. Both are correct today: the C# callers
are ungated and the Perl ones are gated. It becomes dead_code at
-D warnings the moment a C# test module is gated the way
perl_statement_modifier_parity is — i.e. the first time item 1 is worked
on. Adding the gate before then would break the build.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions