28

We are using LCOV/GCOV to produce test coverage of our projects. Recently we tried to enable branch-coverage additionally. But it looks like, this just doesn't yield the results we expected from a high-level developer view.

Using branch-coverage with C++ blows the report up with branches all over the place. We suspect (as the searching for the issues indicates) that mostly exception handling code creates these "hidden branches". And GCOV/LCOV doesn't seem to skip over these.

I created a small test project to show the problem: https://github.com/ghandmann/lcov-branch-coverage-weirdness

Currently we use Ubuntu 16.04. with:

  • gcc v5.4
  • lcov & genhtml v1.12

Our production code is built with c++11 enabled. The minimal example isn't built with c++11 enabled, but as we experimented a bit with all different options (c++ standard, optimization, -fno-exceptions) we didn't come up with a passable result.

Anybody got some ideas? Tipps? Are we using anything the wrong way? Is this - as stated somewhere else - really expected behavior?

Update:

As also pointed out on the gcc-help mailing list, these "hidden branches" occur because of exception handling. So adding the -fno-exceptions switch to gcc produces 100% branch coverage for "simple" programs. But when exceptions are disabled, gcc refuses to compile code which actually uses exceptions (e.g. try-catch, throw). Therefore for real production code this is not an option. Looks like, you have to simply declare ~50% coverage to be the new 100% in this case. ;)

Melebius
  • 6,183
  • 4
  • 39
  • 52
Sven Eppler
  • 1,646
  • 1
  • 15
  • 26
  • As explained by maxschlepzig you actually want a "partial branch coverage" as you explicit want to exclude some of the exception parts (which is often useful). To achieve this you may want to filter the results, see maxschlepzig's answer. – Simon Sobisch Jul 10 '17 at 09:01

6 Answers6

18

The thing is that GCC also records branch information for each line where a scope exit due to some thrown exception is possible (e.g. on Fedora 25 with GCC 6.3.1 and lcov 1.12).

The value of this information is limited. The main use case for branch coverage data are complicated if-statements that have a multi-clausal logical expression like this:

if (foo < 1 && (bar > x || y == 0))

Say you are interested to verify whether your test suite also covers the bar > x case or if you just have test cases where y == 0.

For this, branch coverage data collection and the visualization by lcov's genhtml is useful. For simple if-statements like

if (p == nullptr) {
  return false;
}
return true;

you don't need branch coverage data because you see whether the branch was taken or not via looking at the coverage of the following lines.

The input of genhtml that is generated by lcov is in a relatively simple text format (cf. geninfo(1)). Thus, you can post-process it such that all lines that start with BRDA: and don't belong to an if-statement are removed. See for example filterbr.py which implements this approach. See also gen-coverage.py for the other lcov/genhtml processing steps and an example project where the resulting trace file is uploaded to codecov (codecov doesn't use genhtml but can import lcov trace files and displays branch coverage data).

(Non-)Alternatives

  • disabling exceptions is only an option when your C++ code doesn't use any
  • compiling with something like -O1 -fno-omit-frame-pointer -fno-optimize-sibling-calls somewhat reduces the number of recorded branch coverage data, but not much
  • Clang supports GCOV style coverage collection but also implements a different approach, called 'source-based code coverage' (compile with -fprofile-instr-generate -fcoverage-mapping and post-process with llvm-profdata and llvm-cov). That toolchain doesn't support branch coverage data, though (as of 2017-05-01).
  • by default, lcov+genhtml don't generate branch coverage data - sometimes you don't really need it (see above), thus, it is then an option to disable it (cf. --rc lcov_branch_coverage=0 and --no-branch-coverage)
maxschlepzig
  • 35,645
  • 14
  • 145
  • 182
  • I had the same problem when compiling Ada for coverage using GNAT Community 2018 which uses GCC 7.3.1, whereby the branch coverage was blown up when originally compiled using `--coverage` and no optimization. Adding the `-O1 -fno-omit-frame-pointer -fno-optimize-sibling-calls` options reduced the amount of recorded branch coverage for Ada. – Chester Gillon Jun 24 '18 at 19:09
6

GCC will add a bunch of exception handling stuff. Especially when you do function calls.

You can fix this by adding -fno-exceptions -fno-inline to your build.

I should add, you probably only want these flags on for testing. So something like this:

g++ -O0 --coverage -fno-exceptions -fno-inline main.cpp -o test-coverage 
Melebius
  • 6,183
  • 4
  • 39
  • 52
zsnafu
  • 332
  • 3
  • 13
  • 1
    Thanks for the finding. But as we are using exceptions, "-fno-exceptions" is no option. GCC even refuses to compile the code in the first place, if you are using exceptions and disable them. Looks like, there is no real solution to this behavior. – Sven Eppler Feb 07 '17 at 09:27
  • Ok. But this is what's causing the problem. You could break out your exceptions in a different unit and only link to the file under test. – zsnafu Feb 09 '17 at 18:02
  • the lcov/gcov library checks for branches at the assembly level. You can look at the assembly output to see the branches it's missing (ie the branches caused by the exception handling).. To view the output assembly: objdump -S --disassemble [your_exe] > asm_output – zsnafu Feb 09 '17 at 18:23
5

There's a PR in progress that addresses these limitations. https://github.com/linux-test-project/lcov/pull/86.

This paper explains the theory behind the implementation.

Result

Arun
  • 1,399
  • 12
  • 21
  • 1
    We moved along, a long time ago. And ditched the entire project (ok, the customer ditched it :D) but nevertheless, nice to see, that there is still something going on! – Sven Eppler Nov 15 '20 at 17:00
1

You can try g++ -O3 --coverage main.cpp -o testcov. I have tried this with g++-5.4 on your file and it works fine, meaning exceptions are discarded with standard printf and string calls.

In fact, any optimization flag other than O0 will cause gcov to ignore exceptions generated for plain standard library calls in a CPP file. I am not sure if normal exceptions will also be optimized away (I don't think so, but haven't tried it yet).

But, I am not sure if you have any requirement in your project that only O0 should be used with your code and not O1, O2, O3 or even Os.

Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38
  • 1
    The optimiser may elide EH code where it can see that an exception won't be thrown, but have you been able to get satisfactory results with larder projects, or when using STL containers which throw on heap exhaustion? – John McFarlane Aug 27 '20 at 15:05
1

I did some work to add filtering to geninfo/lcov/genhtml to remove branches in C/C++ code which are associated with source code lines which appear to not contain any conditionals - using some relatively simple regexps. The filters appear to work in our code base/on the products where the modified lcov tools have been used. The filters are not perfect, and are easily defeated by a bloody-minded user.

I recently got approval to upstream my lcov updates. You can find them at https://github.com/henry2cox/lcov

This version adds support for differential coverage as well as date and owner binning.

A peripheral additional change was to add 'filtering', as described above - mainly because branch coverage seemed not usable for C++ code without it.
You can find the (admittedly hacky, and easily circumvented) regexps in method lcovutil::ReadCurrentSource::containsConditional - near line .../bin/lcovutil.pm:1122

While not perfect: this hack seems to work for our code. Your mileage may vary.

This is tested with perl/5.12.5 and gcc/8.3.0 and 9.2.0. It may work with other versions as well (please let me know if you find portability issues, so I can fix them).

TylerH
  • 20,799
  • 66
  • 75
  • 101
Henry Cox
  • 11
  • 1
  • 1
    Hi Henry; welcome to Stack Overflow. Your prior answer (not a comment) was deleted because it did not have any useful information. This answer at least includes a link to a GitHub repo, so that's much more useful. Check out the [Help Center](https://stackoverflow.com/help/how-to-answer) for more info & advice on answering here. – TylerH Jul 28 '20 at 14:28
  • Thank you for your answer and your fork of lcov, Henry. I'll evaluate your branch of the project. It would be helpful if you could hint others a little bit on how to use it, though. From what I've gathered in the upstream pull request, you copied some tools with a different name, so that should be the only difference when using your for, right? – Alejandro Exojo Jan 26 '22 at 15:48
  • It seems that HenryCox's modification has been merged into lcov master but not tagged yet (until 2023-03-10 or so, correct me if I'm wrong). For people who don't know how to use, check `--filter` in genhtml in lcov github master (i.e. man/genhtml.1) . Nothing else to change compared to old usage of branch coverage. (Assuming that you've already known to set `lcov_branch_coverage=1`) I've tested it, not 100% perfect, some branch miss warnings still exist on the line without if/switch. but 95% workable, I guess. Thanks for your job! – Lhfcws Mar 10 '23 at 05:48
-1

I just came across the same problem and I want to get rid of these uncovered branches due to exceptions. I found a suitable solution for me:

I just avoid using "throw exception" in my code, which I want to cover, directly. I have designed a class, which offers some methods which throw the exceptions instead. As the exception class is not that complex, I don't really care about the coverage, so I just exclude everything with LCOV_EXCL_START and LCOV_EXCL_STOP. Alternatively I could also turn off the branch coverage only for that exception class.

I admit, it's not a straightforward solution, but for my purposes it's perfect due to other reasons, too (I need that exception class to be flexible, so that I can offer different implementation of it: once throwing an exception, another time doing something else).

tangoal
  • 724
  • 1
  • 9
  • 28
  • 1
    This is not a solution at all, it's encouragement to either remove exception use or exclude coverage for them entirely. This may work out for your personal project, but, it does not work in many cases. – kevr Jan 23 '22 at 00:56
  • I think you didn't understand the solution. It does not disencourage usage of exception - it is just encapsulating it. That makes you even more flexible with your exception handling. – tangoal Jan 23 '22 at 01:45