Skip to content

Compile a source beside its inline-mocked header from an isolated copy - #1328

Merged
mkarlesky merged 2 commits into
next_versionfrom
bug/inline-mock-sibling-source
Oct 9, 2026
Merged

mkarlesky merged 2 commits into
next_versionfrom
bug/inline-mock-sibling-source

Conversation

@mkarlesky

Copy link
Copy Markdown
Member

Fixes :cmock ↳ :treat_inlines: :include for sources that share a directory with the header their test mocks. That is a very common layout: a source and its headers together in src/. In it, the source under test still ran the real inline bodies instead of the mock, and a hardware-touching inline could crash the test. The fault-injection stress rig found this in its own first fixture. It may also be the crash reported in #1240.

Cause

C's #include "..." checks the including file's own directory before any -I path. Under :treat_inlines: :include, CMock writes an inline-stripped copy of each mocked header into the mocks directory, first on the search path. A source beside the real header never consults that path, so #include "board.h" in src/gpio.c finds src/board.h directly.

Ceedling already handles this rule for headers. isolate_sibling_headers stages a sibling header that a test file's compile reaches. The source under test is not a header, and it never appears in the test file's .d file, so nothing redirected it.

Fix

A qualifying source compiles from a copy staged alone in a throwaway directory. With no real header beside the copy, its #include reaches CMock's stripped copy through the search path. The decision is made up front: mocks enabled, :treat_inlines: :include, a real-header mock rather than a Partial mock, a source other than the test file, the same directory as the mocked header, and a quoted #include of that header by name.

Four details keep the copy invisible to everything but header lookup:

  • #line 1 "<original path>" opens the copy. Diagnostics, __FILE__, debug info and coverage all name the original at its original line numbers. Verified with gcov: the report reads File 'src/reader.c', and gcovr's report is unaffected.
  • The original's directory is appended after every other search path. The source's other quoted and relative includes still resolve, and the mocks directory keeps precedence.
  • The .d file is rewritten to name the original before registration. Dependency tracking never records a file that disappears once the compile ends, so a second build skips the object as usual.
  • Plugins and logs keep the original path. Generator#generate_object_file_c gains compile_source: for the command line. arg_hash[:source], which compile_commands_json_db, Bullseye, gcov and command_hooks read, is unchanged.

Each staging lives for one compile and writes nothing to the testable, so it never touches the two-pass barrier in stage_build_objects.

Consolidation

Staging a file away from its neighbors already happened in two places, and this fix needed a third. The first commit extracts QuoteIncludeIsolator (lib/ceedling/quote_include_isolator.rb) with no change in behavior. It owns staging, scoped release, mapping a copy's path back to its original, the optional #line directive, and the include-by-name scan. Bare-includes extraction and sibling-header isolation move onto it, replacing FileWrapper#stage_isolated_copies and #remove_isolated_copies and the executor's private scan. The second commit is the fix, built on the isolator. Each commit passes the full unit and integration tiers on its own.

Changes

  • lib/ceedling/quote_include_isolator.rb: new, wired in lib/ceedling/objects.yml.
  • lib/ceedling/test_invoker/test_build_executor.rb: inline_mocked_sibling_header decides isolation, and compile_isolated_source compiles the copy. Sibling-header isolation now stages through the isolator.
  • lib/ceedling/preprocess/preprocessinator_includes_handler.rb: bare-includes extraction stages through QuoteIncludeIsolator#within.
  • lib/ceedling/generators/generator.rb: compile_source:.
  • lib/ceedling/file_wrapper.rb: the two staging methods are removed.
  • lib/ceedling/test_invoker/HEADER_COLLISIONS.md, lib/ceedling/preprocess/README.md: the isolator as the mechanism's single home, plus a new section on sources under test.

Tests

  • spec/units/quote_include_isolator_spec.rb, new: staging, release on raise, #line escaping for Windows paths, path restoration in plain, make-escaped and binary text, and the include scan.
  • spec/integration/quote_include_isolator_spec.rb, renamed from file_wrapper_isolated_copies_spec.rb: real files plus a real gcc compile of a staged copy. It reaches a shadow header ahead of the real one beside the original, resolves the original's other includes, reports the original as __FILE__, and leaves a dependency file free of the copy.
  • spec/units/test_build_executor_spec.rb: the isolated compile, .d restoration before registration and on failure, the notice, unchanged staleness meta, and an ordinary compile for each non-qualifying case.
  • spec/units/generators/generator_spec.rb, new: the command line receives compile_source while plugin hooks receive :source.
  • spec/system/sibling_header_collision_spec.rb: a source beside a crash-on-purpose inline header passes, and a second build leaves the object fresh. Without the fix, the same test segfaults.

The existing executor and includes-handler specs now use the isolator in place of the FileWrapper staging stubs. Units pass 3992/0 and integration 298/0, along with the include-handling, preprocessing, Partials and gcov system specs.

Not verified

Bullseye could not be run here. Its covc wraps the real command line, so whether its coverage data names the copy or follows #line is unconfirmed. A source reaching the real header only through a private header of its own is not redirected. That is the same one-level limit sibling-header isolation documents.

@mkarlesky
mkarlesky force-pushed the bug/inline-mock-sibling-source branch from 2666a93 to c5e1c2c Compare October 8, 2026 23:02
mkarlesky and others added 2 commits October 8, 2026 19:37
Ceedling copies a file alone into a fresh directory wherever C's quoted
extraction and sibling-header isolation each did this through FileWrapper
staging calls, with the include-by-name scan kept in TestBuildExecutor.

QuoteIncludeIsolator now owns staging, scoped release, mapping a copy's path
back to its original, an optional #line directive that keeps a copy
reporting its original location, and the include-by-name scan. Both
existing uses move onto it with no change in behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under :treat_inlines :include, CMock writes an inline-stripped copy of a
mocked header first on the search path. A source sharing a directory with
the real header still found the real one through C's quote-include rule,
so its inline bodies ran in place of the mock and could crash the test.

Such a source now compiles from a copy staged alone by
QuoteIncludeIsolator. A #line directive keeps diagnostics, __FILE__, debug
info and coverage naming the original. The original's directory follows
every other search path, and the dependency file is restored to name the
original before registration. Plugins and logs still see the original as
:source while Generator receives the copy as compile_source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mkarlesky
mkarlesky force-pushed the bug/inline-mock-sibling-source branch from c5e1c2c to 1448077 Compare October 8, 2026 23:37
@mkarlesky
mkarlesky merged commit 00a6a90 into next_version Oct 9, 2026
21 checks passed
@mkarlesky
mkarlesky deleted the bug/inline-mock-sibling-source branch October 9, 2026 03:36
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