From ce30624cc37a78ef9a0d6273b7b0a6bbf8e85095 Mon Sep 17 00:00:00 2001 From: Roman Beliaev Date: Wed, 9 Jul 2025 16:06:46 +0300 Subject: [PATCH] Refactor 'llvm2lcov' utility - Improve the algorithm for collecting line coverage data using segments: previously, only segment start lines were used for instrumentation, but lines between these start lines were not. - Add separate 'True' and 'False' branches as it is done for gcov. - Add branches defined in expansions (like macros). The entries were placed to expansions call sites. - Fixed some problems with MC/DC branch expressions. - Add branch expressions for multiline MC/DC entries. - Add support for JSON data format version '3.0.1', which adds 'fileId' to MC/DC entries. This allows using branches from expansions for placing MC/DC from expansions to the expansions call sites. It also helps to add correct expressions for some MC/DC branches belonging to groups that contain branches from expansions. Signed-off-by: Roman Beliaev --- bin/llvm2lcov | 271 ++++++++++++++++++++++---------- tests/Makefile | 2 +- tests/llvm2lcov/Makefile | 6 + tests/llvm2lcov/llvm2lcov.sh | 289 +++++++++++++++++++++++++++++++++++ tests/llvm2lcov/main.cpp | 72 +++++++++ tests/llvm2lcov/test.h | 8 + 6 files changed, 565 insertions(+), 83 deletions(-) create mode 100644 tests/llvm2lcov/Makefile create mode 100755 tests/llvm2lcov/llvm2lcov.sh create mode 100644 tests/llvm2lcov/main.cpp create mode 100644 tests/llvm2lcov/test.h diff --git a/bin/llvm2lcov b/bin/llvm2lcov index 07cd5d1e..e93805a3 100755 --- a/bin/llvm2lcov +++ b/bin/llvm2lcov @@ -156,10 +156,6 @@ sub parse if (defined($version) && $version ne ""); my $lineData = $fileInfo->test($testname); - # use branch data to derive MC/DC expression - so need - # it, even if user didn't ask - my $branchData = $fileInfo->testbr($testname) - if $lcovutil::br_coverage || $lcovutil::mcdc_coverage; my $mcdcData = $fileInfo->testcase_mcdc($testname) if $lcovutil::mcdc_coverage; @@ -170,129 +166,239 @@ sub parse my $mcdc = $f->{mcdc_records} if $lcovutil::mcdc_coverage && exists($f->{mcdc_records}); - foreach my $s (@$segments) { - die("unexpected segment data") unless scalar(@$s) == 6; - my ($line, $col, $count, $hasCount, $isRegion, $isGap) = - @$s; - next unless $hasCount; - $lineData->append($line, $count); + my $index = 0; + my $currentLine = 0; + + while ($index < $#$segments) { + my $segment = $segments->[$index]; + die("unexpected segment data") + unless scalar(@$segment) == 6; + my ($line, $col, $count, $hasCount, $isRegionEntry, $isGap) + = @$segment; + $currentLine = $line if !$currentLine; + if ($hasCount) { + $segment = $segments->[$index + 1]; + die("unexpected segment data") + unless scalar(@$segment) == 6; + my ($next_line, $next_col, $next_count, $next_hasCount, + $next_isRegionEntry, $next_isGap) + = @$segment; + if ($currentLine == $next_line && !$next_isRegionEntry) + { + while ($next_line == $currentLine && + ++$index < $#$segments) { + $segment = $segments->[$index + 1]; + die("unexpected segment data") + unless scalar(@$segment) == 6; + $next_line = $segment->[0]; + $count = $next_count + if ($count && + $next_count > $count && + $currentLine == $next_line); + $next_count = $segment->[2]; + } + $lineData->append($currentLine, $count); + ++$currentLine; + } else { + my $bound = $next_line; + my $i = $index; + while (!$next_isRegionEntry && + $next_line == $bound && + ++$i < $#$segments) { + $segment = $segments->[$i + 1]; + die("unexpected segment data") + unless scalar(@$segment) == 6; + $next_line = $segment->[0]; + $next_isRegionEntry = $segment->[4]; + } + --$bound + if ($next_isRegionEntry && + $next_line == $bound && + !($isRegionEntry && $line == $next_line)); + $count = $next_count + if $next_count > $count && $line == $next_line; + while ($currentLine <= $bound) { + $lineData->append($currentLine, $count); + ++$currentLine; + } + ++$index; + } + } else { + do { + ++$index; + $segment = $segments->[$index]; + die("unexpected segment data") + unless scalar(@$segment) == 6; + ($line, $col, $count, $hasCount, + $isRegionEntry, $isGap) = @$segment; + } while (!$hasCount && $index < $#$segments); + $currentLine = $isRegionEntry ? $line : $line + 1; + } } - - if ($branchData) { - my $currentLine = -1; - my $branchIdx; + if ($mcdc) { + my @mcdcBranches; # array (start line, start column, expression) foreach my $branch (@$branches) { die("unexpected branch data") unless scalar(@$branch) == 9; + # Consider only branches of "MCDCBranchRegion" kind. + next if ($branch->[-1] != 6); my ($line, $startCol, $endline, $endcol, $trueCount, $falseCount, $fileId, $expandedId, $kind) = @$branch; - if ($line != $currentLine && - defined($lineData->value($line))) { - $branchIdx = 0; # restart counter - $currentLine = $line; - } else { - # this branch is part of the current group - ++$branchIdx; - } my $expr = $srcReader->getExpr($line, $startCol, $endline, $endcol) if $srcReader->notEmpty(); - - my $br = - BranchBlock->new($branchIdx, $trueCount, $expr); - $branchData->append($line, 0, $br, $filename); + push(@mcdcBranches, [$line, $startCol, $expr]); } - } - if ($mcdc) { foreach my $m (@$mcdc) { - # what are fileID and kind? die("unexpected MC/DC data") unless scalar(@$m) == 7; - my ($line, $startCol, $endLine, $endcol, $fileId, + my ($line, $startCol, $endLine, $endCol, $expandedId, $kind, $cov) = @$m; die("unexpected MC/DC cov") unless 'ARRAY' eq ref($cov); - my $groupSize = scalar(@$cov); # read the source line and extract the expression... my $expr = $srcReader->getExpr($line, $startCol, $endLine, - $endcol) + $endCol) if ($srcReader->notEmpty()); - + my @brExprs; + foreach my $branch (@mcdcBranches) { + my ($brLine, $brCol, $brExpr) = @$branch; + if (($brLine > $line || + ($brLine == $line && $brCol >= $startCol)) + && + ($brLine < $endLine || + ($brLine == $endLine && $brCol <= $endCol)) + ) { + push(@brExprs, [$brLine, $brCol, $brExpr]); + } + } + @brExprs = + sort { $a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] } + @brExprs; my $current_mcdc = $mcdcData->new_mcdc($mcdcData, $line); - my $branch = $branchData->value($line); - my $idx = 0; + my $groupSize = scalar(@$cov); + my $idx = 0; foreach my $c (@$cov) { - my $branchExpr = - $branch->getBlock(0)->[$idx]->expr() - if $branch && - (scalar(@{$branch->getBlock(0)}) > $idx); - $branchExpr = - defined($branchExpr) ? - "'$branchExpr' in '$expr'" : + my $branchExpr = $brExprs[$idx]->[2] + if $groupSize == scalar(@brExprs); + my $fullExpr = + defined($branchExpr) && + defined($expr) ? "'$branchExpr' in '$expr'" : $idx; - $current_mcdc->insertExpr($filename, $groupSize, 0, - $c, $idx, $branchExpr); + $c, $idx, $fullExpr); $current_mcdc->insertExpr($filename, $groupSize, 1, - $c, $idx, $branchExpr); + $c, $idx, $fullExpr); ++$idx; } $mcdcData->close_mcdcBlock($current_mcdc); } - } # MCDC - $fileInfo->testbr()->remove($testname) - if $lcovutil::mcdc_coverage && !$lcovutil::br_coverage; + } lcovutil::info(2, "finished parsing $filename\n"); } - next unless $lcovutil::func_coverage; foreach my $f (@{$k->{functions}}) { my $name = $f->{name}; my $filenames = $f->{filenames}; # array - if ($#$filenames != 0) { - lcovutil::ignorable_error($lcovutil::ERROR_USAGE, - "unsupported: function $name associated with multiple files" - ); - next; - } my $filename = ReadCurrentSource::resolve_path($filenames->[0], 1); - if (TraceFile::skipCurrentFile($filename)) { - if (!exists($lcovutil::excluded_files{$filename})) { - $lcovutil::excluded_files{$filename} = 1; - lcovutil::info("Excluding $filename\n"); - } - next; - } + next if (TraceFile::skipCurrentFile($filename)); die('unexpected unknown file \'' . $filenames->[0] . '\'') unless $top->file_exists($filename); - my $info = $top->data($filename); - my $count = $f->{count}; - my $regions = $f->{regions}; # startline/col, endline/col/ + $srcReader->open($filename); + + my $info = $top->data($filename); + my $count = $f->{count}; + my $regions = $f->{regions}; # startline/col, endline/col/ + my $branches = $f->{branches}; my $functionMap = $info->testfnc($testname); + # use branch data to derive MC/DC expression - so need + # it, even if user didn't ask + my $branchData = $info->testbr($testname) + if $lcovutil::br_coverage; my $startLine = $regions->[0]->[0]; # startline of first region - # NOTE: might be a mistake to grab the end line of the last region - - # LCOV follows GCC behaviour and associates lines with where they - # start - not where they end... - my $endline = $regions->[-1]->[2]; # endline of last region - my $func = - $functionMap->define_function($name, $startLine, $endline) - unless defined($functionMap->findName($name)); - $functionMap->add_count($name, $count); - - # for the moment - don't worry about the coverpoints in the function - #my $branches = $f->{branches}; - #my $mcdc = $f->{mcdc_records} if exists($f->{mcdc_records}); - #foreach my $r (@$regions) { - # my ($startLine, $startCol, $endLine, $endCol, $count, $fileId, - # $expandedId, $kind) = @$r; - #} + my $endline = $regions->[0]->[2]; # endline of last region + if ($lcovutil::func_coverage) { + my $func = + $functionMap->define_function($name, $startLine, + $endline) + unless defined($functionMap->findName($name)); + $functionMap->add_count($name, $count); + } + if ($branchData) { + my $funcBranchData = BranchData->new(); + my $regionIdx = 0; + foreach my $b (@$branches) { + die("unexpected branch data") unless scalar(@$b) == 9; + my ($brStartLine, $brStartCol, $endLine, + $endCol, $trueCount, $falseCount, + $fileId, $expandedId, $kind) = @$b; + my ($line, $col) = ($brStartLine, $brStartCol); + my $expr; + + if ($fileId == 0) { + $expr = + $srcReader->getExpr($line, $col, $endLine, + $endCol) + if $srcReader->notEmpty(); + } else { + # Find a source range, which contains the branch. + while ($regionIdx < scalar(@$regions)) { + my ($rStartLine, $rStartCol, $rEndLine, + $rEndCol, $rCount, $rFileId, + $rExpandedId, $rKind + ) = @{$regions->[$regionIdx]}; + if ($rExpandedId == $fileId && $rKind == 1) { + if ($rFileId != 0) { + # Check previous regions to find one + # that describes lines of the function's + # source file. + my $rIdx = $regionIdx - 1; + $fileId = $rFileId; + while ($fileId != 0 && $rIdx >= 0) { + ($rStartLine, $rStartCol, + $rEndLine, $rEndCol, + $rCount, $rFileId, + $rExpandedId, $rKind + ) = @{$regions->[$rIdx]}; + $fileId = $rFileId + if ($rExpandedId == $fileId && + $rKind == 1); + --$rIdx; + } + } + ($line, $col) = ($rStartLine, $rStartCol); + last; + } + ++$regionIdx; + } + } + # Processed branch on the same line doesn't have to be the previous. + my $brEntry = $funcBranchData->value($line); + my $branchIdx = + !defined($brEntry) ? 0 : + scalar(@{$brEntry->getBlock(0)}); + my $br = + BranchBlock->new($branchIdx, $trueCount, + !defined($expr) ? $branchIdx : + "(" . $expr . ") == True"); + $funcBranchData->append($line, 0, $br, $filename); + + ++$branchIdx; + $br = + BranchBlock->new($branchIdx, $falseCount, + !defined($expr) ? $branchIdx : + "(" . $expr . ") == False"); + $funcBranchData->append($line, 0, $br, $filename); + } + $branchData->union($funcBranchData); + } } } lcovutil::info(2, "finished $jsonFile\n"); @@ -324,7 +430,8 @@ my %opts = ('test-name|t=s' => \$testname, 'output-filename|o=s' => \$output_filename,); my %rc_opts; if (!lcovutil::parseOptions(\%rc_opts, \%opts, \$output_filename)) { - print(STDERR "argparse failed"); + print(STDERR "argparse failed\n"); + exit(1); } my $info = parse($testname, @ARGV); diff --git a/tests/Makefile b/tests/Makefile index dcc45c76..173cc040 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -12,7 +12,7 @@ all: check report include common.mak -TESTS := genhtml lcov gendiffcov py2lcov perl2lcov xml2lcov +TESTS := genhtml lcov gendiffcov llvm2lcov py2lcov perl2lcov xml2lcov # there may or may not be some .info files generated for exported # tools - py2lcov, perl2lcov, etc. We want them included in the diff --git a/tests/llvm2lcov/Makefile b/tests/llvm2lcov/Makefile new file mode 100644 index 00000000..df6b5065 --- /dev/null +++ b/tests/llvm2lcov/Makefile @@ -0,0 +1,6 @@ +include ../common.mak + +TESTS := llvm2lcov.sh + +clean: + $(shell ./llvm2lcov.sh --clean) diff --git a/tests/llvm2lcov/llvm2lcov.sh b/tests/llvm2lcov/llvm2lcov.sh new file mode 100755 index 00000000..b619c042 --- /dev/null +++ b/tests/llvm2lcov/llvm2lcov.sh @@ -0,0 +1,289 @@ +#!/bin/bash +set +x + +if [[ "x" == ${LCOV_HOME}x ]] ; then + if [ -f ../../bin/lcov ] ; then + LCOV_HOME=../.. + fi +fi + +source ../common.tst + +rm -rf test *.profraw *.profdata *.json *.info report + +clean_cover + +if [[ 1 == $CLEAN_ONLY ]] ; then + exit 0 +fi + +LCOV_OPTS="--branch-coverage $PARALLEL $PROFILE" + +clang++ -fprofile-instr-generate -fcoverage-mapping -fcoverage-mcdc -o test main.cpp +if [ $? != 0 ] ; then + echo "clang++ exec failed" + exit 1 +fi +./test +llvm-profdata merge --sparse *.profraw -o test.profdata +if [ $? != 0 ] ; then + echo "llvm-profdata failed" + exit 1 +fi +llvm-cov export -format=text -instr-profile=test.profdata ./test > test.json +if [ $? != 0 ] ; then + echo "llvm-cov failed" + exit 1 +fi + +# disable function, branch and mcdc coverage +$COVER $LLVM2LCOV_TOOL --rc function_coverage=0 -o test.info test.json +if [ $? != 0 ] ; then + echo "llvm2lcov failed" + exit 1 +fi + +# disable mcdc coverage +$COVER $LLVM2LCOV_TOOL --branch -o test.info test.json +if [ $? != 0 ] ; then + echo "llvm2lcov failed" + exit 1 +fi + +# disable branch coverage +$COVER $LLVM2LCOV_TOOL --mcdc -o test.info test.json +if [ $? != 0 ] ; then + echo "llvm2lcov failed" + exit 1 +fi + +$COVER $LLVM2LCOV_TOOL --branch --mcdc -o test.info test.json +if [ $? != 0 ] ; then + echo "llvm2lcov failed" + exit 1 +fi + +# should be valid data to generate HTML +$COVER $GENHTML_TOOL --flat --branch --mcdc -o report test.info +if [ $? != 0 ] ; then + echo "genhtml failed" + exit 1 +fi + +# run again, excluding 'main.cpp' +$COVER $LLVM2LCOV_TOOL --branch --mcdc -o test.excl.info test.json --exclude '*/main.cpp' +if [ $? != 0 ] ; then + echo "llvm2lcov --exclude failed" + exit 1 +fi + +# should be 3 functions +N=`grep -c "FNA:" test.info` +if [ 3 != "$N" ] ; then + echo "wrong number of functions" + exit 1 +fi + +# look for expected location and function hit counts: +for d in \ + 'FNL:[0-9],20,25' \ + 'FNA:[0-9],2,_Z3fooc' \ + 'FNL:[0-9],27,72' \ + 'FNA:[0-9],1,main' \ + 'FNL:[0-9],2,4' \ + 'FNA:[0-9],1,main.cpp:_ZL3barv' \ + ; do + grep -E $d test.info + if [ 0 != $? ] ; then + echo "did not find expected function data $d" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done + +# lines main.cpp:(31-42) should be hit +for line in $(seq 31 42) ; \ + do \ + grep -E "DA:$line,1" test.info + if [ 0 != $? ] ; then + echo "did not find expected hit on function line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done + +# lines main.cpp:14, 45-48 should be 'not hit +for line in 14 45 46 47 48 ; do + grep "DA:$line,0" test.info + if [ 0 != $? ] ; then + echo "did not find expected zero hit on function line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done + +# lines main.cpp:30, 43, 51, 65 should be 'not instrumented +for line in 30 43 51 65 ; do + grep "DA:$line" test.info + if [ 0 == $? ] ; then + echo "find unexpected instrumented line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done + +# check lines total number +grep -E "LF:55$" test.info +if [ $? != 0 ] ; then + echo "unexpected total number of lines" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +# check lines hit number +grep -E "LH:50$" test.info +if [ $? != 0 ] ; then + echo "unexpected hit number of lines" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + +# check that branches have right expressions +line=41 +N=`grep -c "BRDA:$line," test.info` +if [ 2 != "$N" ] ; then + echo "did not find expected branches on line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +grep "BRDA:$line,0,(i <= 0) == True,1" test.info +if [ 0 != $? ] ; then + echo "did not find expected 'BRDA' entry on line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +grep "BRDA:$line,0,(i <= 0) == False,0" test.info +if [ 0 != $? ] ; then + echo "did not find expected 'BRDA' entry on line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + +# check that branches defined inside macros are instrumented right +# lines main.cpp:33, 36, 39, 44 should contain branches defined inside macros +for line in 33 36 39 44 ; do + grep -E "BRDA:$line," test.info + if [ 0 != $? ] ; then + echo "did not find expected branches on line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done + +# check branches total number +grep -E "BRF:54$" test.info +if [ $? != 0 ] ; then + echo "unexpected total number of branches" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +# check branches hit number +grep -E "BRH:34$" test.info +if [ $? != 0 ] ; then + echo "unexpected hit number of branches" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + +# line main.cpp:70 should contain 2 groups of MC/DC entries +line=70 +MCDC_1=`grep -c "MCDC:$line,2," test.info` +MCDC_2=`grep -c "MCDC:$line,3," test.info` +if [ 4 != "$MCDC_1" ] || [ 6 != "$MCDC_2" ] ; then + echo "did not find expected MC/DC entries on line $line" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +# check that MC/DC entries have right +N=`grep -c "MCDC:63,2,[tf],1,1,'i < 1' in 'a\[i\] && i < 1'" test.info` +if [ 2 != "$N" ] ; then + echo "did not find expected MC/DC entries on line 63" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +# check MC/DC defined in macros +grep -E "MCDC:6,2,[tf]" test.excl.info +if [ 0 != $? ] ; then + echo "did not find expected MC/DC" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +for m in \ + "MCDC:6,2,[tf]" \ + "MCDC:15,2,[tf]" \ + ; do + grep -E $m test.info + if [ 0 != $? ] ; then + echo "did not find expected MC/DC" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi + fi +done +# check MC/DC total number +grep -E "MCF:34$" test.info +if [ $? != 0 ] ; then + echo "unexpected total number of MC/DC entries" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi +# check MC/DC hit number +grep -E "MCH:10$" test.info +if [ $? != 0 ] ; then + echo "unexpected hit number of MC/DC entries" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + +# generate help message +$COVER ${EXEC_COVER} $LLVM2LCOV_TOOL --help +if [ 0 != $? ] ; then + echo "llvm2lcov help failed" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + +# incorrect option +$COVER ${EXEC_COVER} $LLVM2LCOV_TOOL --unsupported +$COVER $LLVM2LCOV_TOOL --unsupported -o test.info test.json +if [ 0 == $? ] ; then + echo "did not see incorrect option" + if [ 0 == $KEEP_GOING ] ; then + exit 1 + fi +fi + +echo "Tests passed" + +if [ "x$COVER" != "x" ] && [ $LOCAL_COVERAGE == 1 ]; then + cover ${COVER_DB} + $PERL2LCOV_TOOL -o ${COVER_DB}/perlcov.info ${COVER_DB} --ignore-errors inconsistent + $GENHTML_TOOL -o ${COVER_DB}/report ${COVER_DB}/perlcov.info --flat --show-navigation --branch --ignore-errors inconsistent +fi diff --git a/tests/llvm2lcov/main.cpp b/tests/llvm2lcov/main.cpp new file mode 100644 index 00000000..47141661 --- /dev/null +++ b/tests/llvm2lcov/main.cpp @@ -0,0 +1,72 @@ +#include "test.h" + +#define macro_1(expr) \ + do \ + { \ + } while (expr) + +#define macro_2(i, expr1, expr2) \ + do { \ + ++(i); \ + if (!(expr1)) \ + ++(i); \ + if (!(expr2)) \ + ++(i); \ + } while((expr1) && (expr2)); + +#define macro_3(expr) macro_1((expr)) + +void foo(char a) +{ + if (a) + /* comment + + */ return; +} + +int main() { + int a[] = {3, 12}; /* comment */ + int i; /* comment + comment + comment */ i = 0; + macro_1(i < 0); + macro_1 ( + BOOL(i < 0 && i % 2 == 0)) + ; + macro_2(i, i < 10, i > 0); + i = 0; + macro_3(i < 0); + macro_4(i > 0 && i < 10); + if (BOOL(i > 0) || + i <= 0) + ; + + if (BOOL(i > 0) + && BOOL(i < 0)) + { + ; + } + + for (; i < sizeof(a) / sizeof(*a); ++i) + + { + if ((a[i] % 4 + == 0) + && + (a[i] % 3 + == 0)) + { + ; + } + if (a[i] < 10) + ; + foo(a[i] && i < 1); + } + /* i == 2 + */ + do { + --i; + } while (i); + while(i < 2 && i < 3 && i < 4) { ++i; } for(i = 0; i < 5 && i < 4; ++i) { (void)i; } + return 0; +} diff --git a/tests/llvm2lcov/test.h b/tests/llvm2lcov/test.h new file mode 100644 index 00000000..851617f5 --- /dev/null +++ b/tests/llvm2lcov/test.h @@ -0,0 +1,8 @@ +static inline void bar() +{ + return; +} + +#define macro_4(expr) ((expr) ? ((void) 0) : bar()) + +#define BOOL(x) (!!(x))