[llvm] [llvm-cov] Add failing test for gap region line coverage bug (PR #203740)

Maksim Levental via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 13 19:13:45 PDT 2026


https://github.com/makslevental created https://github.com/llvm/llvm-project/pull/203740

## Summary

Fix a bug in `LineCoverageStats` where lines are incorrectly reported as uncovered (count=0) when:

1. The wrapping segment is a **gap region** with count=0
2. The line has no region entry segments (`MinRegionCount == 0`)
3. The line has non-entry, non-gap segments with count > 0

This causes `llvm-cov show` and `llvm-cov report` to display count=0 for lines that are genuinely executed.

## Example

```cpp
bool process(bool err1, bool err2) {
  {
    int fd = 0;
    (void)fd;
    if (err1) {
      return false;
    }
    fd = 1;
    (void)fd;
  }
  int result = 42;   // <-- BUG: reported as count=0, actually executed
  (void)result;
  {
    int fd2 = 0;
    (void)fd2;
    if (err2) {
      return false;
    }
    fd2 = 1;
    (void)fd2;
  }
  return true;        // <-- BUG: same issue
}
```

When `process(false, false)` is called, lines 11 and 22 are executed but reported with count=0 because a gap region (from the closing `}` of the scoped block) wraps into those lines and suppresses their execution count.

## Root cause

In `LineCoverageStats::LineCoverageStats()`, when `MinRegionCount == 0`:

```cpp
if (WrappedSegment)
  ExecutionCount = WrappedSegment->Count;  // gap has count=0
if (!MinRegionCount)
  return;  // returns early with ExecutionCount=0
```

The non-entry segment on the line (with count > 0) is never considered.

## Fix

When `MinRegionCount == 0` AND the wrapping segment is a gap region, iterate `LineSegments` for non-gap, `HasCount` segments and use their max count:

```cpp
if (WrappedSegment)
  ExecutionCount = WrappedSegment->Count;
if (!MinRegionCount) {
  if (WrappedSegment && WrappedSegment->IsGapRegion) {
    for (const auto *LS : LineSegments)
      if (!LS->IsGapRegion && LS->HasCount)
        ExecutionCount = std::max(ExecutionCount, LS->Count);
  }
  return;
}
```

When the wrapping segment is NOT a gap, the existing behavior is preserved — this avoids incorrectly inflating counts from region-end markers of outer regions.

## Test plan

- Added `llvm/test/tools/llvm-cov/gap-region-line-coverage.test`
- Test inputs generated with `clang++ -fprofile-instr-generate -fcoverage-mapping -O0` (both Apple and upstream clang produce the same gap-region segment pattern)
- Verifies both `show` (line displays count=1) and `report` (line not counted as missed)
- All existing `ProfileDataTests/test_line_coverage_iterator` unit tests pass
- All existing llvm-cov lit tests pass

## PR structure

1. **First commit** (`e15997697`): Adds the failing test — CI should show it failing.
2. **Second commit**: Adds the fix — CI should show the test passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


>From e159976973d0eb8b23f9d4821a77446ba3b18027 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Sat, 13 Jun 2026 16:33:04 -0700
Subject: [PATCH] [llvm-cov] Add failing test for gap region line coverage bug

LineCoverageStats incorrectly reports lines as uncovered when a gap
region with count=0 wraps into a line that has non-entry segments
with count > 0.

The test demonstrates this with two scoped blocks containing
never-taken early returns. The statement between them ("int result =
42;") and after them ("return true;") are genuinely executed but
reported with count=0 because the gap region from each block's
closing "}" incorrectly suppresses the line's execution count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 .../Inputs/gap-region-quirk/gap-quirk-v2.cpp  |  28 ++++++++++++
 .../gap-region-quirk/gap-quirk.covmapping     | Bin 0 -> 269 bytes
 .../gap-region-quirk/gap-quirk.profdata       | Bin 0 -> 728 bytes
 .../llvm-cov/gap-region-line-coverage.test    |  41 ++++++++++++++++++
 4 files changed, 69 insertions(+)
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata
 create mode 100644 llvm/test/tools/llvm-cov/gap-region-line-coverage.test

diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp
new file mode 100644
index 0000000000000..97e573e50bb24
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp
@@ -0,0 +1,28 @@
+bool process(bool err1, bool err2) {
+  {
+    int fd = 0;
+    (void)fd;
+    if (err1) {
+      return false;
+    }
+    fd = 1;
+    (void)fd;
+  }
+  int result = 42;
+  (void)result;
+  {
+    int fd2 = 0;
+    (void)fd2;
+    if (err2) {
+      return false;
+    }
+    fd2 = 1;
+    (void)fd2;
+  }
+  return true;
+}
+
+int main() {
+  process(false, false);
+  return 0;
+}
diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping
new file mode 100644
index 0000000000000000000000000000000000000000..9b969b483c3f23e89d460cd5bbbabef231d34985
GIT binary patch
literal 269
zcmd1FDa%dHFUw_QfB at kItbz>jQRW3j`N^rp#Ystwxrv#1I$%jjs2VmP%_PRaqF<6*
zAdsF|pj%j)S(L3?W~7%~PykiK_s_-O)$`Q3Vjw at F#nY?BlZ!$7Yt5N9Bl*9Kj4X_-
zOq?Ld&&a4E#>B|N$-t|?%ESPqSQ+>j`WqS=I9M6 at nb??^*ce+uJSIj~ekKMc7RF{U
zj}4 at OlMkeU6Q+R^q=65nfsc`unGddk;n&sP=VmEWZ*v0OBLQ_I!aWR(jFLReOaKXY
BHDdq(

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata
new file mode 100644
index 0000000000000000000000000000000000000000..de47b1163e8d322c97dc20c07939f0cc6d8c1f4c
GIT binary patch
literal 728
zcmeyLQ&5zjmf6V5fE{dLLKQ!O#=ijNvq6;!KxvpV7AT()jfQd=VCn?aVIrvXf;N~=
zRB;DhW>f(v at 4;l4ji}NMQZN^yiYqK+2MfS#fw|{^Iww}~gmqk4#TiU_u!=9(4GCFv
z^FP$_VO4J+&5u<)!VbIm9Y^fqW=`0}?Qw`RG5osP``j#L>TOu~X at K3qz>u4mnI{2d
z!+Zgwp~4ItPzK*W7k^jJQ|BOI!N6bv6_1ZH2PU)R)Z*f#q=*(zuNF@(23Y)|n*}oy
UM#Id8 at L{T9 at x%dRz?4I20Gc at _0ssI2

literal 0
HcmV?d00001

diff --git a/llvm/test/tools/llvm-cov/gap-region-line-coverage.test b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
new file mode 100644
index 0000000000000..e81572a9d5931
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
@@ -0,0 +1,41 @@
+// Tests that LineCoverageStats correctly handles lines where a gap region
+// with count=0 wraps into a line that has a non-entry segment with count > 0.
+//
+// In this test, line 11 ("int result = 42;") is between two scoped blocks
+// each containing a never-taken early return. The gap region from the first
+// block's closing "}" wraps to line 11, but line 11 has a non-entry segment
+// with count=1 (the function body continues). LineCoverageStats should
+// report count=1 for line 11, not count=0.
+//
+// Similarly, line 22 ("return true;") is after the second block and has
+// the same issue.
+//
+// Test inputs generated with:
+//   %clang -fprofile-instr-generate -fcoverage-mapping -O0 \
+//       -c -o gap-quirk.o Inputs/gap-region-quirk/gap-quirk-v2.cpp
+//   llvm-cov convert-for-testing gap-quirk.o -o gap-quirk.covmapping
+//   %clang -fprofile-instr-generate -fcoverage-mapping -O0 \
+//       -o gap-quirk Inputs/gap-region-quirk/gap-quirk-v2.cpp
+//   LLVM_PROFILE_FILE=gap-quirk.profraw ./gap-quirk
+//   llvm-profdata merge --sparse gap-quirk.profraw -o gap-quirk.profdata
+
+// RUN: llvm-cov show %S/Inputs/gap-region-quirk/gap-quirk.covmapping \
+// RUN:   -instr-profile %S/Inputs/gap-region-quirk/gap-quirk.profdata \
+// RUN:   -path-equivalence=/tmp,%S/Inputs/gap-region-quirk \
+// RUN:   2>&1 | FileCheck -check-prefix=SHOW %s
+
+// Lines between the two blocks should show count 1, not 0.
+// SHOW:     11|      1|  int result = 42;
+// SHOW:     22|      1|  return true;
+
+// RUN: llvm-cov report %S/Inputs/gap-region-quirk/gap-quirk.covmapping \
+// RUN:   -instr-profile %S/Inputs/gap-region-quirk/gap-quirk.profdata \
+// RUN:   -path-equivalence=/tmp,%S/Inputs/gap-region-quirk \
+// RUN:   --show-branch-summary=false \
+// RUN:   2>&1 | FileCheck -check-prefix=REPORT %s
+
+// 22 mapped lines, 4 genuinely missed (lines 6-7, 17-18: never-taken returns).
+// Lines 11 and 22 should NOT be counted as missed.
+// REPORT: TOTAL
+// REPORT-SAME: 27
+// REPORT-SAME: 4



More information about the llvm-commits mailing list