Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merging lcov reports containing records with 0 coverage is problematic due to wrong/inconsistent parsing of records #327

Open
ziad-fernride opened this issue Nov 8, 2024 · 16 comments

Comments

@ziad-fernride
Copy link

ziad-fernride commented Nov 8, 2024

Hi 👋

I think I found the root cause behind: #319

It seems that the parsing of files that with LH:0 (cpp files that are part of the test executable but never actually accessed by the test) is different/wrong compared to the parsing of files accessed by the test.

An example I have is the following two following .dat files coming from two different test paths:

coverage_report_1.dat
TN:
SF: my_file.cpp
FN:9,25,is_within_phase_offset_tolerance(Ouster1DriverConfig const&, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000l> >, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000l> >)
FNDA:0,is_within_phase_offset_tolerance(Ouster1DriverConfig const&, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000l> >, std::__1::chrono::duration<long long, std::__1::ratio<1l, 1000l> >)
FNF:1
FNH:0
BRDA:12,0,0,-
BRDA:12,0,1,-
BRDA:25,0,0,-
BRDA:25,0,1,-
BRDA:25,0,2,-
BRDA:25,0,3,-
BRF:6
BRH:0
DA:12,0
DA:15,0
DA:18,0
DA:22,0
DA:23,0
DA:24,0
DA:25,0
LF:7
LH:0
end_of_record
coverage_report_2.dat
TN:
SF: my_file.cpp
FN:11,26,is_within_phase_offset_tolerance(Ouster1DriverConfig const&, std::chrono::duration<long, std::ratio<1l, 1000l> >, std::chrono::duration<long, std::ratio<1l, 1000l> >)
FNDA:1885,is_within_phase_offset_tolerance(Ouster1DriverConfig const&, std::chrono::duration<long, std::ratio<1l, 1000l> >, std::chrono::duration<long, std::ratio<1l, 1000l> >)
FNF:1
FNH:1
BRDA:12,0,0,1
BRDA:12,0,1,1884
BRDA:25,0,0,248
BRDA:25,0,1,1636
BRDA:25,1,2,210
BRDA:25,1,3,1426
BRF:6
BRH:6
DA:12,1885
DA:13,1
DA:15,1
DA:18,1884
DA:20,1884
DA:21,1884
DA:22,1884
DA:23,1884
DA:24,1884
DA:25,1884
DA:26,1885
LF:11
LH:11
end_of_record

Notice that the function beginning line and ending line are different in the reports ... Also lines found and so on.

Now once I try to merge the two, the merged lcov report will show two functions one hit and one not. Trying to filter functions will also fail to show the function as covered.

Would it make sense to have lcov drop records with LH:0 (unless all files have LH:0, in which case, keep 1) since they add nothing to the coverage reporting anyways ?

@ziad-fernride ziad-fernride changed the title weird behaviour when merging lcov reports - part1 (covered functions appear uncovered) Merging lcov reports with 0 coverage is problematic due to wrong parsing of cpp files Nov 8, 2024
@ziad-fernride ziad-fernride changed the title Merging lcov reports with 0 coverage is problematic due to wrong parsing of cpp files Merging lcov reports with 0 coverage is problematic due to wrong/inconsistent parsing of records Nov 8, 2024
@ziad-fernride ziad-fernride changed the title Merging lcov reports with 0 coverage is problematic due to wrong/inconsistent parsing of records Merging lcov reports containing records with 0 coverage is problematic due to wrong/inconsistent parsing of records Nov 8, 2024
@ziad-fernride
Copy link
Author

I just realized that wrong line parsing happens also in the case of templates and inline functions even if the LH != 0. Simply records containing inline/templated have different begin and end line numbers and overall line count when all functions are hit than when some of them aren't. Which creates a problem when merging lcov reports from different platforms/test routes. It seems the issue is from the compilers themselves, but I was wondering if the lcov tooling can work around that ?

@henry2cox
Copy link
Collaborator

Thanks for the report and the testcase. I'll see if I can figure it out.
This could be a similar issue to one I fixed last week, related to merging MC/DC data.
Might be a while before I can get to this issue (sorry).

With respect to different start/end lines: the tool uses : as the primary lookup key - function name is rather secondary. I am not sure what happens if you have the "same" function with different start lines - but I rather suspect that they will be treated and reported as distinct functions. If you have the same start line and a different end line: then you will get a error; if you ignore that error, then it selects either the smallest or largest end line (can't remember which).

If start/end line accuracy turns out to be a problem (which platform and compiler/version?) - then a (somewhat hacky) solution would be to provide a callback which would present you the file, start and end lines, and expect to receive back a 'fixed' start and end line. Your callback could do whatever it liked.
It would be rather difficult/painful to move the function from one file to another (...not supported) - but to move its location is probably straightforward.

@ziad-fernride
Copy link
Author

ziad-fernride commented Nov 10, 2024

If start/end line accuracy turns out to be a problem (which platform and compiler/version?)

The files attached are coming from a gcc compiler and llvm compiler paths.

The main problem is that both compilers have wrong parsing when the record/(inline, templated)function is part of the test executable but not actually touched by the test. For example, comments are reported as untested lines in the case of LH:0 records. Which is part of what makes the bazel integration of coverage painful. Bazel generates a report for each test and then merges them into one. So once you merge a report with a tested record and a report with the wrongly parsed one, you end up with comments showing up uncovered. This also goes for curly brackets etc.

The filters help eliminate the problems caused by the weird behaviour described above. But now trying to merge the different reports from different compilers that seem to screw up the parsing in different ways, or have inconsistent parsing of the same files bring a new angle where the filters are also confused.

@ziad-fernride
Copy link
Author

@henry2cox any thoughts on this issue ? Do you think there is any measure that can be taken to make the function determination more consistent across reports (platforms) ?

@henry2cox
Copy link
Collaborator

henry2cox commented Nov 14, 2024

Ah - sorry for the delay.
I fixed the 'function data merge' issue (or I believe that I fixed it).
Hope to push that soon.

The issue of inconsistent data and what the tool should do about it is more complicated. Right now, it does <something> when data is inconsistent and the inconsistency is ignored. Whether that action is "correct" or a good idea - is somewhat open to debate. YMMV.

henry2cox added a commit that referenced this issue Nov 18, 2024
@henry2cox
Copy link
Collaborator

I think, fixed - please give a try and let me know.

@ziad-fernride
Copy link
Author

will do this weekend ! :)
thanks @henry2cox ! 🤩

@ziad-fernride
Copy link
Author

ziad-fernride commented Nov 29, 2024

Sorry for the late response 🙏

Unfortunately this does not seem to fix it.

 ✘ 🚀   ~/lcov   master  lcov --version              
lcov: LCOV version 2.2-10.g401e15b

As you can see I am on the correct version ... I use this to merge:

lcov --source-directory $(pwd) --output-file merged.dat --add-tracefile report1.dat --add-tracefile report2.dat --ignore-errors "unmapped,category,unsupported,source,negative,unused,inconsistent" --filter "trivial,line,branch,region,range" --branch-coverage
report1.dat
SF:project/common/trace/tracer_singleton.cpp
FN:7,9,_ZN5project5trace15TracerSingletonC2Ev
FNDA:24,_ZN5project5trace15TracerSingletonC2Ev
FNF:1
FNH:1
DA:7,24
DA:13,1682
DA:14,1682
DA:19,17
DA:24,1663
LF:5
LH:5
end_of_record
report2.dat
SF:project/common/trace/tracer_singleton.cpp
FN:11,14,_ZN5project5trace15TracerSingleton3getEv
FN:17,20,_ZN5project5trace15TracerSingleton11set_backendENSt3__110unique_ptrINS0_17TracerBackendBaseENS2_14default_deleteIS4_EEEE
FN:22,24,_ZN5project5trace15TracerSingleton11get_backendEv
FN:7,9,_ZN5project5trace15TracerSingletonC2Ev
FNDA:0,_ZN5project5trace15TracerSingleton3getEv
FNDA:0,_ZN5project5trace15TracerSingleton11set_backendENSt3__110unique_ptrINS0_17TracerBackendBaseENS2_14default_deleteIS4_EEEE
FNDA:0,_ZN5project5trace15TracerSingleton11get_backendEv
FNDA:0,_ZN5project5trace15TracerSingletonC2Ev
FNF:4
FNH:0
DA:7,0
DA:11,0
DA:13,0
DA:14,0
DA:17,0
DA:19,0
DA:22,0
DA:24,0
LF:8
LH:0
end_of_record
merged.dat
TN:
SF:project/common/trace/tracer_singleton.cpp
FNL:0,11,14
FNA:0,0,_ZN5project5trace15TracerSingleton3getEv
FNL:1,17,20
FNA:1,0,_ZN5project5trace15TracerSingleton11set_backendENSt3__110unique_ptrINS0_17TracerBackendBaseENS2_14default_deleteIS4_EEEE
FNL:2,22,24
FNA:2,0,_ZN5project5trace15TracerSingleton11get_backendEv
FNL:3,7,9
FNA:3,24,_ZN5project5trace15TracerSingletonC2Ev
FNF:4
FNH:1
DA:7,24
DA:11,0
DA:13,1682
DA:14,1682
DA:17,0
DA:19,17
DA:22,0
DA:24,1663
LF:8
LH:5
end_of_record

@henry2cox
Copy link
Collaborator

henry2cox commented Nov 29, 2024

the lcov data file format changed recently (still reads - but does not generate - the old format); the new format explicitly stores function alias data rather than re-deriving it. (It also stores MC/DC data...but that is an unrelated item.)

I think that the result is correct...4 total functions, one covered.

$ ./bin/lcov --summary merged.dat --ignore inconsistent --branch
Reading tracefile x.info.
lcov: WARNING: (inconsistent) "project/common/trace/tracer_singleton.cpp":11: function '_ZN5project5trace15TracerSingleton3getEv' is not hit but line 13 is.
        To skip consistency checks, see the 'check_data_consistency' section in man lcovrc(5).
        (use "lcov --ignore-errors inconsistent,inconsistent ..." to suppress this warning)
lcov: WARNING: (inconsistent) "project/common/trace/tracer_singleton.cpp":17: function '_ZN5project5trace15TracerSingleton11set_backendENSt3__110unique_ptrINS0_17TracerBackendBaseENS2_14default_deleteIS4_EEEE' is not hit but line 19 is.
lcov: WARNING: (inconsistent) "project/common/trace/tracer_singleton.cpp":22: function '_ZN5project5trace15TracerSingleton11get_backendEv' is not hit but line 24 is.
Summary coverage rate:
  source files: 1
  lines.......: 62.5% (5 of 8 lines)
  functions...: 25.0% (1 of 4 functions)
  branches....: no data found
Message summary:
  3 warning messages:
    inconsistent: 3

Update: I noticed a bug such that, when the 'inconsistent' message is ignored and the data 'fixed', that the fixed data is incomplete. This results in a strange looking HTML report. I think I fixed the problem but need to do more testing.
Arguably, how the data should be fixed - or if it should be modified at all - should be configurable.

@ziad-fernride
Copy link
Author

ziad-fernride commented Nov 30, 2024

Sorry, that was not the best example ... here is another example:

report1.dat
SF:project/common/trace/tracer_singleton.cpp
FN:23,_ZN5project5trace15TracerSingleton11get_backendEv
FN:18,_ZN5project5trace15TracerSingleton11set_backendESt10unique_ptrINS0_17TracerBackendBaseESt14default_deleteIS3_EE
FN:12,_ZN5project5trace15TracerSingleton3getEv
FN:7,_ZN5project5trace15TracerSingletonC2Ev
FNDA:1920,_ZN5project5trace15TracerSingleton11get_backendEv
FNDA:27,_ZN5project5trace15TracerSingleton11set_backendESt10unique_ptrINS0_17TracerBackendBaseESt14default_deleteIS3_EE
FNDA:1949,_ZN5project5trace15TracerSingleton3getEv
FNDA:34,_ZN5project5trace15TracerSingletonC2Ev
FNF:4
FNH:4
DA:7,34
DA:8,34
DA:9,34
DA:12,1949
DA:13,1949
DA:14,1949
DA:15,1949
DA:18,27
DA:19,27
DA:20,27
DA:23,1920
DA:24,1920
DA:25,1920
LH:13
LF:13
end_of_record

