[llvm] [llvm-cov][CoverageMapping] Fix LineCoverageStats incorrectly using gap region count (PR #203740)

Maksim Levental via llvm-commits llvm-commits at lists.llvm.org
Sun Jun 14 10:20:57 PDT 2026


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

>From 0034a6791064811f7b342283b457468a6d0807e3 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 1/3] [llvm-cov] Add failing test for gap region line coverage
 bug
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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 uses a minimal reproducer: a scoped block with a never-taken
early return followed by a statement. The closing "}" produces a gap
region that wraps to the next line, suppressing its execution count.
The extra statement after the if-block is required — without it,
clang emits a region entry (MinRegionCount > 0) and the bug doesn't
trigger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 .../Inputs/gap-region-quirk/gap-quirk-v2.cpp  |  23 +++++++++++
 .../gap-region-quirk/gap-quirk.covmapping     | Bin 0 -> 213 bytes
 .../gap-region-quirk/gap-quirk.profdata       | Bin 0 -> 720 bytes
 .../llvm-cov/gap-region-line-coverage.test    |  37 ++++++++++++++++++
 4 files changed, 60 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..f235382db64e8
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk-v2.cpp
@@ -0,0 +1,23 @@
+// A scoped block with a never-taken early return followed by at least one
+// statement is needed to produce the gap region pattern. Without the extra
+// statement after the if-block, clang emits a region entry on the line after
+// the closing "}" (MinRegionCount > 0) and the bug doesn't trigger. With the
+// extra statement, the closing "}" produces a gap region that wraps forward,
+// and the next line has only a non-entry segment (MinRegionCount == 0).
+bool process(bool err) {
+  {
+    if (err) {
+      return false;
+    }
+    int fd = 1;
+    (void)fd;
+  }
+  int result = 42;
+  (void)result;
+  return true;
+}
+
+int main() {
+  process(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..b10fa7cf82d731eaa88f171ed8e7b2eca7693f05
GIT binary patch
literal 213
zcmd1FDa%dHFUw_QfB>NxECLMiQRW3j`N^rp#Yv30iJ5si5IISx8a5!!B*ws^Uy at rO
zke*neTUeS|l&xE4q?cS!09B)$utcfzhd`MPkRQ?F$psSD{#tXU%}D+)BM`81FtST<
zGchu8GVmy{GBE%tRt8>%zJ`Vd4ps&}CN?G}HpW&EkBO0$m5+gmg|QjL`*pSVxmn88
W+nhl2BtV7&0m57cMn(}HW+ni7u`AgC

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..3acc509754fcf1cce00538e0b8d6f33f2a924602
GIT binary patch
literal 720
zcmeyLQ&5zjmf6V5fE}!0LKQ!N#y<h&vq6;!KxvpVW+<N#jfQd=VCn?aVIrvXf;N~=
zRB;Dhn8~Q(4<@sq3P5=cQZN^yN-Hd62MfS#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~4ItP=<2C5~a=`0%hD#z5$euk1_`)v*gs`;-rWcPjCpr0vg>YbR(gBm@=3)
L4j2Qb9!diMX9^?n

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..6f1d48c8b2b27
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
@@ -0,0 +1,37 @@
+// 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 15 ("int result = 42;") follows a scoped block containing
+// a never-taken early return. The gap region from the block's closing "}" wraps
+// to line 15, but line 15 has a non-entry segment with count=1 (the function
+// body continues). LineCoverageStats should report count=1 for line 15.
+//
+// Test inputs generated with (CWD and source must be in /tmp for path-equivalence):
+//   cp Inputs/gap-region-quirk/gap-quirk-v2.cpp /tmp/gap-quirk-v2.cpp
+//   cd /tmp
+//   %clang -fprofile-instr-generate -fcoverage-mapping -O0 \
+//       -mllvm -enable-name-compression=false \
+//       -c -o gap-quirk.o 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 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
+
+// Line 15 ("int result = 42;") should show count 1, not 0.
+// SHOW:     15|      1|  int result = 42;
+
+// 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
+
+// Line 15 should NOT be counted as missed (16 total lines, 2 missed).
+// REPORT: Filename{{.*}}Regions{{.*}}Missed Regions{{.*}}Cover{{.*}}Functions{{.*}}Missed Functions{{.*}}Executed{{.*}}Lines{{.*}}Missed Lines{{.*}}Cover
+// REPORT: gap-quirk-v2.cpp{{[[:space:]]+}}5{{[[:space:]]+}}1{{[[:space:]]+}}80.00%{{[[:space:]]+}}2{{[[:space:]]+}}0{{[[:space:]]+}}100.00%{{[[:space:]]+}}16{{[[:space:]]+}}2{{[[:space:]]+}}87.50%

>From 20456954046bfd16e2f46ba32e22d5f06d03cd49 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Sat, 13 Jun 2026 21:49:48 -0700
Subject: [PATCH 2/3] [llvm-cov] Fix undercounting lines wrapped by gap regions

Lines with no region entry that are wrapped by a gap region were
reported with the gap's count (often 0), even when non-gap segments
on the line indicated the line was actually executed. This caused
llvm-cov to undercount coverage for lines that continue a covered
region after a gap (e.g., closing braces, simple statements following
an if/else).

Check for non-gap segments with HasCount on such lines and use their
max count instead of the gap region's count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 llvm/lib/ProfileData/Coverage/CoverageMapping.cpp | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index 0d9a5a6758f06..797ef9c55bfa6 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -1604,8 +1604,19 @@ LineCoverageStats::LineCoverageStats(
   // wrapped count.
   if (WrappedSegment)
     ExecutionCount = WrappedSegment->Count;
-  if (!MinRegionCount)
+  if (!MinRegionCount) {
+    // If no region entries on this line and the wrapping segment is a gap
+    // region, check for non-gap segments with HasCount (e.g., continuation
+    // of a covered region after a gap). Use their max count instead of the
+    // gap's count of 0.
+    if (WrappedSegment && WrappedSegment->IsGapRegion) {
+      for (const auto *LS : LineSegments) {
+        if (!LS->IsGapRegion && LS->HasCount)
+          ExecutionCount = std::max(ExecutionCount, LS->Count);
+      }
+    }
     return;
+  }
   for (const auto *LS : LineSegments)
     if (isStartOfRegion(LS))
       ExecutionCount = std::max(ExecutionCount, LS->Count);

>From 6bda63ccc96867ba1c15e34aea06e2f88814f5f0 Mon Sep 17 00:00:00 2001
From: makslevental <maksim.levental at gmail.com>
Date: Sun, 14 Jun 2026 10:20:09 -0700
Subject: [PATCH 3/3] [llvm-cov] Replace binary test blobs with text formats

Replace .covmapping and .profdata binary blobs with .yaml (obj2yaml)
and .proftext respectively. The test now uses yaml2obj and
llvm-profdata merge to produce inputs at test time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply at anthropic.com>
---
 .../gap-region-quirk/gap-quirk.covmapping     | Bin 213 -> 0 bytes
 .../gap-region-quirk/gap-quirk.profdata       | Bin 720 -> 0 bytes
 .../gap-region-quirk/gap-quirk.proftext       |  16 +++++
 .../Inputs/gap-region-quirk/gap-quirk.yaml    |  57 ++++++++++++++++++
 .../llvm-cov/gap-region-line-coverage.test    |  25 +++-----
 5 files changed, 80 insertions(+), 18 deletions(-)
 delete mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.covmapping
 delete mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.profdata
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.proftext
 create mode 100644 llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.yaml

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
deleted file mode 100644
index b10fa7cf82d731eaa88f171ed8e7b2eca7693f05..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 213
zcmd1FDa%dHFUw_QfB>NxECLMiQRW3j`N^rp#Yv30iJ5si5IISx8a5!!B*ws^Uy at rO
zke*neTUeS|l&xE4q?cS!09B)$utcfzhd`MPkRQ?F$psSD{#tXU%}D+)BM`81FtST<
zGchu8GVmy{GBE%tRt8>%zJ`Vd4ps&}CN?G}HpW&EkBO0$m5+gmg|QjL`*pSVxmn88
W+nhl2BtV7&0m57cMn(}HW+ni7u`AgC

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
deleted file mode 100644
index 3acc509754fcf1cce00538e0b8d6f33f2a924602..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 720
zcmeyLQ&5zjmf6V5fE}!0LKQ!N#y<h&vq6;!KxvpVW+<N#jfQd=VCn?aVIrvXf;N~=
zRB;Dhn8~Q(4<@sq3P5=cQZN^yN-Hd62MfS#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~4ItP=<2C5~a=`0%hD#z5$euk1_`)v*gs`;-rWcPjCpr0vg>YbR(gBm@=3)
L4j2Qb9!diMX9^?n

diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.proftext b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.proftext
new file mode 100644
index 0000000000000..d27d279e954af
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.proftext
@@ -0,0 +1,16 @@
+_Z7processb
+# Func Hash:
+172590168
+# Num Counters:
+2
+# Counter Values:
+1
+0
+
+main
+# Func Hash:
+24
+# Num Counters:
+1
+# Counter Values:
+1
diff --git a/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.yaml b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.yaml
new file mode 100644
index 0000000000000..d34e65270bc6f
--- /dev/null
+++ b/llvm/test/tools/llvm-cov/Inputs/gap-region-quirk/gap-quirk.yaml
@@ -0,0 +1,57 @@
+--- !ELF
+FileHeader:
+  Class:           ELFCLASS64
+  Data:            ELFDATA2LSB
+  OSABI:           ELFOSABI_GNU
+  Type:            ET_REL
+  Machine:         EM_X86_64
+  SectionHeaderStringTable: .strtab
+Sections:
+  - Name:            __llvm_covfun
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_GNU_RETAIN ]
+    AddressAlign:    0x8
+    Content:         2360A42289F810763C0000005884490A0000000012E9CC181F5D5C9C0101010105080107180B02010209000C2005020009000C05000D008E8080800805000E0206020206018580808008020105050E000204018380808008
+  - Name:            '__llvm_covfun (1)'
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_GNU_RETAIN ]
+    AddressAlign:    0x8
+    Content:         FAD58DE7366495DB09000000180000000000000012E9CC181F5D5C9C0101000101140C0302
+  - Name:            __llvm_covmap
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_GNU_RETAIN ]
+    AddressAlign:    0x8
+    Content:         0000000015000000000000000600000002120000106761702D717569726B2D76322E637070000000
+  - Name:            __llvm_prf_names
+    Type:            SHT_PROGBITS
+    Flags:           [ SHF_ALLOC, SHF_GNU_RETAIN ]
+    AddressAlign:    0x1
+    Content:         10005F5A3770726F6365737362016D61696E
+  - Type:            SectionHeaderTable
+    Sections:
+      - Name:            .strtab
+      - Name:            __llvm_covfun
+      - Name:            '__llvm_covfun (1)'
+      - Name:            __llvm_covmap
+      - Name:            __llvm_prf_names
+      - Name:            .symtab
+Symbols:
+  - Name:            __llvm_covmap
+    Type:            STT_SECTION
+    Section:         __llvm_covmap
+  - Name:            __llvm_prf_names
+    Type:            STT_SECTION
+    Section:         __llvm_prf_names
+  - Name:            __covrec_7610F88922A46023u
+    Type:            STT_OBJECT
+    Section:         __llvm_covfun
+    Binding:         STB_WEAK
+    Size:            0x58
+    Other:           [ STV_HIDDEN ]
+  - Name:            __covrec_DB956436E78DD5FAu
+    Type:            STT_OBJECT
+    Section:         '__llvm_covfun (1)'
+    Binding:         STB_WEAK
+    Size:            0x25
+    Other:           [ STV_HIDDEN ]
+...
diff --git a/llvm/test/tools/llvm-cov/gap-region-line-coverage.test b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
index 6f1d48c8b2b27..00d33922f37ea 100644
--- a/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
+++ b/llvm/test/tools/llvm-cov/gap-region-line-coverage.test
@@ -5,30 +5,19 @@
 // a never-taken early return. The gap region from the block's closing "}" wraps
 // to line 15, but line 15 has a non-entry segment with count=1 (the function
 // body continues). LineCoverageStats should report count=1 for line 15.
-//
-// Test inputs generated with (CWD and source must be in /tmp for path-equivalence):
-//   cp Inputs/gap-region-quirk/gap-quirk-v2.cpp /tmp/gap-quirk-v2.cpp
-//   cd /tmp
-//   %clang -fprofile-instr-generate -fcoverage-mapping -O0 \
-//       -mllvm -enable-name-compression=false \
-//       -c -o gap-quirk.o 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 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: yaml2obj %S/Inputs/gap-region-quirk/gap-quirk.yaml -o %t.o
+// RUN: llvm-profdata merge %S/Inputs/gap-region-quirk/gap-quirk.proftext -o %t.profdata
+
+// RUN: llvm-cov show %t.o -instr-profile %t.profdata \
+// RUN:   -path-equivalence=.,%S/Inputs/gap-region-quirk \
 // RUN:   2>&1 | FileCheck -check-prefix=SHOW %s
 
 // Line 15 ("int result = 42;") should show count 1, not 0.
 // SHOW:     15|      1|  int result = 42;
 
-// 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: llvm-cov report %t.o -instr-profile %t.profdata \
+// RUN:   -path-equivalence=.,%S/Inputs/gap-region-quirk \
 // RUN:   --show-branch-summary=false \
 // RUN:   2>&1 | FileCheck -check-prefix=REPORT %s
 



More information about the llvm-commits mailing list