Skip to content

Stage 4: never replace a test file's #include list with an empty one - #1347

Merged
mkarlesky merged 9 commits into
next_versionfrom
fix/stage4-empty-include-list
Oct 17, 2026
Merged

mkarlesky merged 9 commits into
next_versionfrom
fix/stage4-empty-include-list

Conversation

@mkarlesky

Copy link
Copy Markdown
Member

Summary

Stage 4 could replace a test file's #include list with an empty one. The test then lost its mocks and sources, and the build stopped on a compile error about a mock (implicit declaration of function 'Gpt_StartTimer_Expect'). Nothing at default verbosity explained it. Analysis: https://claude.ai/artifact/TR6uCuBLbfdWz9oUNRPcVv

It happened two ways:

  • Both passes failed for one file. Pass 1 reported a failure (no make rule) as [] and cached it as fresh, so one killed or misconfigured run stuck until the test file changed. With pass 2 also failed, reconciling against that [] wiped the list. Since Fallback preprocessing: reconcile test #includes against a plain expansion #1346 this applies in fallback too.
  • -P / --no-line-commands reached pass 2. These strip the line markers pass 2 is read through. They also break mock header preprocessing, and stage 11 then finds zero test cases.

Changes

  • A failed pass 1 is nil, not [].

    • It is logged at obnoxious verbosity with the tool's output.
    • Stage 4 doesn't cache it, so the next run retries, and an empty list stands in meanwhile.
    • A mockable header's or Partial's text scan stands in the same way. Its own cache still keeps the result, which is documented as a best-effort limit.
  • Both passes found nothing. Stage 2's text-scan list stands as it is, as a best effort, with the Partial headers its configuration added.

  • Guard. Reconciliation never replaces a non-empty text-scan list with an empty one. Stage 2's list stays, with a COMPLAIN warning naming the file.

  • Validation. With test preprocessing on, startup validation now rejects -P and --no-line-commands:

    • in the :preprocess and :compile flags of :test and plugin contexts, both array and matcher shapes (:compile flags reach preprocessing by default);
    • in the full and directives-only preprocessor tools' :arguments, naming a :tools_<name> shortcut when that is where the entry was written.

    The new stateless LineMarkerSuppressors finds the entries, and ConfiguratorSetup#validate_line_markers reports each by its configuration path.

  • Refactor. extract_bare_includes and the text scan are broken up. The handler now meets AbcSize and MethodLength, and the RuboCop backlog drops from 838 to 835.

  • Docs. PIPELINE.md and preprocess/README.md are updated. The README's "deduplicated by filename" is corrected to filepath.

Tests (7-step test-first, no new system specs)

  • Pins:
    • a rule naming no #includes gives a cached [];
    • an empty reconciled list is ingested when the text scan is empty too;
    • a failing pass 1 alone still reconciles normally.
  • Red, then green:
    • Handler: nil and its log.
    • Header path: survives nil.
    • Stage 4: no cache on failure.
    • Both failed: keeps the text scan, in both modes.
    • Guard: header and source-only cases.
    • LineMarkerSuppressors: shapes, -Wp,, joined entries, no false positives, malformed input, shortcut attribution.
    • validate_line_markers: including :none skipping the check.
  • Integration (real GCC). The harnesses gained bare_tool: and flags:. FAILING_BARE_TOOL (an unrecognized option) reproduces a pass 1 failure. Examples run in both modes:
    • both passes failing keeps the mocks;
    • a failed pass 1 writes no cache;
    • -P hits the guard;
    • a header with a failing bare tool equals the success result;
    • configurator_pipeline_spec rejects -P and a shortcut's --no-line-commands, and accepts -P under :none.
  • Runs.
    • Units (4716) and integration (480) pass on Apple Clang 15. Integration passes on GCC 14 in Docker.
    • System regressions pass in Docker: conditional_mock_includes, conditional_include_macro_visibility, preprocessing_fallback_conditionals, delta_builds, partials_module_disambiguation, same_named_modules, include_path_reconciliation, test_source_file_directive.
  • RuboCop: 835, with no new file in the backlog. lint:changed is clean.

Notes for review

  • Guard false positive. A test file whose every #include sits in a branch that is off now keeps its text scan and warns, where it used to get the correct empty list. That needs no unconditional #include at all, not even unity.h.
  • Both-failed path. Keeping the text scan means mocks in branches that are off stay. That is the best-effort trade.
  • Breaking change. Rejecting -P breaks a project that sets it with test preprocessing on. Such builds already fail or run zero tests.
  • Release docs. Changelog, KnownIssues, BreakingChanges and mkdocs edits will follow as uncommitted diffs after CI.

🤖 Generated with Claude Code

mkarlesky and others added 9 commits October 17, 2026 15:18
A make rule naming no #includes is a file with none, and an empty
reconciled list is that file's true list when its text names none either.
A failing first pass alone still reconciles as usual. The integration
harnesses can now replace the bare-includes tool and set preprocess flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bare-includes pass now reads as its steps: run the tool on an isolated
copy, check for a make rule, and parse it. The text scan's walk over active
lines is its own method. Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bare-includes run that gives no make rule failed as a tool; the file's
own errors still give one. It used to come back as an empty list, cached
as fresh, so one killed or misconfigured run was remembered until the test
file changed. It now gives nil, logged with the tool's output at
obnoxious verbosity. Stage 4 skips caching it, so the next run tries
again, and uses an empty list meanwhile. A header's or Partial's text scan
stands in the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When GCC's bare list is empty and the second pass failed, nothing is left
to check a test file's text scan against, and reconciling against the
empty list wiped it. Stage 2's list now stands as it is, as a best effort,
the Partial headers its configuration added included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reconciling a test file to no #includes while its text names some means
preprocessing found none it should have, as when the second pass's output
has no line markers. Ingesting that empty list replaced the test's header
and source #includes, so its mocks and sources vanished and the build
stopped on a compile error about a mock. Stage 2's list now stands, with a
warning at default verbosity naming the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test preprocessing reads preprocessor line markers to find a file's
#includes, its mockable functions, and its test cases. -P and
--no-line-commands drop them, yet the preprocessor still succeeds, so a
test lost its #includes, a mock its functions, or a test file its test
cases without any error saying why. Configuration validation now names
each such entry in the flags that reach test preprocessing, :compile flags
included, and in the preprocessor tools' arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The includes handler now meets AbcSize and MethodLength, taking the total
from 838 to 835.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mkarlesky
mkarlesky merged commit ec68b86 into next_version Oct 17, 2026
22 checks passed
@mkarlesky
mkarlesky deleted the fix/stage4-empty-include-list branch October 17, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant