####################################################################################################################################
# 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;
####################################################################################################################################
# testRunName
#
# Create module/test names by upper-casing the first letter and then inserting capitals after each -.
####################################################################################################################################
sub testRunName
{
my $strName = shift;
my $bInitCapFirst = shift;
$bInitCapFirst = defined($bInitCapFirst) ? $bInitCapFirst : true;
my $bFirst = true;
my @stryName = split('\-', $strName);
$strName = undef;
foreach my $strPart (@stryName)
{
$strName .= ($bFirst && $bInitCapFirst) || !$bFirst ? ucfirst($strPart) : $strPart;
$bFirst = false;
}
return $strName;
}
####################################################################################################################################
# Generate an lcov configuration file
####################################################################################################################################
sub coverageLCovConfigGenerate
{
my $oStorage = shift;
my $strOutFile = shift;
my $bContainer = shift;
my $bCoverageSummary = shift;
my $strBranchFilter =
'OBJECT_DEFINE_[A-Z0-9_]+\(|\s{4}[A-Z][A-Z0-9_]+\([^\?]*\)|\s{4}(ASSERT|CHECK|CHECK_FMT|assert|switch\s)\(|\{\+{0,1}' .
($bCoverageSummary ? 'uncoverable_branch' : 'uncover(ed|able)_branch');
my $strLineFilter =
'\{\+{0,1}' . ($bCoverageSummary ? 'uncoverable' : '(uncover(ed|able)' . ($bContainer ? '' : '|vm_covered') . ')') . '[^_]';
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/(|CHECK/(|assert\( - exclude asserts/checks 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 $bContainer = shift;
my $bSummary = shift;
my $strContainerImage = shift;
my $strWorkPath = shift;
my $strWorkTmpPath = shift;
my $strWorkUnitPath = shift;
my $strTestResultCoveragePath = shift . '/coverage';
# Coverage summary must be run in a container
if ($bSummary && !$bContainer)
{
confess &log(ERROR, "coverage summary must be run on containers for full 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, $bContainer, $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} 2>&1");
# Generate coverage report for each module
foreach my $strCoveredModule (@stryCoveredModule)
{
my $strModuleName = testRunName($strCoveredModule, false);
if ($strModuleName =~ /^test/mg)
{
$strModuleName =~ s/^test/src/mg;
}
elsif ($strModuleName =~ /^doc/mg)
{
$strModuleName =~ s/^doc/doc\/src/mg;
}
my $strModuleOutName = $strModuleName;
my $bTest = false;
if ($strModuleOutName =~ /^module/mg)
{
$strModuleOutName =~ s/^module/test/mg;
$bTest = true;
}
# Generate lcov reports
my $strModulePath = "${strWorkPath}/repo/";
if (${strModuleOutName} =~ /^src\//)
{
$strModulePath .= 'test/src/' . substr(${strModuleOutName}, 4);
}
elsif (${strModuleOutName} =~ /^test\//)
{
$strModulePath .= 'test/src/module/' . substr(${strModuleOutName}, 5);
}
elsif (${strModuleOutName} =~ /^doc\//)
{
$strModulePath .= "${strModuleOutName}";
}
else
{
$strModulePath .= "src/${strModuleOutName}";
}
my $strLCovFile = "${strTestResultCoveragePath}/raw/${strModuleOutName}.lcov";
my $strLCovTotal = "${strWorkTmpPath}/all.lcov";
my $bInc = $strModuleName =~ '\.vendor$' || $strModuleName =~ '\.auto$';
my $strModuleSourceFile = $strModulePath . '.c' . ($bInc ? '.inc' : '');
executeTest(
"${strLCovExe}" . ($bTest ? ' --rc lcov_branch_coverage=0' : '') . " --extract=${strLCovOut} *${strModuleName}.c" .
($bInc ? '.inc' : '') . " --o=${strLCovOutTmp}");
# Combine with prior run if there was one
if ($oStorage->exists($strLCovFile))
{
my $strCoverage = ${$oStorage->get($strLCovOutTmp)};
$strCoverage =~ s/^SF\:.*$/SF:$strModuleSourceFile/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:$strModuleSourceFile/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 $bCoverageReport = shift;
my $bCoverageSummary = shift;
my $strWorkPath = 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
#---------------------------------------------------------------------------------------------------------------------------
my $strLCovFile = "${strWorkTmpPath}/all.lcov";
if ($oStorage->exists($strLCovFile))
{
foreach my $strCodeModule (sort(keys(%{$hCoverageActual})))
{
my $strCoverageFile = $strCodeModule;
$strCoverageFile =~ s/^test/src/mg;
$strCoverageFile =~ s/^doc/doc\/src/mg;
$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++;
}
}
}
if ($result == 0)
{
&log(INFO, "tested modules have full coverage");
}
# Always generate unified coverage report if there was missing coverage. This is useful for CI.
if ($bCoverageReport || $result != 0)
{
&log(INFO, 'writing C coverage report');
if ($bCoverageReport)
{
executeTest(
"genhtml ${strLCovFile} --config-file=${strTestResultCoveragePath}/raw/lcov.conf" .
" --prefix=${strWorkPath}/repo" .
" --output-directory=${strTestResultCoveragePath}/lcov");
}
coverageGenerate(
$oStorage, "${strWorkPath}/repo", "${strTestResultCoveragePath}/raw", "${strTestResultCoveragePath}/coverage.html",
$bCoverageReport);
}
# Else output report status in the HTML
else
{
$oStorage->put(
"${strTestResultCoveragePath}/coverage.html",
"
[ " . ($result == 0 ? "Coverage Complete" : "No Coverage Report") . " ]");
}
if ($bCoverageSummary)
{
&log(INFO, 'writing C coverage summary report');
coverageDocSummaryGenerate(
$oStorage, "${strTestResultCoveragePath}/raw", "${strTestResultSummaryPath}/metric-coverage-report.auto.xml");
}
}
return $result;
}
push @EXPORT, qw(coverageValidateAndGenerate);
####################################################################################################################################
# Generate a C coverage report
####################################################################################################################################
# Helper to get the function for the current line
sub coverageGenerateFunction
{
my $rhFile = shift;
my $iCurrentLine = shift;
my $rhFunction;
foreach my $iFunctionLine (sort(keys(%{$rhFile->{function}})))
{
last if $iCurrentLine < $iFunctionLine;
$rhFunction = $rhFile->{function}{$iFunctionLine};
}
if (!defined($rhFunction))
{
confess &log(ERROR, "function not found at line ${iCurrentLine}");
}
return $rhFunction;
}
sub coverageGenerate
{
my $oStorage = shift;
my $strBasePath = shift;
my $strCoveragePath = shift;
my $strOutFile = shift;
my $bCoverageReport = 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 a coverage report was not requested then skip coverage of test modules. If we are here it means there was missing
# coverage on CI and we want to keep the report as small as possible.
next if !$bCoverageReport && $strFileCov =~ /Test\.lcov$/;
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));
}
# Mark functions as initially covered
if ($strLine =~ /^FN\:/)
{
my ($strLineBegin) = split("\,", substr($strLine, 3));
$rhCoverage->{$strFile}{function}{sprintf("%09d", $strLineBegin - 1)}{covered} = true;
}
# 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] + 0;
$iBranch = $stryData[1] + 0;
$iBranchIdx = 0;
$iBranchPart = 0;
}
elsif ($iBranch != $stryData[1])
{
if ($iBranchPart != 1)
{
confess &log(ERROR, "line ${iBranchLine}, branch ${iBranch} does not have at least two parts");
}
$iBranch = $stryData[1] + 0;
$iBranchIdx++;
$iBranchPart = 0;
}
else
{
$iBranchPart++;
}
$rhCoverage->{$strFile}{line}{$strBranchLine}{branch}{$iBranchIdx}{$iBranchPart} =
$stryData[3] eq '-' || $stryData[3] eq '0' ? false : true;
# If the branch is uncovered then the function is uncovered
if (!$rhCoverage->{$strFile}{line}{$strBranchLine}{branch}{$iBranchIdx}{$iBranchPart})
{
coverageGenerateFunction($rhCoverage->{$strFile}, $strBranchLine)->{covered} = false;
}
}
# Check line coverage
if ($strLine =~ /^DA\:/)
{
my @stryData = split("\,", substr($strLine, 3));
if (@stryData < 2)
{
confess &log(ERROR, "'${strLine}' should have two fields");
}
my $strStatementLine = sprintf("%09d", $stryData[0]);
# If the statement is uncovered then the function is uncovered
if ($stryData[1] eq '0')
{
$rhCoverage->{$strFile}{line}{$strStatementLine}{statement} = 0;
coverageGenerateFunction($rhCoverage->{$strFile}, $strStatementLine)->{covered} = false;
}
}
}
}
}
# Report on the entire function if any branches/lines in the function are uncovered
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
my $bFileCovered = true;
# Proceed if there is some coverage data
if (defined($rhCoverage->{$strFile}{line}))
{
my @stryC = split("\n", ${$oStorage->get($strFile)});
my $bInUncoveredFunction = false;
# Iterate every line in the C file
for (my $iLineIdx = 0; $iLineIdx < @stryC; $iLineIdx++)
{
my $iLine = sprintf("%09d", $iLineIdx + 1);
# If not in an uncovered function see if this line is the start of an uncovered function
if (!$bInUncoveredFunction)
{
$bInUncoveredFunction =
defined($rhCoverage->{$strFile}{function}{$iLine}) && !$rhCoverage->{$strFile}{function}{$iLine}{covered};
# If any function is uncovered then the file is uncovered
if ($bInUncoveredFunction)
{
$bFileCovered = false;
}
}
# If not in an uncovered function remove coverage
if (!$bInUncoveredFunction)
{
delete($rhCoverage->{$strFile}{line}{$iLine});
}
# Else in an uncovered function
else
{
# If there is no coverage for this line define it so it will show up on the report
if (!defined($rhCoverage->{$strFile}{line}{$iLine}))
{
$rhCoverage->{$strFile}{line}{$iLine} = undef;
}
# Stop processing at the function end brace. This depends on the file being formated correctly, but worst case
# is we run on a display the entire file rather than just uncovered functions.
if ($stryC[$iLineIdx] =~ '^\}')
{
$bInUncoveredFunction = false;
}
}
}
}
# Remove coverage info when file is fully covered
if ($bFileCovered)
{
delete($rhCoverage->{$strFile}{line});
}
}
# 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'));
my $strFileDisplay = substr($strFile, length($strBasePath) + 1);
# 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 => $strFileDisplay, strRef => '#' . $rhCoverage->{$strFile}{anchor}});
}
# Else just show the file name
else
{
$oRow->addNew(HTML_TD, 'list-table-row-file', {strContent => $strFileDisplay});
}
}
# Report on files that are missing coverage
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
my $strFileDisplay = substr($strFile, length($strBasePath) + 1);
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 => $strFileDisplay});
# 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
my $bBranchCovered = true;
if (defined($rhCoverage->{$strFile}{line}{$strLine}{branch}))
{
foreach my $iBranch (sort(keys(%{$rhCoverage->{$strFile}{line}{$strLine}{branch}})))
{
$strBranch .= '[';
my $bBranchPartFirst = true;
foreach my $iBranchPart (sort(keys(%{$rhCoverage->{$strFile}{line}{$strLine}{branch}{$iBranch}})))
{
if (!$bBranchPartFirst)
{
$strBranch .= ' ';
}
if ($rhCoverage->{$strFile}{line}{$strLine}{branch}{$iBranch}{$iBranchPart})
{
$strBranch .= '+';
}
else
{
$strBranch .= '-';
$bBranchCovered = false;
}
$bBranchPartFirst = false;
}
$strBranch .= ']';
}
}
$oRow->addNew(
HTML_TD, 'report-table-row-branch' . (!$bBranchCovered ? '-uncovered' : ''), {strContent => $strBranch});
# Color code based on coverage
$oRow->addNew(
HTML_TD,
'report-table-row-code' . (defined($rhCoverage->{$strFile}{line}{$strLine}{statement}) ? '-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})))
{
# Skip doc/test modules (this includes modules that start with src/ since src/ is stripped from core modules)
next if $strFileCov =~ /^doc\// || $strFileCov =~ /^test\// || $strFileCov =~ /^src\//;
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" : '') .
"\n" .
" " . ($strModule eq 'zzztotal' ? 'TOTAL' : $strModule) . "\n" .
" " .
coverageDocSummaryGenerateValue($rhModuleData->{function}{hit}, $rhModuleData->{function}{found}) .
"\n" .
" " .
coverageDocSummaryGenerateValue($rhModuleData->{branch}{hit}, $rhModuleData->{branch}{found}) .
"\n" .
" " .
coverageDocSummaryGenerateValue($rhModuleData->{line}{hit}, $rhModuleData->{line}{found}) .
"\n" .
"";
}
# Write coverage report
$oStorage->put($strOutFile, $strSummary);
}
push @EXPORT, qw(coverageDocSummaryGenerate);
1;