report2.dat
SF:project/common/trace/tracer_singleton.cpp
FN:22,_ZN5project5trace15TracerSingleton11get_backendEv
FN:17,_ZN5project5trace15TracerSingleton11set_backendENSt3__110unique_ptrINS0_17TracerBackendBaseENS2_14default_deleteIS4_EEEE
FN:11,_ZN5project5trace15TracerSingleton3getEv
FN:7,_ZN5project5trace15TracerSingletonC2Ev
FNDA:0,_ZN5project5trace15TracerSingleton11get_backendEv
FNDA:0,_ZN5project5trace15TracerSingleton11set_backendENSt3__110unique_ptrINS0_17TracerBackendBaseENS2_14default_deleteIS4_EEEE
FNDA:0,_ZN5project5trace15TracerSingleton3getEv
FNDA:0,_ZN5project5trace15TracerSingletonC2Ev
FNF:4
FNH:0
BRDA:13,0,0,-
BRDA:13,0,1,-
BRDA:13,0,2,-
BRDA:13,0,3,-
BRDA:13,0,4,-
BRDA:13,0,5,-
BRDA:13,0,6,-
BRDA:13,0,7,-
BRF:8
BRH:0
DA:7,0
DA:9,0
DA:11,0
DA:13,0
DA:14,0
DA:17,0
DA:19,0
DA:20,0
DA:22,0
DA:24,0
LH:0
LF:10
end_of_record

merged.dat
TN:
SF:project/common/trace/tracer_singleton.cpp
FNL:0,11,14
FNA:0,0,_ZN5project5trace15TracerSingleton3getEv
FNL:1,12,15
FNA:1,1949,_ZN5project5trace15TracerSingleton3getEv
FNL:2,17,20
FNA:2,0,_ZN5project5trace15TracerSingleton11set_backendENSt3__110unique_ptrINS0_17TracerBackendBaseENS2_14default_deleteIS4_EEEE
FNL:3,18,20
FNA:3,27,_ZN5project5trace15TracerSingleton11set_backendESt10unique_ptrINS0_17TracerBackendBaseESt14default_deleteIS3_EE
FNL:4,22,24
FNA:4,0,_ZN5project5trace15TracerSingleton11get_backendEv
FNL:5,23,25
FNA:5,1920,_ZN5project5trace15TracerSingleton11get_backendEv
FNL:6,7,9
FNA:6,34,_ZN5project5trace15TracerSingletonC2Ev
FNF:7
FNH:4
DA:7,34
DA:8,34
DA:11,0
DA:12,1949
DA:13,1949
DA:14,1949
DA:17,0
DA:18,27
DA:19,27
DA:22,0
DA:23,1920
DA:24,1920
LF:12
LH:9
end_of_record

And this is how the report/code looks:

Screenshot 2024-11-30 at 11 15 43

@henry2cox
Copy link
Collaborator

I'm not quite sure what we expect to see from the above data. It looks like the two input files are from different versions of source code. The input data looks inconsistent - so it might not be too surprising to see odd output data.

For example:

  • in report1.dat, function _ZN5project5trace15TracerSingleton11get_backendEv starts on line 23 such that both the function and its opening line are hit, but in report2.dat, it starts on line 22 and neither the function nor its opening line are hit.
    The first data set doesn't contain line 22, and the second doesn't contain line 23.
  • we see similar inconsistency for _ZN5project5trace15TracerSingleton3getEv.

lcov keys on file/line to determine uniqueness - so it thinks that these are different functions which happen to have the same name. Maybe it ought to check that - and give errors, at least in C/C++ code where function names are supposed to be unique.

This creates weirdness in the 'function detail' window - for example, because the summary table at the top says that there are 7 functions/4 hit - but the 'detail' table shows only 5 entries (...because there are 2 pairs which have the same name but different location, but alias in the table). 'visit' order (..somewhat random) determines which one appears.

This could be fixed (say, to disambiguate the duplicate names) - but that is rather a hack that I'm not eager to introduce because I don't think it is necessary - or even possible - to consistently display inconsistent data.
As the documentation mentions: Note that ignoring certain errors can cause subsequent errors and/or can result in inconsistent or confusing coverage reports.

The next question is: what is the source of this input data? Why is it inconsistent? Which platform(s) and compiler version(s)?

  • We saw some rather similar very weird data for a project some time ago; the root cause was a library version mismatch such that we were inadvertently trying to combine and/or compare coverage data from different sites, for different code bases.
  • to avoid those issues, we use a --version-script callback to check the data and a library callback to deduce the version.

@ziad-fernride
Copy link
Author

It is the same source code with the same version ... We have two platforms (qcc and clang) and depending on the target platform, the execution naturally takes a platform-dependent path in the code. The two reports are generated in the same PR run (the same commit), and we want to merge them to get the overall coverage of the code.

@ziad-fernride
Copy link
Author

but that is rather a hack that I'm not eager to introduce

So in my experience so far, merging reports is always a messy process, even in the case of the same platform the same source code the same version, since compilers seem to not bother parsing files correctly if the files are part of the test executable without being accessed by the test. Curly brackets, comments and all sort of stuff show up as uncovered lines. If you merge such a report with another report where the same file is 100% covered, the merged report will not show the same file to be covered 100%.

If we don't apply filters and even in some cases exclude some patterns, we would not have a chance of having 100% coverage even if we have a 100% coverage and we only use 1 platform. This imperfection is part of the reasons we have filters to begin with, no ? 😆

The case of two platforms just adds one degree of imperfection to the mix, which is that different platforms parse function beginning and end differently.

All filters are handling situations that should not happen but here we are 😆 and this is just one more case of that, no?

@henry2cox
Copy link
Collaborator

Yeah - you aren't wrong.
For nontrivial projects, we tend to see issues related to file pathnames and revision control - but we don't tend to try to correlate data from different toolchains so we don't see a lot of this sort of inconsistency.
I haven't seen comments and blank lines show up in gcc data - but have seen them in LLVM. Newer versions of LLMV tend to be better.
Function locations, in particular: look like a bug in one toolchain or the other (or both).

I'm not sure how many inconsistencies you see, or whether they are clustered in some subset of files.

If they are clustered, then you might be able to hack things - say, to create two entries for those files ("foo_gcc.cpp" and "foo_llvm.cpp"), to manage them separately (and then write your 'criteria' callback to handle/merge them).

I think that the real trick is to think about what you are trying to achieve - what the numbers actually mean to you - and then we can think of a way to make that happen. (That is more-or-less how we ended up with the set of tool features we have.)

In the meantime: I have another small set of updates to push - mostly related to additional consistency checking and actions to 'fix' the issues.

@ziad-fernride
Copy link
Author

Yeah the use-case of merging reports that are inconsistent is legitimate imho. Merging reports from different platforms is the only way one can have a complete coverage view of the code in a mono-repo intended to run on several targets. Separating code based on platforms is not always possible since much of the code is shared among multiple platforms.

What you said earlier is correct, it is safe to assume a function with the same signature is the same function, no ? Don't you think this a safe way of handling the inconsistency among different platforms ?

One more observation, that can make the inconsistency handling even safer: The different between the platforms seems to be that one platform assumes the beginning of the function is the curly bracket, and one to be the function signature. So it is really a matter of +- 1 line.

@henry2cox
Copy link
Collaborator

I guess my concern would be whether the tools misplace just the function decl/start lines - or also misplace something else that matters more. That's the problem with ignoring errors...all of them get ignored, even if you really wanted to look at certain ones.
Thus, I would probably write a simple filter to parse the source code to find the open/close - then adjust the numbers to not be off-by-one. I'd also check to see what objdump says about the locations (though I would be very surprised - and more than a little disappointed - if it gave a different answer than the corresponding coverage tool).

I suspect that the location may differ by more than 1 - say, if the signature extends across multiple lines, or if a bloody-minded user puts a comment in exactly the wrong place.

Another way to track the multi-platform issue is to actually build distinct DBs, and generate a distinct differential reports for each. If you find new code that isn't covered in one, verify that it is covered in the other...now that (missed) point will appear in the 'UBC' category in the next build - and you know that you reviewed it and signed off earlier, so no need to investigate.
Similar argument applies to the other categories you likely care about.
It isn't to hard to write a criteria callback that does exactly this (and then fails your jenkins job if your criteria is not met).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants