Skip to content

[coverage] incorrect coverage when non-throwing code exists within a try block #138871

Open
@justincady

Description

@justincady

Code coverage reporting is incorrect for try/catch blocks where the contents of the try cannot throw. Example:

// coverage.cc
#include <stdio.h>
#include <stdlib.h>

static __attribute__((noinline)) int do_throw(bool b) {
  if (b) {
    throw b;
  }
  return 1;
}

int main() {
  int i = 0;
  try {
    i = 1;
  } catch (...) {
    abort();
  }
  printf("%s\n", __func__);
  return i;
}
# build.sh
/usr/bin/clang++ --version | /bin/grep "clang version"
/usr/bin/clang++ -fprofile-instr-generate -fcoverage-mapping coverage.cc -o coverage
./coverage
/usr/bin/llvm-profdata merge -sparse default.profraw -o default.profdata
/usr/bin/llvm-cov show ./coverage -instr-profile=default.profdata

The coverage report incorrectly marks lines following catch as uncovered:

$ ./build.sh
clang version 21.0.0
main
    1|       |#include <stdio.h>
    2|       |#include <stdlib.h>
    3|       |
    4|      0|static __attribute__((noinline)) int do_throw(bool b) {
    5|      0|  if (b) {
    6|      0|    throw b;
    7|      0|  }
    8|      0|  return 1;
    9|      0|}
   10|       |
   11|      1|int main() {
   12|      1|  int i = 0;
   13|      1|  try {
   14|      1|    i = 1;
   15|      1|  } catch (...) {
   16|      0|    abort();
   17|      0|  }
   18|      0|  printf("%s\n", __func__); // INCORRECT
   19|      0|  return i;                 // INCORRECT
   20|      1|}

If the example is updated with a call that may throw, everything works as expected:

$ ./build.sh
clang version 21.0.0
main
    1|       |#include <stdio.h>
    2|       |#include <stdlib.h>
    3|       |
    4|      1|static __attribute__((noinline)) int do_throw(bool b) {
    5|      1|  if (b) {
    6|      0|    throw b;
    7|      0|  }
    8|      1|  return 1;
    9|      1|}
   10|       |
   11|      1|int main() {
   12|      1|  int i = 0;
   13|      1|  try {
   14|      1|    do_throw(false); // MAY THROW NOW
   15|      1|  } catch (...) {
   16|      0|    abort();
   17|      0|  }
   18|      1|  printf("%s\n", __func__); // CORRECT
   19|      1|  return i;                 // CORRECT
   20|      1|}

Note that the mapping for main appears to be identical for both cases. First, for the non-throwing case:

$ clang++ -fprofile-instr-generate -fcoverage-mapping coverage.cc -o coverage -Xclang -dump-coverage-mapping
main:
  File 0, 11:12 -> 20:2 = #0
  File 0, 13:7 -> 15:4 = #0
  File 0, 15:17 -> 17:4 = #2
  Gap,File 0, 17:4 -> 18:3 = 0
  File 0, 18:3 -> 19:11 = #1
[...]

And for the maybe throwing case:

$ clang++ -fprofile-instr-generate -fcoverage-mapping coverage.cc -o coverage -Xclang -dump-coverage-mapping
main:
  File 0, 11:12 -> 20:2 = #0
  File 0, 13:7 -> 15:4 = #0
  File 0, 15:17 -> 17:4 = #2
  Gap,File 0, 17:4 -> 18:3 = 0
  File 0, 18:3 -> 19:11 = #1
[...]

Thus I suspect this to be a problem with exception handling removal in codegen.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions