1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-14 10:13:05 +02:00
pgbackrest/test/lib/pgBackRestTest/Common/CoverageTest.pm
David Steele 8af8029006 Fix lcov report when test module missing coverage.
We don't report branch coverage on test modules (e.g. test/src/module/common/errorTest.c) but the code that excluded branch coverage from the test module would also exclude it from all core modules if the test module was included in the lcov report due to lack of function/line coverage.

Adjust the coverage code to only exclude branches during the extraction of test module coverage.
2020-03-20 09:16:38 -04:00

974 lines
34 KiB
Perl

####################################################################################################################################
# Generate C Coverage Report
####################################################################################################################################
package pgBackRestTest::Common::CoverageTest;
####################################################################################################################################
# Perl includes
####################################################################################################################################
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use English '-no_match_vars';
use Digest::SHA qw(sha1_hex);
use Exporter qw(import);
our @EXPORT = qw();
use File::Basename qw(dirname);
use pgBackRestDoc::Common::Log;
use pgBackRestDoc::Common::String;
use pgBackRestDoc::Html::DocHtmlBuilder;
use pgBackRestDoc::Html::DocHtmlElement;
use pgBackRestDoc::ProjectInfo;
use pgBackRestTest::Common::ContainerTest;
use pgBackRestTest::Common::DefineTest;
use pgBackRestTest::Common::ExecuteTest;
use pgBackRestTest::Common::ListTest;
use pgBackRestTest::Common::RunTest;
####################################################################################################################################
# Generate an lcov configuration file
####################################################################################################################################
sub coverageLCovConfigGenerate
{
my $oStorage = shift;
my $strOutFile = shift;
my $bCoverageSummary = shift;
my $strBranchFilter =
'OBJECT_DEFINE_[A-Z0-9_]+\(|\s{4}[A-Z][A-Z0-9_]+\([^\?]*\)|\s{4}(ASSERT|assert|switch\s)\(|\{\+{0,1}' .
($bCoverageSummary ? 'uncoverable_branch' : 'uncover(ed|able)_branch');
my $strLineFilter = '\{\+{0,1}uncover' . ($bCoverageSummary ? 'able' : '(ed|able)') . '[^_]';
my $strConfig =
"# LCOV Settings\n" .
"\n" .
"# Specify if branch coverage data should be collected and processed\n" .
"lcov_branch_coverage=1\n" .
"\n" .
"# Specify the regular expression of lines to exclude from branch coverage\n" .
"#\n" .
'# OBJECT_DEFINE_[A-Z0-9_]+\( - exclude object definitions' . "\n" .
'# \s{4}[A-Z][A-Z0-9_]+\([^\?]*\) - exclude macros that do not take a conditional parameter and are not themselves a parameter' . "\n" .
'# ASSERT/(|assert\( - exclude asserts since it usually not possible to trigger both branches' . "\n" .
'# switch \( - lcov requires default: to show complete coverage but --Wswitch-enum enforces all enum values be present' . "\n" .
"lcov_excl_br_line=${strBranchFilter}\n" .
"\n" .
"# Specify the regular expression of lines to exclude\n" .
"lcov_excl_line=${strLineFilter}\n" .
"\n" .
"# Coverage rate limits\n" .
"genhtml_hi_limit = 100\n" .
"genhtml_med_limit = 90\n" .
"\n" .
"# Width of line coverage field in source code view\n" .
"genhtml_line_field_width = 9\n";
# Write configuration file
$oStorage->put($strOutFile, $strConfig);
}
push @EXPORT, qw(coverageLCovConfigGenerate);
####################################################################################################################################
# Extract coverage using gcov
####################################################################################################################################
sub coverageExtract
{
my $oStorage = shift;
my $strModule = shift;
my $strTest = shift;
my $bSummary = shift;
my $strContainerImage = shift;
my $strWorkPath = shift;
my $strWorkTmpPath = shift;
my $strWorkUnitPath = shift;
my $strTestResultCoveragePath = shift . '/coverage';
# Generate a list of files to cover
my $hTestCoverage = (testDefModuleTest($strModule, $strTest))->{&TESTDEF_COVERAGE};
my @stryCoveredModule;
foreach my $strModule (sort(keys(%{$hTestCoverage})))
{
push (@stryCoveredModule, $strModule);
}
push(@stryCoveredModule, "module/${strModule}/" . testRunName($strTest, false) . 'Test');
# Generate coverage reports for the modules
my $strLCovConf = "${strTestResultCoveragePath}/raw/lcov.conf";
coverageLCovConfigGenerate($oStorage, $strLCovConf, $bSummary);
my $strLCovExe = "lcov --config-file=${strLCovConf}";
my $strLCovOut = "${strWorkUnitPath}/test.lcov";
my $strLCovOutTmp = "${strWorkUnitPath}/test.tmp.lcov";
executeTest(
(defined($strContainerImage) ? 'docker exec -i -u ' . TEST_USER . " ${strContainerImage} " : '') .
"${strLCovExe} --capture --directory=${strWorkUnitPath} --o=${strLCovOut}");
# Generate coverage report for each module
foreach my $strCoveredModule (@stryCoveredModule)
{
my $strModuleName = testRunName($strCoveredModule, false);
my $strModuleOutName = $strModuleName;
my $bTest = false;
if ($strModuleOutName =~ /^module/mg)
{
$strModuleOutName =~ s/^module/test/mg;
$bTest = true;
}
# Generate lcov reports
my $strModulePath =
"${strWorkPath}/repo/" .
(${strModuleOutName} =~ /^test\// ?
'test/src/module/' . substr(${strModuleOutName}, 5) : "src/${strModuleOutName}");
my $strLCovFile = "${strTestResultCoveragePath}/raw/${strModuleOutName}.lcov";
my $strLCovTotal = "${strWorkTmpPath}/all.lcov";
executeTest(
"${strLCovExe}" . ($bTest ? ' --rc lcov_branch_coverage=0' : '') . " --extract=${strLCovOut} */${strModuleName}.c" .
" --o=${strLCovOutTmp}");
# Combine with prior run if there was one
if ($oStorage->exists($strLCovFile))
{
my $strCoverage = ${$oStorage->get($strLCovOutTmp)};
$strCoverage =~ s/^SF\:.*$/SF:$strModulePath\.c/mg;
$oStorage->put($strLCovOutTmp, $strCoverage);
executeTest("${strLCovExe} --add-tracefile=${strLCovOutTmp} --add-tracefile=${strLCovFile} --o=${strLCovOutTmp}");
}
# Update source file
my $strCoverage = ${$oStorage->get($strLCovOutTmp)};
if (defined($strCoverage))
{
if (!$bTest && $hTestCoverage->{$strCoveredModule} eq TESTDEF_COVERAGE_NOCODE)
{
confess &log(ERROR, "module '${strCoveredModule}' is marked 'no code' but has code");
}
# Get coverage info
my $iTotalLines = (split(':', ($strCoverage =~ m/^LF:.*$/mg)[0]))[1] + 0;
my $iCoveredLines = (split(':', ($strCoverage =~ m/^LH:.*$/mg)[0]))[1] + 0;
my $iTotalBranches = 0;
my $iCoveredBranches = 0;
if ($strCoverage =~ /^BRF\:/mg && $strCoverage =~ /^BRH\:/mg)
{
# If this isn't here the statements below fail -- huh?
my @match = $strCoverage =~ m/^BRF\:.*$/mg;
$iTotalBranches = (split(':', ($strCoverage =~ m/^BRF:.*$/mg)[0]))[1] + 0;
$iCoveredBranches = (split(':', ($strCoverage =~ m/^BRH:.*$/mg)[0]))[1] + 0;
}
# Report coverage if this is not a test or if the test does not have complete coverage
if (!$bTest || $iTotalLines != $iCoveredLines || $iTotalBranches != $iCoveredBranches)
{
# Fix source file name
$strCoverage =~ s/^SF\:.*$/SF:$strModulePath\.c/mg;
$oStorage->put($oStorage->openWrite($strLCovFile, {bPathCreate => true}), $strCoverage);
if ($oStorage->exists($strLCovTotal))
{
executeTest("${strLCovExe} --add-tracefile=${strLCovFile} --add-tracefile=${strLCovTotal} --o=${strLCovTotal}");
}
else
{
$oStorage->copy($strLCovFile, $strLCovTotal)
}
}
else
{
$oStorage->remove($strLCovFile);
}
}
else
{
if ($hTestCoverage->{$strCoveredModule} ne TESTDEF_COVERAGE_NOCODE)
{
confess &log(ERROR, "module '${strCoveredModule}' is marked 'code' but has no code");
}
}
}
}
push @EXPORT, qw(coverageExtract);
####################################################################################################################################
# Validate converage and generate reports
####################################################################################################################################
sub coverageValidateAndGenerate
{
my $oyTestRun = shift;
my $oStorage = shift;
my $bCoverageSummary = shift;
my $strWorkTmpPath = shift;
my $strTestResultCoveragePath = shift . '/coverage';
my $strTestResultSummaryPath = shift;
my $result = 0;
# Determine which modules were covered (only check coverage if all tests were successful)
#-----------------------------------------------------------------------------------------------------------------------
my $hModuleTest; # Everything that was run
# Build a hash of all modules, tests, and runs that were executed
foreach my $hTestRun (@{$oyTestRun})
{
# Get coverage for the module
my $strModule = $hTestRun->{&TEST_MODULE};
my $hModule = testDefModule($strModule);
# Get coverage for the test
my $strTest = $hTestRun->{&TEST_NAME};
my $hTest = testDefModuleTest($strModule, $strTest);
# If no tests are listed it means all of them were run
if (@{$hTestRun->{&TEST_RUN}} == 0)
{
$hModuleTest->{$strModule}{$strTest} = true;
}
}
# Now compare against code modules that should have full coverage
my $hCoverageList = testDefCoverageList();
my $hCoverageType = testDefCoverageType();
my $hCoverageActual;
foreach my $strCodeModule (sort(keys(%{$hCoverageList})))
{
if (@{$hCoverageList->{$strCodeModule}} > 0)
{
my $iCoverageTotal = 0;
foreach my $hTest (@{$hCoverageList->{$strCodeModule}})
{
if (!defined($hModuleTest->{$hTest->{strModule}}{$hTest->{strTest}}))
{
next;
}
$iCoverageTotal++;
}
if (@{$hCoverageList->{$strCodeModule}} == $iCoverageTotal)
{
$hCoverageActual->{testRunName($strCodeModule, false)} = $hCoverageType->{$strCodeModule};
}
}
}
if (keys(%{$hCoverageActual}) == 0)
{
&log(INFO, 'no code modules had all tests run required for coverage');
}
# Generate C coverage report
#---------------------------------------------------------------------------------------------------------------------------
&log(INFO, 'writing C coverage report');
my $strLCovFile = "${strWorkTmpPath}/all.lcov";
if ($oStorage->exists($strLCovFile))
{
executeTest(
"genhtml ${strLCovFile} --config-file=${strTestResultCoveragePath}/raw/lcov.conf" .
" --prefix=${strWorkTmpPath}/repo" .
" --output-directory=${strTestResultCoveragePath}/lcov");
foreach my $strCodeModule (sort(keys(%{$hCoverageActual})))
{
my $strCoverageFile = $strCodeModule;
$strCoverageFile =~ s/^module/test/mg;
$strCoverageFile = "${strTestResultCoveragePath}/raw/${strCoverageFile}.lcov";
my $strCoverage = $oStorage->get($oStorage->openRead($strCoverageFile, {bIgnoreMissing => true}));
if (defined($strCoverage) && defined($$strCoverage))
{
my $iTotalLines = (split(':', ($$strCoverage =~ m/^LF\:.*$/mg)[0]))[1] + 0;
my $iCoveredLines = (split(':', ($$strCoverage =~ m/^LH\:.*$/mg)[0]))[1] + 0;
my $iTotalBranches = 0;
my $iCoveredBranches = 0;
if ($$strCoverage =~ /^BRF\:/mg && $$strCoverage =~ /^BRH\:/mg)
{
# If this isn't here the statements below fail -- huh?
my @match = $$strCoverage =~ m/^BRF\:.*$/mg;
$iTotalBranches = (split(':', ($$strCoverage =~ m/^BRF\:.*$/mg)[0]))[1] + 0;
$iCoveredBranches = (split(':', ($$strCoverage =~ m/^BRH\:.*$/mg)[0]))[1] + 0;
}
# Generate detail if there is missing coverage
my $strDetail = undef;
if ($iCoveredLines != $iTotalLines)
{
$strDetail .= "$iCoveredLines/$iTotalLines lines";
}
if ($iTotalBranches != $iCoveredBranches)
{
$strDetail .= (defined($strDetail) ? ', ' : '') . "$iCoveredBranches/$iTotalBranches branches";
}
if (defined($strDetail))
{
&log(ERROR, "c module ${strCodeModule} is not fully covered ($strDetail)");
$result++;
}
}
}
coverageGenerate(
$oStorage, "${strWorkTmpPath}/repo", "${strTestResultCoveragePath}/raw", "${strTestResultCoveragePath}/coverage.html");
if ($bCoverageSummary)
{
&log(INFO, 'writing C coverage summary report');
coverageDocSummaryGenerate(
$oStorage, "${strTestResultCoveragePath}/raw", "${strTestResultSummaryPath}/metric-coverage-report.auto.xml");
}
}
else
{
executeTest("rm -rf ${strTestResultCoveragePath}/test/tesult/coverage");
}
return $result;
}
push @EXPORT, qw(coverageValidateAndGenerate);
####################################################################################################################################
# Generate a C coverage report
####################################################################################################################################
sub coverageGenerate
{
my $oStorage = shift;
my $strBasePath = shift;
my $strCoveragePath = shift;
my $strOutFile = shift;
# Track missing coverage
my $rhCoverage = {};
# Find all lcov files in the coverage path
my $rhManifest = $oStorage->manifest($strCoveragePath);
foreach my $strFileCov (sort(keys(%{$rhManifest})))
{
if ($strFileCov =~ /\.lcov$/)
{
my $strCoverage = ${$oStorage->get("${strCoveragePath}/${strFileCov}")};
# Show that the file is part of the coverage report even if there is no missing coverage
my $strFile;
my $iBranchLine = -1;
my $iBranch = undef;
my $iBranchIdx = -1;
my $iBranchPart = undef;
foreach my $strLine (split("\n", $strCoverage))
{
# Get source file name
if ($strLine =~ /^SF\:/)
{
$strFile = substr($strLine, 3);
$rhCoverage->{$strFile} = undef;
# Generate a random anchor so new reports will not show links as already followed. This is also an easy way
# to create valid, disambiguos links.
$rhCoverage->{$strFile}{anchor} = sha1_hex(rand(16));
}
# Check branch coverage
elsif ($strLine =~ /^BRDA\:/)
{
my @stryData = split("\,", substr($strLine, 5));
if (@stryData < 4)
{
confess &log(ERROR, "'${strLine}' should have four fields");
}
my $strBranchLine = sprintf("%09d", $stryData[0]);
if ($iBranchLine != $stryData[0])
{
$iBranchLine = $stryData[0];
$iBranch = $stryData[1];
$iBranchIdx = 0;
$iBranchPart = 0;
}
elsif ($iBranch != $stryData[1])
{
if ($iBranchPart != 1)
{
confess &log(ERROR, "line ${iBranchLine}, branch ${iBranch} does not have two parts");
}
$iBranch = $stryData[1];
$iBranchIdx++;
$iBranchPart = 0;
}
else
{
$iBranchPart++;
}
$rhCoverage->{$strFile}{line}{$strBranchLine}{branch}{$iBranchIdx}{$iBranchPart} =
$stryData[3] eq '-' || $stryData[3] eq '0' ? false : true;
}
# Check line coverage
if ($strLine =~ /^DA\:/)
{
my @stryData = split("\,", substr($strLine, 3));
if (@stryData < 2)
{
confess &log(ERROR, "'${strLine}' should have two fields");
}
my $strBranchLine = sprintf("%09d", $stryData[0]);
if ($stryData[1] eq '0')
{
$rhCoverage->{$strFile}{line}{$strBranchLine}{statement} = 0;
}
}
}
}
}
# Remove branch coverage on lines that are completely covered
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
foreach my $iLine (sort(keys(%{$rhCoverage->{$strFile}{line}})))
{
if (defined($rhCoverage->{$strFile}{line}{$iLine}{branch}))
{
# We'll assume the line is completely covered
my $bCovered = true;
foreach my $iBranch (sort(keys(%{$rhCoverage->{$strFile}{line}{$iLine}{branch}})))
{
foreach my $iBranchPart (sort(keys(%{$rhCoverage->{$strFile}{line}{$iLine}{branch}{$iBranch}})))
{
if (!$rhCoverage->{$strFile}{line}{$iLine}{branch}{$iBranch}{$iBranchPart})
{
$bCovered = false;
}
}
}
if ($bCovered)
{
# &log(WARN, "removed branch coverage for ${strFile} line ${iLine}");
if (defined($rhCoverage->{$strFile}{line}{$iLine}{statement}))
{
delete($rhCoverage->{$strFile}{line}{$iLine}{branch});
}
else
{
delete($rhCoverage->{$strFile}{line}{$iLine});
}
}
}
}
}
# Remove line when no lines are uncovered
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
my $bCovered = true;
foreach my $iLine (sort(keys(%{$rhCoverage->{$strFile}{line}})))
{
$bCovered = false;
last;
}
if ($bCovered)
{
delete($rhCoverage->{$strFile}{line});
}
}
# Report on the entire function if any lines in the function are uncovered
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
if (defined($rhCoverage->{$strFile}{line}))
{
my $strC = ${$oStorage->get($strFile)};
my @stryC = split("\n", $strC);
foreach my $iLine (sort(keys(%{$rhCoverage->{$strFile}{line}})))
{
# Run back to the beginning of the function comment
for (my $iLineIdx = $iLine; $iLineIdx >= 0; $iLineIdx--)
{
if (!defined($rhCoverage->{$strFile}{line}{sprintf("%09d", $iLineIdx)}))
{
$rhCoverage->{$strFile}{line}{sprintf("%09d", $iLineIdx)} = undef;
}
last if ($stryC[$iLineIdx - 1] =~ '^\/\*');
}
# Run forward to the end of the function
for (my $iLineIdx = $iLine; $iLineIdx < @stryC; $iLineIdx++)
{
if (!defined($rhCoverage->{$strFile}{line}{sprintf("%09d", $iLineIdx)}))
{
$rhCoverage->{$strFile}{line}{sprintf("%09d", $iLineIdx)} = undef;
}
last if ($stryC[$iLineIdx - 1] eq '}');
}
}
}
}
# Build html
my $strTitle = PROJECT_NAME . ' Coverage Report';
my $strDarkRed = '#580000';
my $strGray = '#555555';
my $strDarkGray = '#333333';
my $oHtml = new pgBackRestDoc::Html::DocHtmlBuilder(
PROJECT_NAME, $strTitle,
undef, undef, undef,
true, true,
"html\n" .
"{\n" .
" background-color: ${strGray};\n" .
" font-family: Avenir, Corbel, sans-serif;\n" .
" color: white;\n" .
" font-size: 12pt;\n" .
" margin-top: 8px;\n" .
" margin-left: 1\%;\n" .
" margin-right: 1\%;\n" .
" width: 98\%;\n" .
"}\n" .
"\n" .
"body\n" .
"{\n" .
" margin: 0px auto;\n" .
" padding: 0px;\n" .
" width: 100\%;\n" .
" text-align: justify;\n" .
"}\n" .
".title\n" .
"{\n" .
" width: 100\%;\n" .
" text-align: center;\n" .
" font-size: 200\%;\n" .
"}\n" .
"\n" .
".list-table\n" .
"{\n" .
" width: 100\%;\n" .
"}\n" .
"\n" .
".list-table-caption\n" .
"{\n" .
" margin-top: 1em;\n" .
" font-size: 130\%;\n" .
" margin-bottom: .25em;\n" .
"}\n" .
"\n" .
".list-table-caption::after\n" .
"{\n" .
" content: \"Modules Tested for Coverage:\";\n" .
"}\n" .
"\n" .
".list-table-header-file\n" .
"{\n" .
" padding-left: .5em;\n" .
" padding-right: .5em;\n" .
" background-color: ${strDarkGray};\n" .
" width: 100\%;\n" .
"}\n" .
"\n" .
".list-table-row-uncovered\n" .
"{\n" .
" background-color: ${strDarkRed};\n" .
" color: white;\n" .
" width: 100\%;\n" .
"}\n" .
"\n" .
".list-table-row-file\n" .
"{\n" .
" padding-left: .5em;\n" .
" padding-right: .5em;\n" .
"}\n" .
"\n" .
".report-table\n" .
"{\n" .
" width: 100\%;\n" .
"}\n" .
"\n" .
".report-table-caption\n" .
"{\n" .
" margin-top: 1em;\n" .
" font-size: 130\%;\n" .
" margin-bottom: .25em;\n" .
"}\n" .
"\n" .
".report-table-caption::after\n" .
"{\n" .
" content: \" report:\";\n" .
"}\n" .
"\n" .
".report-table-header\n" .
"{\n" .
"}\n" .
"\n" .
".report-table-header-line, .report-table-header-branch, .report-table-header-code\n" .
"{\n" .
" padding-left: .5em;\n" .
" padding-right: .5em;\n" .
" background-color: ${strDarkGray};\n" .
"}\n" .
"\n" .
".report-table-header-code\n" .
"{\n" .
" width: 100\%;\n" .
"}\n" .
"\n" .
".report-table-row-dot-tr, .report-table-row\n" .
"{\n" .
" font-family: \"Courier New\", Courier, monospace;\n" .
"}\n" .
"\n" .
".report-table-row-dot-skip\n" .
"{\n" .
" height: 1em;\n" .
" padding-top: .25em;\n" .
" padding-bottom: .25em;\n" .
" text-align: center;\n" .
"}\n" .
"\n" .
".report-table-row-line, .report-table-row-branch, .report-table-row-branch-uncovered," .
" .report-table-row-code, .report-table-row-code-uncovered\n" .
"{\n" .
" padding-left: .5em;\n" .
" padding-right: .5em;\n" .
"}\n" .
"\n" .
".report-table-row-line\n" .
"{\n" .
" text-align: right;\n" .
"}\n" .
"\n" .
".report-table-row-branch, .report-table-row-branch-uncovered\n" .
"{\n" .
" text-align: right;\n" .
" white-space: nowrap;\n" .
"}\n" .
"\n" .
".report-table-row-branch-uncovered\n" .
"{\n" .
" background-color: ${strDarkRed};\n" .
" color: white;\n" .
"}\n" .
"\n" .
".report-table-row-code, .report-table-row-code-uncovered\n" .
"{\n" .
" white-space: pre;\n" .
"}\n" .
"\n" .
".report-table-row-code-uncovered\n" .
"{\n" .
" background-color: ${strDarkRed};\n" .
" color: white;\n" .
"}\n");
# File list title
$oHtml->bodyGet()->addNew(HTML_DIV, 'title', {strContent => $strTitle});
# Build the file list table
$oHtml->bodyGet()->addNew(HTML_DIV, 'list-table-caption');
my $oTable = $oHtml->bodyGet()->addNew(HTML_TABLE, 'list-table');
my $oHeader = $oTable->addNew(HTML_TR, 'list-table-header');
$oHeader->addNew(HTML_TH, 'list-table-header-file', {strContent => 'FILE'});
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
my $oRow = $oTable->addNew(HTML_TR, 'list-table-row-' . (defined($rhCoverage->{$strFile}{line}) ? 'uncovered' : 'covered'));
# Link only created when file is uncovered
if (defined($rhCoverage->{$strFile}{line}))
{
$oRow->addNew(HTML_TD, 'list-table-row-file')->addNew(
HTML_A, undef,
{strContent => substr($strFile, length($strBasePath) + 1), strRef => '#' . $rhCoverage->{$strFile}{anchor}});
}
# Else just show the file name
else
{
$oRow->addNew(HTML_TD, 'list-table-row-file', {strContent => substr($strFile, length($strBasePath) + 1)});
}
}
# Report on files that are missing coverage
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
if (defined($rhCoverage->{$strFile}{line}))
{
# Anchor only created when file is uncovered
$oHtml->bodyGet()->addNew(HTML_A, undef, {strId => $rhCoverage->{$strFile}{anchor}});
# Report table caption, i.e. the uncovered file name
$oHtml->bodyGet()->addNew(HTML_DIV, 'report-table-caption', {strContent => substr($strFile, length($strBasePath) + 1)});
# Build the file report table
$oTable = $oHtml->bodyGet()->addNew(HTML_TABLE, 'report-table');
$oHeader = $oTable->addNew(HTML_TR, 'report-table-header');
$oHeader->addNew(HTML_TH, 'report-table-header-line', {strContent => 'LINE'});
$oHeader->addNew(HTML_TH, 'report-table-header-branch', {strContent => 'BRANCH'});
$oHeader->addNew(HTML_TH, 'report-table-header-code', {strContent => 'CODE'});
my $strC = ${$oStorage->get($strFile)};
my @stryC = split("\n", $strC);
my $iLastLine = undef;
foreach my $strLine (sort(keys(%{$rhCoverage->{$strFile}{line}})))
{
if (defined($iLastLine) && $strLine != $iLastLine + 1)
{
my $oRow = $oTable->addNew(HTML_TR, 'report-table-row-dot');
$oRow->addNew(HTML_TD, 'report-table-row-dot-skip', {strExtra => 'colspan="3"'});
}
$iLastLine = $strLine;
my $iLine = int($strLine);
my $oRow = $oTable->addNew(HTML_TR, 'report-table-row');
$oRow->addNew(HTML_TD, 'report-table-row-line', {strContent => $iLine});
my $strBranch;
# Show missing branch coverage
if (defined($rhCoverage->{$strFile}{line}{$strLine}{branch}) &&
!defined($rhCoverage->{$strFile}{line}{$strLine}{statement}))
{
my $iBranchIdx = 0;
foreach my $iBranch (sort(keys(%{$rhCoverage->{$strFile}{line}{$strLine}{branch}})))
{
$strBranch .=
'[' .
($rhCoverage->{$strFile}{line}{$strLine}{branch}{$iBranch}{0} ?
'+' : '-') .
' ' .
($rhCoverage->{$strFile}{line}{$strLine}{branch}{$iBranch}{1} ?
'+' : '-') .
']';
if ($iBranchIdx == 1)
{
$strBranch .= '<br/>';
$iBranchIdx = 0;
}
else
{
$strBranch .= ' ';
$iBranchIdx++;
}
}
}
$oRow->addNew(
HTML_TD, 'report-table-row-branch' . (defined($strBranch) ? '-uncovered' : ''),
{strContent => $strBranch});
# Color code based on coverage
my $bUncovered =
defined($rhCoverage->{$strFile}{line}{$strLine}{branch}) ||
defined($rhCoverage->{$strFile}{line}{$strLine}{statement});
$oRow->addNew(
HTML_TD, 'report-table-row-code' . ($bUncovered ? '-uncovered' : ''),
{bPre => true, strContent => $stryC[$strLine - 1]});
}
}
}
# Write coverage report
$oStorage->put($strOutFile, $oHtml->htmlGet());
}
push @EXPORT, qw(coverageGenerate);
####################################################################################################################################
# Generate a C coverage summary for the documentation
####################################################################################################################################
sub coverageDocSummaryGenerateValue
{
my $iHit = shift;
my $iFound = shift;
if (!defined($iFound) || !defined($iHit) || $iFound == 0)
{
return "---";
}
my $fPercent = $iHit * 100 / $iFound;
my $strPercent;
if ($fPercent == 100)
{
$strPercent = '100.0';
}
elsif ($fPercent > 99.99)
{
$strPercent = '99.99';
}
else
{
$strPercent = sprintf("%.2f", $fPercent);
}
return "${iHit}/${iFound} (${strPercent}%)";
}
sub coverageDocSummaryGenerate
{
my $oStorage = shift;
my $strCoveragePath = shift;
my $strOutFile = shift;
# Track coverage summary
my $rhSummary;
# Find all lcov files in the coverage path
my $rhManifest = $oStorage->manifest($strCoveragePath);
foreach my $strFileCov (sort(keys(%{$rhManifest})))
{
next if $strFileCov =~ /^test\//;
if ($strFileCov =~ /\.lcov$/)
{
my $strCoverage = ${$oStorage->get("${strCoveragePath}/${strFileCov}")};
my $strModule = dirname($strFileCov);
foreach my $strLine (split("\n", $strCoverage))
{
# Get Line Coverage
if ($strLine =~ /^LF\:/)
{
$rhSummary->{$strModule}{line}{found} += substr($strLine, 3) + 0;
$rhSummary->{zzztotal}{line}{found} += substr($strLine, 3) + 0;
}
if ($strLine =~ /^LH\:/)
{
$rhSummary->{$strModule}{line}{hit} += substr($strLine, 3) + 0;
$rhSummary->{zzztotal}{line}{hit} += substr($strLine, 3) + 0;
}
# Get Function Coverage
if ($strLine =~ /^FNF\:/)
{
$rhSummary->{$strModule}{function}{found} += substr($strLine, 4) + 0;
$rhSummary->{zzztotal}{function}{found} += substr($strLine, 4) + 0;
}
if ($strLine =~ /^FNH\:/)
{
$rhSummary->{$strModule}{function}{hit} += substr($strLine, 4) + 0;
$rhSummary->{zzztotal}{function}{hit} += substr($strLine, 4) + 0;
}
# Get Branch Coverage
if ($strLine =~ /^BRF\:/)
{
$rhSummary->{$strModule}{branch}{found} += substr($strLine, 4) + 0;
$rhSummary->{zzztotal}{branch}{found} += substr($strLine, 4) + 0;
}
if ($strLine =~ /^BRH\:/)
{
$rhSummary->{$strModule}{branch}{hit} += substr($strLine, 4) + 0;
$rhSummary->{zzztotal}{branch}{hit} += substr($strLine, 4) + 0;
}
}
}
}
# use Data::Dumper;confess Dumper($rhSummary);
my $strSummary;
foreach my $strModule (sort(keys(%{$rhSummary})))
{
my $rhModuleData = $rhSummary->{$strModule};
$strSummary .=
(defined($strSummary) ? "\n\n" : '') .
"<table-row>\n" .
" <table-cell>" . ($strModule eq 'zzztotal' ? 'TOTAL' : $strModule) . "</table-cell>\n" .
" <table-cell>" .
coverageDocSummaryGenerateValue($rhModuleData->{function}{hit}, $rhModuleData->{function}{found}) .
"</table-cell>\n" .
" <table-cell>" .
coverageDocSummaryGenerateValue($rhModuleData->{branch}{hit}, $rhModuleData->{branch}{found}) .
"</table-cell>\n" .
" <table-cell>" .
coverageDocSummaryGenerateValue($rhModuleData->{line}{hit}, $rhModuleData->{line}{found}) .
"</table-cell>\n" .
"</table-row>";
}
# Write coverage report
$oStorage->put($strOutFile, $strSummary);
}
push @EXPORT, qw(coverageDocSummaryGenerate);
1;