1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-12 10:04:14 +02:00
pgbackrest/test/lib/pgBackRestTest/Common/CoverageTest.pm
David Steele 5c1d4bcd0d Automate coverage summary report generation.
This report replaces the lcov report that was generated manually for each release.

The lcov report was overly verbose just to say that we have virtually 100% coverage.
2019-05-15 13:04:56 -04:00

657 lines
22 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 Exporter qw(import);
our @EXPORT = qw();
use File::Basename qw(dirname);
use pgBackRest::Common::Log;
use pgBackRest::Common::String;
use pgBackRest::Version;
use BackRestDoc::Html::DocHtmlBuilder;
use BackRestDoc::Html::DocHtmlElement;
####################################################################################################################################
# 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);
####################################################################################################################################
# Generate a C coverage report
####################################################################################################################################
sub coverageGenerate
{
my $oStorage = 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 = substr($strFileCov, 0, length($strFileCov) - 5) . '.c';
$rhCoverage->{$strFile} = undef;
my $iBranchLine = -1;
my $iBranch = undef;
my $iBranchIdx = -1;
my $iBranchPart = undef;
foreach my $strLine (split("\n", $strCoverage))
{
# Check branch coverage
if ($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("${strCoveragePath}/${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 BackRestDoc::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");
$oHtml->bodyGet()->addNew(HTML_DIV, 'title', {strContent => $strTitle});
# Build the file list table
my $oTable = $oHtml->bodyGet()->addNew(HTML_TABLE, 'list-table');
$oTable->addNew(HTML_DIV, 'list-table-caption');
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'));
$oRow->addNew(HTML_TD, 'list-table-row-file', {strContent => $strFile});
}
# Report on files that are missing coverage
foreach my $strFile (sort(keys(%{$rhCoverage})))
{
if (defined($rhCoverage->{$strFile}{line}))
{
# Build the file report table
$oTable = $oHtml->bodyGet()->addNew(HTML_TABLE, 'report-table');
$oTable->addNew(HTML_DIV, 'report-table-caption', {strContent => "${strFile}"});
$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("${strCoveragePath}/${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;