Skip to content

[coverage] nested macro with unused branch leads to incorrect coverage #140893

@justincady

Description

@justincady

Code coverage reporting is incorrect at the call site when a macro contains an unused branch. Example:

// macro.c
#include <stdio.h>

int enabled = 0;

#define MY_ID(x) 7

#define MY_LOG(fmt, ...)            \
  {                                 \
    if (enabled) {                  \
      printf(fmt, ## __VA_ARGS__);  \
    }                               \
  }

int main(int argc, char *argv[]) {
  MY_LOG("%d, %s, %d\n",
         MY_ID(argc),
         "a",
         1);
}
# build-c.sh
/usr/bin/clang --version | /bin/grep "clang version"
/usr/bin/clang -fprofile-instr-generate -fcoverage-mapping coverage.c -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 confusingly marks the MY_LOG usage in main as partially covered:

$ ./build-c.sh
clang version 19.1.7
    1|       |#include <stdio.h>
    2|       |
    3|       |int enabled = 0;
    4|       |
    5|      0|#define MY_ID(x) 7
    6|       |
    7|       |#define MY_LOG(fmt, ...)            \
    8|      1|  {                                 \
    9|      1|    if (enabled) {                  \
   10|      0|      printf(fmt, ## __VA_ARGS__);  \
   11|      0|    }                               \
   12|      1|  }
   13|       |
   14|      1|int main(int argc, char *argv[]) {
   15|      1|  MY_LOG("%d, %s, %d\n",
   16|      1|         MY_ID(argc),
   17|      0|         "a", // INCORRECT
   18|      0|         1);  // INCORRECT
   19|      1|}

If the example is updated to avoid using MY_ID withing MY_LOG, the problem disappears (even if the updated line has a branch):

$ ./build-c.sh
clang version 19.1.7
    1|       |#include <stdio.h>
    2|       |
    3|       |int enabled = 0;
    4|       |
    5|       |#define MY_ID(x) 7
    6|       |
    7|       |#define MY_LOG(fmt, ...)            \
    8|      1|  {                                 \
    9|      1|    if (enabled) {                  \
   10|      0|      printf(fmt, ## __VA_ARGS__);  \
   11|      0|    }                               \
   12|      1|  }
   13|       |
   14|      1|int main(int argc, char *argv[]) {
   15|      1|  MY_LOG("%d, %s, %d\n",
   16|      1|         enabled ? 1 : 2, // Now the entire MY_LOG usage is covered
   17|      1|         "a",
   18|      1|         1);
   19|      1|}

I suspect this is specific to macro expansion because I can substitute other constructs, including function calls, in the place of MY_ID and get correct results.

Also, I believe the ideal behavior here is that all lines within MY_LOG usage in main should be covered; we did execute that line and that's what usually happens. Then, appropriately, the MY_LOG implementation lines should be marked uncovered.

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