Skip to content

coverage: the clang-coverage preset uses the shared compiler cache, which serves objects across worktrees and silently drops files from the report #426

Description

@Yaraslaut

Found by the N ladder lane while trying to measure morph#411 and morph#412,
and routed through the manager because the compiler cache is shared by every
lane. Structurally confirmed against master be478df1 before filing.

What is wrong

A clang-coverage build uses the shared compiler cache, the cache can serve an
object compiled in a different worktree, and nothing normalises the source
paths that llvm-cov records. The result is a coverage report whose rows carry
two different absolute prefixes for the same logical file.

Measured by N ladder, from a build configured entirely inside
/home/yaraslau/repo/morph-wt/368:

368/examples/crm/include/crm/models/account_model.hpp
372/examples/crm/src/models/account_model.cpp     <- from ANOTHER worktree

The .cpp objects came out of lane 372's worktree through
fastcache-cc (FASTCACHE_ADDR=127.0.0.1:6674).

The three structural facts that make it possible

All read on master be478df1:

  1. The coverage preset does not disable the cache. CMakePresets.json's
    clang-coverage sets CMAKE_BUILD_TYPE=Debug and AF_COVERAGE=ON and
    nothing else, while cmake/CompileCache.cmake:48 declares
    USE_COMPILER_CACHE with default ON.
  2. Nothing normalises paths. grep -rn "coverage-prefix-map\|file-prefix-map\|fprofile-abs-path"
    over cmake/ and CMakeLists.txt returns nothing, so clang embeds absolute
    source paths verbatim into the instrumented objects.
  3. scripts/coverage.sh filters by relative pathSOURCES=(include/morph),
    examples/<rung>/<sub>. A row recorded as .../372/examples/crm/src/...
    cannot match examples/crm/src/models, so it is not merely mis-attributed,
    it vanishes from the report entirely.

What it costs, measured

Two independent consequences, both observed:

  • Whole file sets disappear. In that run, crm's and lims's entire
    src/models/*.cpp set is absent from the combined report, while ledger's and
    kanban's are present.

  • llvm-cov drops functions it cannot reconcile, reporting
    1071 functions have mismatched data. The same file measured two ways:

    file via coverage.sh's 8-object invocation against ladder_common_tests alone
    examples/common/gui/event_poller.hpp 92 lines / 31 missed — 66.30% 106 lines / 1 missed — 99.06%
    presenter.hpp 86.54% 100.00%

A 33-point swing on the same file, from the cache alone.

Why this matters beyond one lane

Every coverage ticket in the current sprint measures locally before proposing a
target: morph#403, morph#402, morph#404, morph#406, morph#409, morph#410,
morph#411, morph#412. No local coverage number is trustworthy while this
holds
, which means a target derived from one is a target derived from noise.
morph#403 in particular is about making the coverage denominator honest; it
cannot succeed while the numerator is contaminated.

What this does NOT claim

CI is probably unaffected, and the Codecov figures may still be real. CI
uses sccache in a single checkout, so there is no second worktree for a path to
come from. The 90.39% (examples/common) and 87.67% (crm) figures that
morph#411 and morph#412 quote are from the Codecov API and are not challenged
here. This issue is about local measurement, and about a hazard CI would
inherit the moment it built from more than one checkout.

Verification status

Reproduced (by N ladder): the two-prefix report, the missing rung sources,
the 1071 functions have mismatched data line, and the two per-file deltas
above. Structurally confirmed (by the manager, independently): the preset's
cache variables, USE_COMPILER_CACHE's ON default, the absence of any
prefix-map flag, and coverage.sh's relative SOURCES filters.

Not verified: that -DUSE_COMPILER_CACHE=OFF alone produces a report
matching CI's numbers. A cache-free rebuild was in progress when this was filed
and its result should be recorded here.

Not verified: whether the same cross-worktree reuse corrupts anything other
than coverage. A non-coverage object carries paths only in debug info, so the
blast radius is probably limited to coverage and to debugger source resolution —
but that is reasoning, not measurement.

What would close this

  1. A clang-coverage configure cannot silently consume a foreign worktree's
    objects. Either AF_COVERAGE=ON forces USE_COMPILER_CACHE=OFF, or the
    build adds -ffile-prefix-map/-fcoverage-prefix-map so recorded paths are
    worktree-independent and cache hits become correct rather than merely fast.
    State which was chosen and why.
  2. A coverage report produced from any worktree contains no path outside the
    configured source directory — checkable mechanically, and worth a check in
    scripts/coverage.sh itself, since a silently shrinking report is precisely
    the failure mode that file's own comments warn about twice (morph#141,
    morph#179).
  3. llvm-cov's functions have mismatched data count is zero, or the
    remaining count is explained. A nonzero count means data is being discarded.
  4. A local cache-free run and CI's Codecov figures are compared and the
    difference stated, so future local measurements have a known relationship to
    the number the project gates on.

Blocks the measurement clauses of morph#403, morph#402, morph#404,
morph#406, morph#409, morph#410, morph#411 and morph#412.

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

    area: ciSubsystem: cibugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions