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 794c577130 Migrate integration tests to C.
The Perl integration tests were migrated as faithfully as possible, but there was some cruft and a few unit tests that it did not make sense to migrate.

Also remove all Perl code made obsolete by this migration.

All unit, performance, and integration tests are now written in C but significant parts of the test harness remain to be migrated.
2024-03-06 11:00:09 +13:00

1071 lines
37 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;
####################################################################################################################################
# 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",
"<center>[ " . ($result == 0 ? "Coverage Complete" : "No Coverage Report") . " ]</center>");
}
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 test modules (this includes modules that start with src/ since src/ is stripped from core modules)
next if $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" : '') .
"<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;