mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2024-12-14 10:13:05 +02:00
04b0437976
Coverage of the documentation code is not important enough to report to users. If it were reported it should be in a separate section (along with test code coverage).
1071 lines
37 KiB
Perl
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 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" : '') .
|
|
"<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;
|