Coverage suppression merged into GCC
A few days ago I
pushed
support for coverage suppression in GCC. Sometimes we write very
defensive guards, especially in debug builds (like asserts), that are
intended to never trigger because they signal a logic bug
elsewhere. It’s hard to argue that defensive code and asserts isn’t
excellent, but they paradoxically introduce a problem for coverage
since we now introduce decisions and code paths that will never be
exercised. The new #pragma GCC suppress_coverage and
__attribute__((suppress_coverage)) address this problem. GCC could
already ignore files for coverage (with
-fprofile-filter-files
and
-fprofile-exclude-files),
but this is more granular.
The problem is easily demonstrated with a trivial example. n/m where
m == 0 is undefined behaviour and inputs should be sanitized. The
assert is there to find the cases when it isn’t.
double ratiof(unsigned int n, unsigned int m) {
assert (m != 0);
return (double)n / m;
}The assert is really a branch, it expands to something of the effect
of if (cond) abort(). Since the abort should never be reached we
cannot achieve full coverage.
Now, what we could do is to simply ignore these false positives when we analyze the coverage report. The problem is that false positives poison the report and dull our sensitivity to problems, and we don’t detect new or surprising coverage. This is similar to why we want clean compiles, because under a deluge of warnings the real problems don’t stand out.
Here’s a slightly more sophisticated example. The comparison function
given to qsort has these requirements:
comp is a comparison function which returns a negative integer value if the first argument is less than the second, a positive integer value if the first argument is greater than the second and zero if the arguments are equivalent.
These properties should be true for all calls to comp for all
elements in the array. We want to verify this in our testsuite and can
afford the (debug build) performance hit.
void qsort(void *ptr, size_t count, int (*comp)(const void*, const void*)) {
// ...
while (1) {
void* x = ptr[bot];
void* y = ptr[top];
#if defined(DEBUG)
int lt = comp(x, y);
int gt = comp(y, x);
if (lt < 0) assert (gt > 0);
if (lt == 0) assert (gt == 0);
if (lt > 0) assert (gt < 0);
#endif
}
// ...
}This would tank the coverage totals, while at the same providing
stronger confidence in the provided comp functions and something we
want to keep. We can fix the coverage problem with the
suppress_coverage pragma:
void qsort(void *ptr, size_t count, int (*comp)(const void*, const void*)) {
// ...
while (1) {
void* x = ptr[bot];
void* y = ptr[top];
#if defined(DEBUG)
#pragma GCC suppress_coverage begin
int lt = comp(x, y);
int gt = comp(y, x);
if (lt < 0) assert (gt > 0);
if (lt == 0) assert (gt == 0);
if (lt > 0) assert (gt < 0);
#pragma GCC suppress_coverage end
#endif
}
// ...
}That’s it, really, and quite intuitive. The code in the
suppress_coverage begin/end blocks won’t contribute to total
coverage, and the (sub)paths that thread through the
suppress_coverage block won’t be counted towards prime path
coverage.
We can also suppress whole functions using the new
__attribute__((suppress_coverage)) (or [[gnu::suppress_coverage]])
attribute. This is ideal for pure pre/post condition checking
functions, but since it disables all coverage for the function it
should probably be use sparingly to not suppress the code we do want
to cover.
__attribute__((suppress_coverage))
double ratiof(unsigned int n, unsigned int m) {
assert (m != 0); // won't count for coverage
return (double)n / m; // this won't either
}This feature will be released with GCC 17 (spring 2027 probably), but can be backported to earlier releases. This feature was commissioned; Patch can support you in backporting coverage suppression or adding new features.