#################################################################################################################################### # Generate C Coverage Report #################################################################################################################################### package pgBackRestTest::Common::CoverageTest; #################################################################################################################################### # Perl includes #################################################################################################################################### use strict; use warnings FATAL => qw(all); use Carp qw(confess); use English '-no_match_vars'; use Digest::SHA qw(sha1_hex); use Exporter qw(import); our @EXPORT = qw(); use File::Basename qw(dirname); use pgBackRestDoc::Common::Log; use pgBackRestDoc::Common::String; use pgBackRestDoc::Html::DocHtmlBuilder; use pgBackRestDoc::Html::DocHtmlElement; use pgBackRestDoc::ProjectInfo; use pgBackRestTest::Common::ContainerTest; use pgBackRestTest::Common::DefineTest; use pgBackRestTest::Common::ExecuteTest; use pgBackRestTest::Common::ListTest; #################################################################################################################################### # testRunName # # Create module/test names by upper-casing the first letter and then inserting capitals after each -. #################################################################################################################################### sub testRunName { my $strName = shift; my $bInitCapFirst = shift; $bInitCapFirst = defined($bInitCapFirst) ? $bInitCapFirst : true; my $bFirst = true; my @stryName = split('\-', $strName); $strName = undef; foreach my $strPart (@stryName) { $strName .= ($bFirst && $bInitCapFirst) || !$bFirst ? ucfirst($strPart) : $strPart; $bFirst = false; } return $strName; } #################################################################################################################################### # Generate an lcov configuration file #################################################################################################################################### sub coverageLCovConfigGenerate { my $oStorage = shift; my $strOutFile = shift; my $bContainer = shift; my $bCoverageSummary = shift; my $strBranchFilter = 'OBJECT_DEFINE_[A-Z0-9_]+\(|\s{4}[A-Z][A-Z0-9_]+\([^\?]*\)|\s{4}(ASSERT|CHECK|CHECK_FMT|assert|switch\s)\(|\{\+{0,1}' . ($bCoverageSummary ? 'uncoverable_branch' : 'uncover(ed|able)_branch'); my $strLineFilter = '\{\+{0,1}' . ($bCoverageSummary ? 'uncoverable' : '(uncover(ed|able)' . ($bContainer ? '' : '|vm_covered') . ')') . '[^_]'; my $strConfig = "# LCOV Settings\n" . "\n" . "# Specify if branch coverage data should be collected and processed\n" . "lcov_branch_coverage=1\n" . "\n" . "# Specify the regular expression of lines to exclude from branch coverage\n" . "#\n" . '# OBJECT_DEFINE_[A-Z0-9_]+\( - exclude object definitions' . "\n" . '# \s{4}[A-Z][A-Z0-9_]+\([^\?]*\) - exclude macros that do not take a conditional parameter and are not themselves a parameter' . "\n" . '# ASSERT/(|CHECK/(|assert\( - exclude asserts/checks since it usually not possible to trigger both branches' . "\n" . '# switch \( - lcov requires default: to show complete coverage but --Wswitch-enum enforces all enum values be present' . "\n" . "lcov_excl_br_line=${strBranchFilter}\n" . "\n" . "# Specify the regular expression of lines to exclude\n" . "lcov_excl_line=${strLineFilter}\n" . "\n" . "# Coverage rate limits\n" . "genhtml_hi_limit = 100\n" . "genhtml_med_limit = 90\n" . "\n" . "# Width of line coverage field in source code view\n" . "genhtml_line_field_width = 9\n"; # Write configuration file $oStorage->put($strOutFile, $strConfig); } push @EXPORT, qw(coverageLCovConfigGenerate); #################################################################################################################################### # Extract coverage using gcov #################################################################################################################################### sub coverageExtract { my $oStorage = shift; my $strModule = shift; my $strTest = shift; my $bContainer = shift; my $bSummary = shift; my $strContainerImage = shift; my $strWorkPath = shift; my $strWorkTmpPath = shift; my $strWorkUnitPath = shift; my $strTestResultCoveragePath = shift . '/coverage'; # Coverage summary must be run in a container if ($bSummary && !$bContainer) { confess &log(ERROR, "coverage summary must be run on containers for full coverage"); } # Generate a list of files to cover my $hTestCoverage = (testDefModuleTest($strModule, $strTest))->{&TESTDEF_COVERAGE}; my @stryCoveredModule; foreach my $strModule (sort(keys(%{$hTestCoverage}))) { push (@stryCoveredModule, $strModule); } push(@stryCoveredModule, "module/${strModule}/" . testRunName($strTest, false) . 'Test'); # Generate coverage reports for the modules my $strLCovConf = "${strTestResultCoveragePath}/raw/lcov.conf"; coverageLCovConfigGenerate($oStorage, $strLCovConf, $bContainer, $bSummary); my $strLCovExe = "lcov --config-file=${strLCovConf}"; my $strLCovOut = "${strWorkUnitPath}/test.lcov"; my $strLCovOutTmp = "${strWorkUnitPath}/test.tmp.lcov"; executeTest( (defined($strContainerImage) ? 'docker exec -i -u ' . TEST_USER . " ${strContainerImage} " : '') . "${strLCovExe} --capture --directory=${strWorkUnitPath} --o=${strLCovOut} 2>&1"); # Generate coverage report for each module foreach my $strCoveredModule (@stryCoveredModule) { my $strModuleName = testRunName($strCoveredModule, false); if ($strModuleName =~ /^test/mg) { $strModuleName =~ s/^test/src/mg; } elsif ($strModuleName =~ /^doc/mg) { $strModuleName =~ s/^doc/doc\/src/mg; } my $strModuleOutName = $strModuleName; my $bTest = false; if ($strModuleOutName =~ /^module/mg) { $strModuleOutName =~ s/^module/test/mg; $bTest = true; } # Generate lcov reports my $strModulePath = "${strWorkPath}/repo/"; if (${strModuleOutName} =~ /^src\//) { $strModulePath .= 'test/src/' . substr(${strModuleOutName}, 4); } elsif (${strModuleOutName} =~ /^test\//) { $strModulePath .= 'test/src/module/' . substr(${strModuleOutName}, 5); } elsif (${strModuleOutName} =~ /^doc\//) { $strModulePath .= "${strModuleOutName}"; } else { $strModulePath .= "src/${strModuleOutName}"; } my $strLCovFile = "${strTestResultCoveragePath}/raw/${strModuleOutName}.lcov"; my $strLCovTotal = "${strWorkTmpPath}/all.lcov"; my $bInc = $strModuleName =~ '\.vendor$' || $strModuleName =~ '\.auto$'; my $strModuleSourceFile = $strModulePath . '.c' . ($bInc ? '.inc' : ''); executeTest( "${strLCovExe}" . ($bTest ? ' --rc lcov_branch_coverage=0' : '') . " --extract=${strLCovOut} *${strModuleName}.c" . ($bInc ? '.inc' : '') . " --o=${strLCovOutTmp}"); # Combine with prior run if there was one if ($oStorage->exists($strLCovFile)) { my $strCoverage = ${$oStorage->get($strLCovOutTmp)}; $strCoverage =~ s/^SF\:.*$/SF:$strModuleSourceFile/mg; $oStorage->put($strLCovOutTmp, $strCoverage); executeTest("${strLCovExe} --add-tracefile=${strLCovOutTmp} --add-tracefile=${strLCovFile} --o=${strLCovOutTmp}"); } # Update source file my $strCoverage = ${$oStorage->get($strLCovOutTmp)}; if (defined($strCoverage)) { if (!$bTest && $hTestCoverage->{$strCoveredModule} eq TESTDEF_COVERAGE_NOCODE) { confess &log(ERROR, "module '${strCoveredModule}' is marked 'no code' but has code"); } # Get coverage info my $iTotalLines = (split(':', ($strCoverage =~ m/^LF:.*$/mg)[0]))[1] + 0; my $iCoveredLines = (split(':', ($strCoverage =~ m/^LH:.*$/mg)[0]))[1] + 0; my $iTotalBranches = 0; my $iCoveredBranches = 0; if ($strCoverage =~ /^BRF\:/mg && $strCoverage =~ /^BRH\:/mg) { # If this isn't here the statements below fail -- huh? my @match = $strCoverage =~ m/^BRF\:.*$/mg; $iTotalBranches = (split(':', ($strCoverage =~ m/^BRF:.*$/mg)[0]))[1] + 0; $iCoveredBranches = (split(':', ($strCoverage =~ m/^BRH:.*$/mg)[0]))[1] + 0; } # Report coverage if this is not a test or if the test does not have complete coverage if (!$bTest || $iTotalLines != $iCoveredLines || $iTotalBranches != $iCoveredBranches) { # Fix source file name $strCoverage =~ s/^SF\:.*$/SF:$strModuleSourceFile/mg; $oStorage->put($oStorage->openWrite($strLCovFile, {bPathCreate => true}), $strCoverage); if ($oStorage->exists($strLCovTotal)) { executeTest("${strLCovExe} --add-tracefile=${strLCovFile} --add-tracefile=${strLCovTotal} --o=${strLCovTotal}"); } else { $oStorage->copy($strLCovFile, $strLCovTotal) } } else { $oStorage->remove($strLCovFile); } } else { if ($hTestCoverage->{$strCoveredModule} ne TESTDEF_COVERAGE_NOCODE) { confess &log(ERROR, "module '${strCoveredModule}' is marked 'code' but has no code"); } } } } push @EXPORT, qw(coverageExtract); #################################################################################################################################### # Validate converage and generate reports #################################################################################################################################### sub coverageValidateAndGenerate { my $oyTestRun = shift; my $oStorage = shift; my $bCoverageReport = shift; my $bCoverageSummary = shift; my $strWorkPath = shift; my $strWorkTmpPath = shift; my $strTestResultCoveragePath = shift . '/coverage'; my $strTestResultSummaryPath = shift; my $result = 0; # Determine which modules were covered (only check coverage if all tests were successful) #----------------------------------------------------------------------------------------------------------------------- my $hModuleTest; # Everything that was run # Build a hash of all modules, tests, and runs that were executed foreach my $hTestRun (@{$oyTestRun}) { # Get coverage for the module my $strModule = $hTestRun->{&TEST_MODULE}; my $hModule = testDefModule($strModule); # Get coverage for the test my $strTest = $hTestRun->{&TEST_NAME}; my $hTest = testDefModuleTest($strModule, $strTest); # If no tests are listed it means all of them were run if (@{$hTestRun->{&TEST_RUN}} == 0) { $hModuleTest->{$strModule}{$strTest} = true; } } # Now compare against code modules that should have full coverage my $hCoverageList = testDefCoverageList(); my $hCoverageType = testDefCoverageType(); my $hCoverageActual; foreach my $strCodeModule (sort(keys(%{$hCoverageList}))) { if (@{$hCoverageList->{$strCodeModule}} > 0) { my $iCoverageTotal = 0; foreach my $hTest (@{$hCoverageList->{$strCodeModule}}) { if (!defined($hModuleTest->{$hTest->{strModule}}{$hTest->{strTest}})) { next; } $iCoverageTotal++; } if (@{$hCoverageList->{$strCodeModule}} == $iCoverageTotal) { $hCoverageActual->{testRunName($strCodeModule, false)} = $hCoverageType->{$strCodeModule}; } } } if (keys(%{$hCoverageActual}) == 0) { &log(INFO, 'no code modules had all tests run required for coverage'); } # Generate C coverage report #--------------------------------------------------------------------------------------------------------------------------- my $strLCovFile = "${strWorkTmpPath}/all.lcov"; if ($oStorage->exists($strLCovFile)) { foreach my $strCodeModule (sort(keys(%{$hCoverageActual}))) { my $strCoverageFile = $strCodeModule; $strCoverageFile =~ s/^test/src/mg; $strCoverageFile =~ s/^doc/doc\/src/mg; $strCoverageFile =~ s/^module/test/mg; $strCoverageFile = "${strTestResultCoveragePath}/raw/${strCoverageFile}.lcov"; my $strCoverage = $oStorage->get($oStorage->openRead($strCoverageFile, {bIgnoreMissing => true})); if (defined($strCoverage) && defined($$strCoverage)) { my $iTotalLines = (split(':', ($$strCoverage =~ m/^LF\:.*$/mg)[0]))[1] + 0; my $iCoveredLines = (split(':', ($$strCoverage =~ m/^LH\:.*$/mg)[0]))[1] + 0; my $iTotalBranches = 0; my $iCoveredBranches = 0; if ($$strCoverage =~ /^BRF\:/mg && $$strCoverage =~ /^BRH\:/mg) { # If this isn't here the statements below fail -- huh? my @match = $$strCoverage =~ m/^BRF\:.*$/mg; $iTotalBranches = (split(':', ($$strCoverage =~ m/^BRF\:.*$/mg)[0]))[1] + 0; $iCoveredBranches = (split(':', ($$strCoverage =~ m/^BRH\:.*$/mg)[0]))[1] + 0; } # Generate detail if there is missing coverage my $strDetail = undef; if ($iCoveredLines != $iTotalLines) { $strDetail .= "$iCoveredLines/$iTotalLines lines"; } if ($iTotalBranches != $iCoveredBranches) { $strDetail .= (defined($strDetail) ? ', ' : '') . "$iCoveredBranches/$iTotalBranches branches"; } if (defined($strDetail)) { &log(ERROR, "c module ${strCodeModule} is not fully covered ($strDetail)"); $result++; } } } if ($result == 0) { &log(INFO, "tested modules have full coverage"); } # Always generate unified coverage report if there was missing coverage. This is useful for CI. if ($bCoverageReport || $result != 0) { &log(INFO, 'writing C coverage report'); if ($bCoverageReport) { executeTest( "genhtml ${strLCovFile} --config-file=${strTestResultCoveragePath}/raw/lcov.conf" . " --prefix=${strWorkPath}/repo" . " --output-directory=${strTestResultCoveragePath}/lcov"); } coverageGenerate( $oStorage, "${strWorkPath}/repo", "${strTestResultCoveragePath}/raw", "${strTestResultCoveragePath}/coverage.html", $bCoverageReport); } # Else output report status in the HTML else { $oStorage->put( "${strTestResultCoveragePath}/coverage.html", "
[ " . ($result == 0 ? "Coverage Complete" : "No Coverage Report") . " ]
"); } if ($bCoverageSummary) { &log(INFO, 'writing C coverage summary report'); coverageDocSummaryGenerate( $oStorage, "${strTestResultCoveragePath}/raw", "${strTestResultSummaryPath}/metric-coverage-report.auto.xml"); } } return $result; } push @EXPORT, qw(coverageValidateAndGenerate); #################################################################################################################################### # Generate a C coverage report #################################################################################################################################### # Helper to get the function for the current line sub coverageGenerateFunction { my $rhFile = shift; my $iCurrentLine = shift; my $rhFunction; foreach my $iFunctionLine (sort(keys(%{$rhFile->{function}}))) { last if $iCurrentLine < $iFunctionLine; $rhFunction = $rhFile->{function}{$iFunctionLine}; } if (!defined($rhFunction)) { confess &log(ERROR, "function not found at line ${iCurrentLine}"); } return $rhFunction; } sub coverageGenerate { my $oStorage = shift; my $strBasePath = shift; my $strCoveragePath = shift; my $strOutFile = shift; my $bCoverageReport = shift; # Track missing coverage my $rhCoverage = {}; # Find all lcov files in the coverage path my $rhManifest = $oStorage->manifest($strCoveragePath); foreach my $strFileCov (sort(keys(%{$rhManifest}))) { # If a coverage report was not requested then skip coverage of test modules. If we are here it means there was missing # coverage on CI and we want to keep the report as small as possible. next if !$bCoverageReport && $strFileCov =~ /Test\.lcov$/; if ($strFileCov =~ /\.lcov$/) { my $strCoverage = ${$oStorage->get("${strCoveragePath}/${strFileCov}")}; # Show that the file is part of the coverage report even if there is no missing coverage my $strFile; my $iBranchLine = -1; my $iBranch = undef; my $iBranchIdx = -1; my $iBranchPart = undef; foreach my $strLine (split("\n", $strCoverage)) { # Get source file name if ($strLine =~ /^SF\:/) { $strFile = substr($strLine, 3); $rhCoverage->{$strFile} = undef; # Generate a random anchor so new reports will not show links as already followed. This is also an easy way to # create valid, disambiguos links. $rhCoverage->{$strFile}{anchor} = sha1_hex(rand(16)); } # Mark functions as initially covered if ($strLine =~ /^FN\:/) { my ($strLineBegin) = split("\,", substr($strLine, 3)); $rhCoverage->{$strFile}{function}{sprintf("%09d", $strLineBegin - 1)}{covered} = true; } # Check branch coverage elsif ($strLine =~ /^BRDA\:/) { my @stryData = split("\,", substr($strLine, 5)); if (@stryData < 4) { confess &log(ERROR, "'${strLine}' should have four fields"); } my $strBranchLine = sprintf("%09d", $stryData[0]); if ($iBranchLine != $stryData[0]) { $iBranchLine = $stryData[0] + 0; $iBranch = $stryData[1] + 0; $iBranchIdx = 0; $iBranchPart = 0; } elsif ($iBranch != $stryData[1]) { if ($iBranchPart != 1) { confess &log(ERROR, "line ${iBranchLine}, branch ${iBranch} does not have at least two parts"); } $iBranch = $stryData[1] + 0; $iBranchIdx++; $iBranchPart = 0; } else { $iBranchPart++; } $rhCoverage->{$strFile}{line}{$strBranchLine}{branch}{$iBranchIdx}{$iBranchPart} = $stryData[3] eq '-' || $stryData[3] eq '0' ? false : true; # If the branch is uncovered then the function is uncovered if (!$rhCoverage->{$strFile}{line}{$strBranchLine}{branch}{$iBranchIdx}{$iBranchPart}) { coverageGenerateFunction($rhCoverage->{$strFile}, $strBranchLine)->{covered} = false; } } # Check line coverage if ($strLine =~ /^DA\:/) { my @stryData = split("\,", substr($strLine, 3)); if (@stryData < 2) { confess &log(ERROR, "'${strLine}' should have two fields"); } my $strStatementLine = sprintf("%09d", $stryData[0]); # If the statement is uncovered then the function is uncovered if ($stryData[1] eq '0') { $rhCoverage->{$strFile}{line}{$strStatementLine}{statement} = 0; coverageGenerateFunction($rhCoverage->{$strFile}, $strStatementLine)->{covered} = false; } } } } } # Report on the entire function if any branches/lines in the function are uncovered foreach my $strFile (sort(keys(%{$rhCoverage}))) { my $bFileCovered = true; # Proceed if there is some coverage data if (defined($rhCoverage->{$strFile}{line})) { my @stryC = split("\n", ${$oStorage->get($strFile)}); my $bInUncoveredFunction = false; # Iterate every line in the C file for (my $iLineIdx = 0; $iLineIdx < @stryC; $iLineIdx++) { my $iLine = sprintf("%09d", $iLineIdx + 1); # If not in an uncovered function see if this line is the start of an uncovered function if (!$bInUncoveredFunction) { $bInUncoveredFunction = defined($rhCoverage->{$strFile}{function}{$iLine}) && !$rhCoverage->{$strFile}{function}{$iLine}{covered}; # If any function is uncovered then the file is uncovered if ($bInUncoveredFunction) { $bFileCovered = false; } } # If not in an uncovered function remove coverage if (!$bInUncoveredFunction) { delete($rhCoverage->{$strFile}{line}{$iLine}); } # Else in an uncovered function else { # If there is no coverage for this line define it so it will show up on the report if (!defined($rhCoverage->{$strFile}{line}{$iLine})) { $rhCoverage->{$strFile}{line}{$iLine} = undef; } # Stop processing at the function end brace. This depends on the file being formated correctly, but worst case # is we run on a display the entire file rather than just uncovered functions. if ($stryC[$iLineIdx] =~ '^\}') { $bInUncoveredFunction = false; } } } } # Remove coverage info when file is fully covered if ($bFileCovered) { delete($rhCoverage->{$strFile}{line}); } } # Build html my $strTitle = PROJECT_NAME . ' Coverage Report'; my $strDarkRed = '#580000'; my $strGray = '#555555'; my $strDarkGray = '#333333'; my $oHtml = new pgBackRestDoc::Html::DocHtmlBuilder( PROJECT_NAME, $strTitle, undef, undef, undef, true, true, "html\n" . "{\n" . " background-color: ${strGray};\n" . " font-family: Avenir, Corbel, sans-serif;\n" . " color: white;\n" . " font-size: 12pt;\n" . " margin-top: 8px;\n" . " margin-left: 1\%;\n" . " margin-right: 1\%;\n" . " width: 98\%;\n" . "}\n" . "\n" . "body\n" . "{\n" . " margin: 0px auto;\n" . " padding: 0px;\n" . " width: 100\%;\n" . " text-align: justify;\n" . "}\n" . ".title\n" . "{\n" . " width: 100\%;\n" . " text-align: center;\n" . " font-size: 200\%;\n" . "}\n" . "\n" . ".list-table\n" . "{\n" . " width: 100\%;\n" . "}\n" . "\n" . ".list-table-caption\n" . "{\n" . " margin-top: 1em;\n" . " font-size: 130\%;\n" . " margin-bottom: .25em;\n" . "}\n" . "\n" . ".list-table-caption::after\n" . "{\n" . " content: \"Modules Tested for Coverage:\";\n" . "}\n" . "\n" . ".list-table-header-file\n" . "{\n" . " padding-left: .5em;\n" . " padding-right: .5em;\n" . " background-color: ${strDarkGray};\n" . " width: 100\%;\n" . "}\n" . "\n" . ".list-table-row-uncovered\n" . "{\n" . " background-color: ${strDarkRed};\n" . " color: white;\n" . " width: 100\%;\n" . "}\n" . "\n" . ".list-table-row-file\n" . "{\n" . " padding-left: .5em;\n" . " padding-right: .5em;\n" . "}\n" . "\n" . ".report-table\n" . "{\n" . " width: 100\%;\n" . "}\n" . "\n" . ".report-table-caption\n" . "{\n" . " margin-top: 1em;\n" . " font-size: 130\%;\n" . " margin-bottom: .25em;\n" . "}\n" . "\n" . ".report-table-caption::after\n" . "{\n" . " content: \" report:\";\n" . "}\n" . "\n" . ".report-table-header\n" . "{\n" . "}\n" . "\n" . ".report-table-header-line, .report-table-header-branch, .report-table-header-code\n" . "{\n" . " padding-left: .5em;\n" . " padding-right: .5em;\n" . " background-color: ${strDarkGray};\n" . "}\n" . "\n" . ".report-table-header-code\n" . "{\n" . " width: 100\%;\n" . "}\n" . "\n" . ".report-table-row-dot-tr, .report-table-row\n" . "{\n" . " font-family: \"Courier New\", Courier, monospace;\n" . "}\n" . "\n" . ".report-table-row-dot-skip\n" . "{\n" . " height: 1em;\n" . " padding-top: .25em;\n" . " padding-bottom: .25em;\n" . " text-align: center;\n" . "}\n" . "\n" . ".report-table-row-line, .report-table-row-branch, .report-table-row-branch-uncovered," . " .report-table-row-code, .report-table-row-code-uncovered\n" . "{\n" . " padding-left: .5em;\n" . " padding-right: .5em;\n" . "}\n" . "\n" . ".report-table-row-line\n" . "{\n" . " text-align: right;\n" . "}\n" . "\n" . ".report-table-row-branch, .report-table-row-branch-uncovered\n" . "{\n" . " text-align: right;\n" . " white-space: nowrap;\n" . "}\n" . "\n" . ".report-table-row-branch-uncovered\n" . "{\n" . " background-color: ${strDarkRed};\n" . " color: white;\n" . "}\n" . "\n" . ".report-table-row-code, .report-table-row-code-uncovered\n" . "{\n" . " white-space: pre;\n" . "}\n" . "\n" . ".report-table-row-code-uncovered\n" . "{\n" . " background-color: ${strDarkRed};\n" . " color: white;\n" . "}\n"); # File list title $oHtml->bodyGet()->addNew(HTML_DIV, 'title', {strContent => $strTitle}); # Build the file list table $oHtml->bodyGet()->addNew(HTML_DIV, 'list-table-caption'); my $oTable = $oHtml->bodyGet()->addNew(HTML_TABLE, 'list-table'); my $oHeader = $oTable->addNew(HTML_TR, 'list-table-header'); $oHeader->addNew(HTML_TH, 'list-table-header-file', {strContent => 'FILE'}); foreach my $strFile (sort(keys(%{$rhCoverage}))) { my $oRow = $oTable->addNew(HTML_TR, 'list-table-row-' . (defined($rhCoverage->{$strFile}{line}) ? 'uncovered' : 'covered')); my $strFileDisplay = substr($strFile, length($strBasePath) + 1); # Link only created when file is uncovered if (defined($rhCoverage->{$strFile}{line})) { $oRow->addNew(HTML_TD, 'list-table-row-file')->addNew( HTML_A, undef, {strContent => $strFileDisplay, strRef => '#' . $rhCoverage->{$strFile}{anchor}}); } # Else just show the file name else { $oRow->addNew(HTML_TD, 'list-table-row-file', {strContent => $strFileDisplay}); } } # Report on files that are missing coverage foreach my $strFile (sort(keys(%{$rhCoverage}))) { my $strFileDisplay = substr($strFile, length($strBasePath) + 1); if (defined($rhCoverage->{$strFile}{line})) { # Anchor only created when file is uncovered $oHtml->bodyGet()->addNew(HTML_A, undef, {strId => $rhCoverage->{$strFile}{anchor}}); # Report table caption, i.e. the uncovered file name $oHtml->bodyGet()->addNew(HTML_DIV, 'report-table-caption', {strContent => $strFileDisplay}); # Build the file report table $oTable = $oHtml->bodyGet()->addNew(HTML_TABLE, 'report-table'); $oHeader = $oTable->addNew(HTML_TR, 'report-table-header'); $oHeader->addNew(HTML_TH, 'report-table-header-line', {strContent => 'LINE'}); $oHeader->addNew(HTML_TH, 'report-table-header-branch', {strContent => 'BRANCH'}); $oHeader->addNew(HTML_TH, 'report-table-header-code', {strContent => 'CODE'}); my $strC = ${$oStorage->get($strFile)}; my @stryC = split("\n", $strC); my $iLastLine = undef; foreach my $strLine (sort(keys(%{$rhCoverage->{$strFile}{line}}))) { if (defined($iLastLine) && $strLine != $iLastLine + 1) { my $oRow = $oTable->addNew(HTML_TR, 'report-table-row-dot'); $oRow->addNew(HTML_TD, 'report-table-row-dot-skip', {strExtra => 'colspan="3"'}); } $iLastLine = $strLine; my $iLine = int($strLine); my $oRow = $oTable->addNew(HTML_TR, 'report-table-row'); $oRow->addNew(HTML_TD, 'report-table-row-line', {strContent => $iLine}); my $strBranch; # Show missing branch coverage my $bBranchCovered = true; if (defined($rhCoverage->{$strFile}{line}{$strLine}{branch})) { foreach my $iBranch (sort(keys(%{$rhCoverage->{$strFile}{line}{$strLine}{branch}}))) { $strBranch .= '['; my $bBranchPartFirst = true; foreach my $iBranchPart (sort(keys(%{$rhCoverage->{$strFile}{line}{$strLine}{branch}{$iBranch}}))) { if (!$bBranchPartFirst) { $strBranch .= ' '; } if ($rhCoverage->{$strFile}{line}{$strLine}{branch}{$iBranch}{$iBranchPart}) { $strBranch .= '+'; } else { $strBranch .= '-'; $bBranchCovered = false; } $bBranchPartFirst = false; } $strBranch .= ']'; } } $oRow->addNew( HTML_TD, 'report-table-row-branch' . (!$bBranchCovered ? '-uncovered' : ''), {strContent => $strBranch}); # Color code based on coverage $oRow->addNew( HTML_TD, 'report-table-row-code' . (defined($rhCoverage->{$strFile}{line}{$strLine}{statement}) ? '-uncovered' : ''), {bPre => true, strContent => $stryC[$strLine - 1]}); } } } # Write coverage report $oStorage->put($strOutFile, $oHtml->htmlGet()); } push @EXPORT, qw(coverageGenerate); #################################################################################################################################### # Generate a C coverage summary for the documentation #################################################################################################################################### sub coverageDocSummaryGenerateValue { my $iHit = shift; my $iFound = shift; if (!defined($iFound) || !defined($iHit) || $iFound == 0) { return "---"; } my $fPercent = $iHit * 100 / $iFound; my $strPercent; if ($fPercent == 100) { $strPercent = '100.0'; } elsif ($fPercent > 99.99) { $strPercent = '99.99'; } else { $strPercent = sprintf("%.2f", $fPercent); } return "${iHit}/${iFound} (${strPercent}%)"; } sub coverageDocSummaryGenerate { my $oStorage = shift; my $strCoveragePath = shift; my $strOutFile = shift; # Track coverage summary my $rhSummary; # Find all lcov files in the coverage path my $rhManifest = $oStorage->manifest($strCoveragePath); foreach my $strFileCov (sort(keys(%{$rhManifest}))) { # Skip doc/test modules (this includes modules that start with src/ since src/ is stripped from core modules) next if $strFileCov =~ /^doc\// || $strFileCov =~ /^test\// || $strFileCov =~ /^src\//; if ($strFileCov =~ /\.lcov$/) { my $strCoverage = ${$oStorage->get("${strCoveragePath}/${strFileCov}")}; my $strModule = dirname($strFileCov); foreach my $strLine (split("\n", $strCoverage)) { # Get Line Coverage if ($strLine =~ /^LF\:/) { $rhSummary->{$strModule}{line}{found} += substr($strLine, 3) + 0; $rhSummary->{zzztotal}{line}{found} += substr($strLine, 3) + 0; } if ($strLine =~ /^LH\:/) { $rhSummary->{$strModule}{line}{hit} += substr($strLine, 3) + 0; $rhSummary->{zzztotal}{line}{hit} += substr($strLine, 3) + 0; } # Get Function Coverage if ($strLine =~ /^FNF\:/) { $rhSummary->{$strModule}{function}{found} += substr($strLine, 4) + 0; $rhSummary->{zzztotal}{function}{found} += substr($strLine, 4) + 0; } if ($strLine =~ /^FNH\:/) { $rhSummary->{$strModule}{function}{hit} += substr($strLine, 4) + 0; $rhSummary->{zzztotal}{function}{hit} += substr($strLine, 4) + 0; } # Get Branch Coverage if ($strLine =~ /^BRF\:/) { $rhSummary->{$strModule}{branch}{found} += substr($strLine, 4) + 0; $rhSummary->{zzztotal}{branch}{found} += substr($strLine, 4) + 0; } if ($strLine =~ /^BRH\:/) { $rhSummary->{$strModule}{branch}{hit} += substr($strLine, 4) + 0; $rhSummary->{zzztotal}{branch}{hit} += substr($strLine, 4) + 0; } } } } # use Data::Dumper;confess Dumper($rhSummary); my $strSummary; foreach my $strModule (sort(keys(%{$rhSummary}))) { my $rhModuleData = $rhSummary->{$strModule}; $strSummary .= (defined($strSummary) ? "\n\n" : '') . "\n" . " " . ($strModule eq 'zzztotal' ? 'TOTAL' : $strModule) . "\n" . " " . coverageDocSummaryGenerateValue($rhModuleData->{function}{hit}, $rhModuleData->{function}{found}) . "\n" . " " . coverageDocSummaryGenerateValue($rhModuleData->{branch}{hit}, $rhModuleData->{branch}{found}) . "\n" . " " . coverageDocSummaryGenerateValue($rhModuleData->{line}{hit}, $rhModuleData->{line}{found}) . "\n" . ""; } # Write coverage report $oStorage->put($strOutFile, $strSummary); } push @EXPORT, qw(coverageDocSummaryGenerate); 1;