#################################################################################################################################### # 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 .= '
'; $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" : '') . "\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;