#!/usr/bin/env perl # # Copyright (c) International Business Machines Corp., 2002,2012 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or (at # your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see # . # # # genhtml # # This script generates HTML output from .info files as created by the # geninfo script. Call it with --help and refer to the genhtml man page # to get information on usage and available options. # # # History: # 2002-08-23 created by Peter Oberparleiter # IBM Lab Boeblingen # based on code by Manoj Iyer and # Megan Bock # IBM Austin # 2002-08-27 / Peter Oberparleiter: implemented frame view # 2002-08-29 / Peter Oberparleiter: implemented test description filtering # so that by default only descriptions for test cases which # actually hit some source lines are kept # 2002-09-05 / Peter Oberparleiter: implemented --no-sourceview # 2002-09-05 / Mike Kobler: One of my source file paths includes a "+" in # the directory name. I found that genhtml.pl died when it # encountered it. I was able to fix the problem by modifying # the string with the escape character before parsing it. # 2002-10-26 / Peter Oberparleiter: implemented --num-spaces # 2003-04-07 / Peter Oberparleiter: fixed bug which resulted in an error # when trying to combine .info files containing data without # a test name # 2003-04-10 / Peter Oberparleiter: extended fix by Mike to also cover # other special characters # 2003-04-30 / Peter Oberparleiter: made info write to STDERR, not STDOUT # 2003-07-10 / Peter Oberparleiter: added line checksum support # 2004-08-09 / Peter Oberparleiter: added configuration file support # 2005-03-04 / Cal Pierog: added legend to HTML output, fixed coloring of # "good coverage" background # 2006-03-18 / Marcus Boerger: added --custom-intro, --custom-outro and # overwrite --no-prefix if --prefix is present # 2006-03-20 / Peter Oberparleiter: changes to custom_* function (rename # to html_prolog/_epilog, minor modifications to implementation), # changed prefix/noprefix handling to be consistent with current # logic # 2006-03-20 / Peter Oberparleiter: added --html-extension option # 2008-07-14 / Tom Zoerner: added --function-coverage command line option; # added function table to source file page # 2008-08-13 / Peter Oberparleiter: modified function coverage # implementation (now enabled per default), # introduced sorting option (enabled per default) # April/May 2020 / Henry Cox/Steven Dovich - Mediatek, inc # Add support for differential line coverage categorization, # date- and owner- binning. # June/July 2020 / Henry Cox - Mediatek, inc # Add support for differential branch coverage categorization, # Add a bunch of navigation features - href to next code block # of type T, of type T in date- or owner bin B, etc. # Add sorted tables for date/owner bin summaries. # October 2020 / Henry Cox - Mediatek, inc # Add "--hierarchical" display option. # use strict; use warnings; use File::Basename; use File::Copy; use File::Find; use File::Path; use File::Spec; use File::Temp; use Scalar::Util qw/looks_like_number/; use Digest::MD5 qw(md5_base64); use Cwd qw/abs_path realpath cwd/; use DateTime; #use Regexp::Common qw(time); # damn - not installed use Date::Parse; use FileHandle; use Carp; use Storable; use FindBin; use Time::HiRes; # for profiling use POSIX; use Data::Dumper; # Windows path issues - see comment in jacoco2lcov use lib "$FindBin::RealBin/../lib", do { (my $d = $0) =~ s{\\}{/}g; $d =~ s{[^/]*$}{}; $d . '../lib' }; use lcovutil qw (set_tool_name define_errors parse_ignore_errors $tool_name $tool_dir $lcov_version $lcov_url ignorable_error $ERROR_MISMATCH $ERROR_SOURCE $ERROR_BRANCH $ERROR_FORMAT $ERROR_EMPTY $ERROR_VERSION $ERROR_UNUSED $ERROR_PACKAGE $ERROR_CORRUPT $ERROR_NEGATIVE $ERROR_COUNT $ERROR_UNSUPPORTED $ERROR_DEPRECATED $ERROR_INCONSISTENT_DATA $ERROR_CALLBACK $ERROR_RANGE $ERROR_PATH $ERROR_PARALLEL $ERROR_CHILD report_parallel_error report_exit_status summarize_messages $br_coverage $func_coverage info $verbose init_verbose_flag debug $debug $devnull parseOptions strip_directories parse_cov_filters summarize_cov_filters $FILTER_BRANCH_NO_COND $FILTER_LINE_CLOSE_BRACE @cov_filter rate get_overall_line $default_precision check_precision die_handler warn_handler parse_w3cdtf); # Global constants our $title = "LCOV - differential code coverage report"; # the name we were invoked with - which might not be the one worked out by # lcovutil: that one is resolved, so a link is reported as the tool it points # at. 'posix_path' makes basename() work on the Windows $0 so # git-bash, MSYS or Cygwin perl can be handled - see lcovutil.pm. lcovutil::set_tool_name(basename(lcovutil::posix_path($0))); our $debugScheduler = 0; # if false, then keep track of only enough information to be able to # produce a valid HTML report. # - Do not keep track of everything - say, to enable serialize/deserialize # of the complete coverage DB. # - In practice: this means to throw away the 'FileDetails' structure after # the source file HTML has been constructed. 'FileDetails' is the lion's # share of the memory footprint. # This has the effect of reducing memory footprint and improving parallel # performance. our $buildSerializableDatabase = 0; # Specify coverage rate limits (in %) for classifying file entries # HI: $hi_limit <= rate <= 100 graph color: green # MED: $med_limit <= rate < $hi_limit graph color: orange # LO: 0 <= rate < $med_limit graph color: red # For line coverage/all coverage types if not specified our $hi_limit = 90; our $med_limit = 75; # For line coverage our $ln_hi_limit; our $ln_med_limit; # For function coverage our $fn_hi_limit; our $fn_med_limit; # For branch coverage our $br_hi_limit; our $br_med_limit; # For MC/DC coverage our $mcdc_hi_limit; our $mcdc_med_limit; # For callgraph (call site) coverage our $callgraph_hi_limit; our $callgraph_med_limit; # Width of overview image our $overview_width = 80; # Resolution of overview navigation: this number specifies the maximum # difference in lines between the position a user selected from the overview # and the position the source code window is scrolled to. our $nav_resolution = 4; # Clicking a line in the overview image should show the source code view at # a position a bit further up so that the requested line is not the first # line in the window. This number specifies that offset in lines. our $nav_offset = 10; # Clicking on a function name should show the source code at a position a # few lines before the first line of code of that function. This number # specifies that offset in lines. our $func_offset = 2; our $overview_title = "top level"; # Width for line coverage information in the source code view our $line_field_width = 12; # Width for branch coverage information in the source code view our $br_field_width = 16; # Width for MC/DC coverage information in the source code view our $mcdc_field_width = 14; # Width for owner name in the source code view our $owner_field_width = 20; # Width for block age in the source code view our $age_field_width = 5; # Width for TLA entry in the source code view our $tla_field_width = 3; # Internal Constants # Header types our $HDR_DIR = 0; our $HDR_FILE = 1; our $HDR_SOURCE = 2; our $HDR_TESTDESC = 3; our $HDR_FUNC = 4; # the two call site pages of a file: one direction of the callgraph each - see # 'write_callgraph_page' our $HDR_CALLERS = 5; our $HDR_CALLEES = 6; # Sort types our $SORT_FILE = 0; our $SORT_LINE = 1; our $SORT_FUNC = 2; our $SORT_BRANCH = 3; our $SORT_MCDC = 4; our $SORT_CALLGRAPH = 5; # ..the file-view sort types are also indices into @fileview_sortname, so a new # one goes at the end of that group rather than beside its metric # function detail sort types # - each of them a proportion, ordered lowest first: see '_rateOrder' our $SORT_MISSING_LINE = 6; # by proportion of hit lines in function our $SORT_MISSING_BRANCH = 7; # by proportion of hit branches in function our $SORT_MISSING_MCDC = 8; # by proportion of hit MC/DC expressions our $SORT_MISSING_CALLGRAPH = 9; # by proportion of hit call sites in function our $SORT_MISSING_CALLERS = 10; # by proportion of hit calls into function # call site page sort types: the name and the page's own 'Rate' are # SORT_FILE and SORT_MISSING_CALLGRAPH our $SORT_XREF = 11; # by the proportion of the other direction our $SORT_OWNER = 12; # by who last changed the function our $SORT_AGE = 13; # by how long ago that was # The two call site pages of a file, in the order the page header names them: # one direction of the callgraph each, indexed by CALLGRAPH_VIEW_ and carrying # the header type of that page, the name it is written under, what its first # column holds, what a column which leads to it is headed and what its own side # of a call is called. One row per page - the two pages differ in their data # and in nothing else, so everything which is per page is here. # See 'write_callgraph_page' use constant { CALLGRAPH_VIEW_CALLERS => 0, CALLGRAPH_VIEW_CALLEES => 1, CALLGRAPH_VIEW_COUNT => 2, }; # 'siteTitle' is what the location cell of a row leads to, said in the terms of # the page the cell is on: on the callers page that location is the caller's # own source line, while on the callees page it is the line of this function # which makes the call - the callee's definition is somewhere else entirely, # and a reader should not be told to expect it our @callgraphView = ( { hdr => $HDR_CALLERS, name => 'callers', title => 'callers', column => 'Caller', heading => ['Called from', 'Call site'], hitAlt => 'unexercised callers of function', siteTitle => 'Click to go to caller source line', }, { hdr => $HDR_CALLEES, name => 'callees', title => 'callees', column => 'Callee', heading => ['Call site', 'Callee'], hitAlt => 'unexercised calls made by function', siteTitle => 'Click to go to callsite source line (in this function)', }); # Call site page: the name of an alias group is - see '_showAliasGroups' our $ALIAS_GROUP_TITLE = 'one alias group of this function: the symbols which make and receive' . ' the same calls. The adjacent rows are the calls of this group'; # ..and what the page says in place of the groups, where they are suppressed our $ALIAS_GROUPS_SUPPRESSED_NOTE = "Alias groups suppressed - see 'suppress_alias_groups' configuration file option"; # ..and a call cell with no calls in it, on a row which contains no call our $CALLGRAPH_EMPTY_CELL = ' ' . "\n"; # Call site page: why the caller rows of a function with several aliases are # captioned. A function which has nothing to say in a direction is not on that # direction's page at all - see 'callgraphViewEntries' - so no row of either # page has to say that it has nothing to say our $ALIAS_CAPTION_TITLE = 'the callers of one alias of this function: a second symbol for the same' . ' definition, which the function table lists under the name in the first' . ' column'; # ..and which side of the callgraph boundary a function's callers are on, # once beside its name rather than left to be counted out of its rows. # Only the answers which involve the boundary are written: # 'every caller is a site in this report' # is both the ordinary case and what the rows themselves show. See # _callgraphCallerNote and lcovutil::classify_callers our $CALLERS_EXTERNAL_NOTE = 'external callers only'; our $CALLERS_EXTERNAL_TITLE = 'no call site in this report names this function: its only callers are' . ' outside the callgraph boundary'; our $CALLERS_BOTH_NOTE = 'internal and external callers'; our $CALLERS_BOTH_TITLE = 'this function is named both by a call site within the callgraph and by' . ' one outside of it'; our $CALLERS_UNKNOWN_NOTE = 'callers may be outside the callgraph boundary'; our $CALLERS_UNKNOWN_TITLE = "an unknown call count ('-') among the call sites which name this function" . ' makes it impossible to tell whether any call came from outside the' . ' callgraph boundary'; # ..and the extra last row each of the two sides can have: an edge which is only # a fact about this function, having no call site of its own, because the other # end of it is outside the boundary of the report - see FunctionEntry # ::has_external_callees and FunctionAlias::has_external_callers. Named in the # angle brackets the sources cannot contain, so that neither can be read as a # symbol # ..and explain that the 'Callgraph:' numbers on the 'callers' detail page # is the number of calls made *from* this file (sources in this file) - # whereas the row counts calls made *into* the file (or directory, or project) # on the other pages ('callee' detail, function call detail, source view, # etc.) our $CALLS_INTO_TITLE = 'Callgraph edge sources in this file (number of callee sites): each call' . ' location in the table below, once per alias of the function it calls' . ' - or, for a function shown as alias groups, once: its unique call' . ' sites. See \'unique call site\' in genhtml(1)'; # ..and on every other page - the project, a directory, or the other pages of a # single file: the source view, the function detail page and the callees # page. '%s' replaced by page name our $CALLS_FROM_TITLE = 'Callgraph edge destinations in this %s (number of caller sites): every' . ' call site record, including several at one location - which the' . ' call site pages count as one unique call site where they call the' . ' same function. See \'unique call site\' in genhtml(1)'; our $EXTERNAL_CALLEE_ROW = '<external callee>'; our $EXTERNAL_CALLEE_TITLE = 'this function calls at least one function which is outside the callgraph' . ' boundary: a function no file of the report defines, or one which an' . ' exclude pattern removed from it. There is no call site' . ' row for it because the callee is not within the callgraph boundary,' . ' and no count because the site which carried it was outside too'; our $EXTERNAL_CALLER_ROW = '<external callers>'; our $EXTERNAL_CALLER_TITLE = 'at least one call site outside the callgraph boundary calls this function:' . ' a site in a file the report does not contain, one which was excluded,' . ' or one in a function which was erased. We don\'t know the caller' . ' name because it is not part of the report. The count outside the' . ' parentheses is the number of calls those sites made'; our $DEDUCED_CALLER_TITLE = 'this function was entered more often than the call sites in this report' . ' account for, so at least one call reached it from outside the callgraph' . ' boundary: an uninstrumented or uncaptured translation unit, a call' . ' through a function pointer, or the runtime itself. The count in' . ' parentheses is the number of calls not accounted for'; our $DEDUCED_CALLER_ROW = '<deduced external callers>'; our $DEDUCED_AND_EXTERNAL_CALLER_ROW = '<external and deduced callers>'; our $EXTERNAL_COUNT_TITLE = 'a call site outside the callgraph boundary called this function, and' . ' how often it called is not recorded'; # Fileview heading types our $HEAD_NO_DETAIL = 1; our $HEAD_DETAIL_HIDDEN = 2; our $HEAD_DETAIL_SHOWN = 3; # Additional offsets used when converting branch coverage data to HTML our $BR_LEN = -3; our $BR_OPEN = -2; # 2nd last element our $BR_CLOSE = -1; # last element # Data related prototypes sub print_usage(*); sub gen_html(); sub html_create($$); sub process_file($$$$$); sub compute_title($$); sub get_prefix($@); sub shorten_prefix($); sub get_relative_base_path($); sub read_testfile($); sub get_date_string($); sub remove_unused_descriptions(); sub get_affecting_tests($$$$); sub apply_prefix($@); sub get_html_prolog($); sub get_html_epilog($); #sub write_dir_page($$$$$$$;$); sub write_summary_pages($$$$$$$$); sub classify_rate($$$$); sub parse_dir_prefix(@); # The symbols some call site of this report names - see TraceFile::calledSymbols, # and the note above %calleeLocation for why it is built in the parent. # Declared here because it is read before the sub which builds it our $callgraphCalledSymbols; # call-site display prototypes sub build_callee_location_index($); sub build_callgraph_foreign_index($); sub categorize_all_call_sites($$$); sub calleeLocation($$); sub callSiteGroups($$;$); sub callgraphSourceGroups($;$); sub callgraphDecorateSource($$$); sub _callgraphGroupHrefAttr($$); sub filePageBase($$); sub pageName($$); sub callgraphPageName($$;$); sub callgraphDetailEntries($$$); sub callgraphViewEntries($$$); sub write_callgraph_page($$$$$$$$$$$); sub isCallgraphHeader($); sub _headerTitle($$); sub callgraphview_get_label($$$$$); sub _calleeFuncAnchor($); sub callgraphShownAnchors($$); sub _pageShows($$$); sub _shownSourceLine($$); sub _callgraphCaptionCell($$); sub _callgraphCellLine($); sub _callgraphForeignPage($$$$); sub _calleeFuncCell($$$$$); sub _aliasMerged($$); sub _mergedNameNote($$); sub _calleeXrefCell($$$$$); sub _callgraphAnnotCells($$$$$); sub _callgraphAnnotation($$); sub _callgraphHitCell($$); sub _rateOrder($$); sub _callgraphPageSuffix($); sub callgraphPageSorts($$); sub callgraphLinkSort($$$); sub _callgraphXrefTitle($); sub _callgraphCallerCells($$$$); sub _callerListCells($$$$$); sub _callSiteCells($$$$$); sub _callSiteGroupCells($$$$$$); sub _externalCalleeRow($); sub _callerRowAnchor($$); sub _showAliasGroups($$); sub _aliasGroupAnchor($); sub _aliasGroupSites($$); sub _aliasGroupMetrics($$$); sub _aliasGroupLeadCells($$$$$$$); sub _aliasGroupsInOrder($$$@); sub _countedCallSites($$); sub _callgraphTotals(@); sub _callgraphGroupHref($$$); sub _callgraphCountCell($$$$); sub _callgraphExternalCountCell($$); sub _callgraphRowStyle($$); sub _callgraphTlaLabel($); sub _nextGroupIndex($$); sub _callgraphHitKey($); sub _callgraphCountKey($); sub _callgraphUnhitTla($); sub _callgraphMergedRuns($$); sub _callgraphRowRuns($); sub _functionSourceHref($$); sub _functionProportionCell($$$$$); sub parseMetricColumnOrder($); sub metricColumnRank($); sub functionTableColumns($); # HTML related prototypes sub escape_html($); sub escape_id($); sub get_bar_graph_code($$$); sub write_png_files(); sub write_htaccess_file(); sub write_css_file(); sub write_description_file($$); sub write_function_table(*$$$$$$$$$$$$); sub write_html(*$); sub write_html_prolog(*$$); sub write_html_epilog(*$;$); sub write_header(*$$$$$$$;$); sub write_header_prolog(*$); sub write_header_line(*@); sub write_header_epilog(*$); sub write_file_table(*$$$$$$); sub write_file_table_prolog(*$$$@); sub write_file_table_entry(*$$@); sub write_file_table_detail_entry(*$$$$@); sub _inColumnOrder($$@); sub write_file_table_epilog(*); sub write_test_table_prolog(*$); sub write_test_table_entry(*$$); sub write_test_table_epilog(*); sub write_source($$$$$$$$$); sub write_source_prolog(*$$$); sub write_source_line(*$$$$$$$); sub write_source_epilog(*); sub write_frameset(*$$$); sub write_overview_line(*$$$); sub write_overview(*$$$$); # External prototype (defined in genpng) sub gen_png($$$$$@); sub simplify_function_name($); package SummaryInfo; our @selectCallbackScript; our $selectCallback; our @cleanDirectoryList; our @tlaPriorityOrder = ("UNC", "LBC", "UIC", "UBC", "GBC", "GIC", "GNC", "CBC", "EUB", "ECB", 'ECC', 'EUC', "DUB", "DCB",); # 'noBaseline' rewrites @tlaPriorityOrder down to the two legacy categories, so # that array answers "in what order should these be presented" and not "what # TLAs can the data still contain". An excluded coverpoint is categorized ECC # or EUC whether there is a baseline or not - the 'U' flag is carried in the # '.info' file - so anything which has to be able to style or name every TLA # iterates this copy instead. our @allTla = @tlaPriorityOrder; # The rank of each category, for the one case which has to reduce two # categorized records to one display row - see 'main::callSiteGroups'. # Derived from @allTla and not from @tlaPriorityOrder, which 'noBaseline' # rewrites down to two entries our %tlaRank; @tlaRank{@allTla} = (0 .. $#allTla); # Membership tables - for asking "is this TLA one of ...", not for iterating. # A hash lookup is exact, and considerably cheaper. our %excludedCoverpointTla = ('ECC' => 1, 'EUC' => 1); our %excludedTla = ('EUB' => 1, 'ECB' => 1, %excludedCoverpointTla); # not in the current version, so no line in the source listing our %deletedTla = ('DUB' => 1, 'DCB' => 1); # neither of those has an entry to keep in a date or owner bin our %unbinnedTla = (%deletedTla, %excludedCoverpointTla); # the TLAs which count as 'hit' our %hitTla = ('GBC' => 1, 'GIC' => 1, 'GNC' => 1, 'CBC' => 1); # The same order, without the non-displaying elements - derived rather than # written out again, so the two lists cannot drift apart. our @tlaOrderNoExclude = grep({ !exists($excludedCoverpointTla{$_}) } @tlaPriorityOrder); our %displayedTla = map({ ($_ => 1) } @tlaOrderNoExclude); # The categories a count cell is colorized for: code which is not covered now # and was not already uncovered in the baseline. Called the "concerning" # categories by the summary table code. our %concerningTla = ('UNC' => 1, 'LBC' => 1, 'UIC' => 1); our %tlaLocation = ("UNC" => 1, "LBC" => 3, "UIC" => 3, "UBC" => 3, "GBC" => 3, "GIC" => 3, "GNC" => 1, "CBC" => 3, "EUB" => 3, "ECB" => 3, 'ECC' => 1, 'EUC' => 1, "DUB" => 2, "DCB" => 2,); our %tlaToTitle = ("UNC" => "Uncovered New Code (+ => 0):\n" . "Newly added code is not tested", "LBC" => "Lost Baseline Coverage (1 => 0):\n" . "Unchanged code is no longer tested", "UIC" => "Uncovered Included Code (# => 0):\n" . "Previously unused code is untested", "UBC" => "Uncovered Baseline Code (0 => 0):\n" . "Unchanged code was untested before, is untested now", "GBC" => "Gained Baseline Coverage (0 => 1):\n" . "Unchanged code is tested now", "GIC" => "Gained Included Coverage (# => 1):\n" . "Previously unused code is tested now", "GNC" => "Gained New Coverage (+ => 1):\n" . "Newly added code is tested", "CBC" => "Covered Baseline Code (1 => 1):\n" . "Unchanged code was tested before and is still tested", "EUB" => "Excluded Uncovered Baseline (0 => #):\n" . "Previously untested code is unused now", "ECB" => "Excluded Covered Baseline (1 => #):\n" . "Previously tested code is unused now", "EUC" => "Excluded Uncovered Current:\n" . "Untested current coverpoint is excluded", "ECC" => "Excluded Covered Current:\n" . "Tested current code is excluded", "DUB" => "Deleted Uncovered Baseline (0 => -):\n" . "Previously untested code has been deleted", "DCB" => "Deleted Covered Baseline (1 => -):\n" . "Previously tested code has been deleted",); our %tlaToLegacy = ("UNC" => "Missed", "GNC" => "Hit",); our %tlaToLegacySrcLabel = ("UNC" => "MIS", "GNC" => "HIT",); our @defaultCutpoints = (7, 30, 180); our @cutpoints; our @ageGroupHeader; our %ageHeaderToBin; our @truncateOwnerTableLevels; # default: truncate everywhere if enabled our $ownerTableElements; # default: do not truncate our $compactSummaryTables = 1; # on by default # How many of the aliases appear in the tooltip popup - including the leader # itself: 0 is all of them, and 1 is the alias alone - which # needs no tooltip. The 'genhtml_alias_list_length' setting our $alias_list_length = 10; use constant { TYPE => 0, NAME => 1, PARENT => 2, RELATIVE_DIR => 3, FULL_DIR => 4, LINE_DATA => 5, BRANCH_DATA => 6, MCDC_DATA => 7, FUNCTION_DATA => 8, FILE_DETAILS => 9, # SourceFile struct - only used for 'file' type SOURCES => 9, # used by top and directory types IS_ABSOLUTE => 10, # used by directory type only # Call-site data - appended rather than inserted beside the other # four metrics so that FILE_DETAILS/SOURCES and IS_ABSOLUTE keep # the slots a serialized database may already have been written # with. Iterate @covTypes rather than writing the list out. CALLGRAPH_DATA => 11, # coverage data list for type DATA => 0, AGE => 1, OWNERS => 2, # not used by Function coverage - no owner }; # The coverage metrics, in the order the report presents them. Defined here # so all the traversals in the code are consistent. our @covTypes = (LINE_DATA, FUNCTION_DATA, CALLGRAPH_DATA, BRANCH_DATA, MCDC_DATA); # Our cover type constants are slots in this tool's data structures and not # lcovutil's COVER_TYPE_ indexes, so this maps each optional metric onto its # row in '@lcovutil::coverType'. The point is that the NAME of a cover type - # the word its messages, its option and its summary label are all built from - # is spelled in exactly one place for the whole toolchain. 'line' has no row # there: line coverage cannot be turned off. our %type2coverTypeRow = (BRANCH_DATA() => lcovutil::COVER_TYPE_BRANCH, MCDC_DATA() => lcovutil::COVER_TYPE_MCDC, FUNCTION_DATA() => lcovutil::COVER_TYPE_FUNCTION, CALLGRAPH_DATA() => lcovutil::COVER_TYPE_CALLGRAPH,); sub type2str { my $t = shift; return 'line' if ($t == LINE_DATA); die("unexpected type '$t'") unless exists($type2coverTypeRow{$t}); return $lcovutil::coverType[$type2coverTypeRow{$t}]->{name}; } sub type2coverpoint { # the user-visible noun for one coverpoint of this type - may not be the # name of the type: a callgraph coverpoint is a callee and an MC/DC # coverpoint is a condition. The words are lcovutil's, so that this tool # and 'lcov' cannot name the same metric two different things return lcovutil::coverpointName(type2str(shift)); } sub _initCounts { # the same key set the TLA list describes, rather than a third copy of it my %hash; foreach my $key ('found', 'hit', @tlaOrderNoExclude) { $hash{$key} = 0; } return \%hash; } sub noBaseline { # no baseline - so we will have only 'UIC' and 'GIC' code # legacy display order is 'hit' followed by 'not hit' @tlaPriorityOrder = ('GNC', 'UNC'); # ..but the titles are not narrowed along with it. This hash is keyed by # category and nothing iterates it, while the stylesheet is written for # every category the data can contain - an unreachable coverpoint is # categorized ECC or EUC whether there is a baseline or not - so overwrite # the two labels which change rather than dropping the ones still needed $tlaToTitle{'UNC'} = 'Not Hit'; $tlaToTitle{'GNC'} = 'Hit'; # the two derived structures are computed once at load time, so recompute # them here rather than leaving them describing the wider list @tlaOrderNoExclude = grep({ !exists($excludedCoverpointTla{$_}) } @tlaPriorityOrder); %displayedTla = map({ ($_ => 1) } @tlaOrderNoExclude); } sub setAgeGroups { #my $numGroups = scalar(@_) + 1; @cutpoints = sort({ $a <=> $b } @_); if (@ageGroupHeader) { # labels were specified by user @ageGroupHeader = split($lcovutil::split_pattern, join($lcovutil::split_char, @ageGroupHeader)); goto done if (scalar(@ageGroupHeader) == scalar(@cutpoints) + 1); # mismatched number - generate warning lcovutil::ignorable_error($lcovutil::ERROR_USAGE, "expected number of 'age' labels to match 'date-bin' cutpoints"); # if message ignored, then assign default labels } @ageGroupHeader = (); my $prefix = "[.."; foreach my $days (@cutpoints) { my $header = $prefix . $days . "] days"; push(@ageGroupHeader, $header); $prefix = "(" . $days . ","; } push(@ageGroupHeader, "(" . $cutpoints[-1] . "..) days"); done: %ageHeaderToBin = (); my $bin = 0; foreach my $header (@ageGroupHeader) { $ageHeaderToBin{$header} = $bin; ++$bin; } } sub findAgeBin { my $age = shift; defined($age) or die("undefined age"); my $bin; for ($bin = 0; $bin <= $#cutpoints; $bin++) { last if ($age <= $cutpoints[$bin]); } return $bin; } sub new { my ($class, $type, $name, $is_absolute_dir) = @_; defined($name) || $type eq 'top' or die("SummaryInfo name should be defined, except at top-level"); my $self = [$type, # 'type' expected to be one of 'file', 'directory', 'top' $name, undef, # parent undef, # relative dir, undef, # full directory path, [undef, [], {}], # line data [undef, [], {}], # branch data [undef, [], {}], # MC/DC data [undef, []] # function data ]; if ($type eq 'file') { $self->[FILE_DETAILS] = undef; # will be SourceFile object } else { $self->[SOURCES] = {}; $self->[IS_ABSOLUTE] = $is_absolute_dir if ($type eq 'directory'); } # a call site has a line line, so it has an owner and a date - see 'owners' $self->[CALLGRAPH_DATA] = [undef, [], {}]; for my $type (@covTypes) { _initCovData($self->[$type]); } bless $self, $class; return $self; } sub _initCovData { # the zero counts of one coverage type: the totals, and the date bins where # annotations are enabled - there is no age data otherwise my $data = shift; $data->[DATA] = _initCounts(); return unless @SourceFile::annotateScript; my $ageList = $data->[AGE]; foreach my $i (0 .. $#cutpoints + 1) { my $h = _initCounts(); $h->{_LB} = ($i == 0) ? undef : $cutpoints[$i - 1]; $h->{_UB} = ($i == $#cutpoints + 1) ? undef : $cutpoints[$i]; $h->{_INDEX} = $i; push(@$ageList, $h); } } sub withCallgraphDestinations { # A copy of this file summary whose callgraph numbers are about the other end # of each call: the call sites whose destination is a function of this # file, wherever they are written, rather than those this file's own # functions contain. Those are the numbers the header of the callers page # shows, that page being the list of exactly those calls - see # 'write_callgraph_page' - and every other number of the copy is this # summary's own. The callgraph numbers start at zero and are filled one # call at a time by 'countCallgraphDestination' my $self = shift; my $copy = bless([@$self], ref($self)); $copy->[CALLGRAPH_DATA] = [undef, [], {}]; _initCovData($copy->[CALLGRAPH_DATA]); return $copy; } sub countCallgraphDestination { # one call into this file, of category '$tla', whose destination function was # last changed by the author and at the age '$annot' says - or undef, where # the annotation callback has no answer for it. Binned the way # '_countCallgraphTlaData' bins a call site, except by the line the callee # starts on rather than the line the call is written on my ($self, $tla, $annot) = @_; my $data = $self->[CALLGRAPH_DATA]; $data->[DATA]->{$tla} += 1; # a deleted call is in no current source, so it has neither a date nor an # author - and is not a call the current tests could have made return unless $tlaLocation{$tla} & 0x1; my $hit = exists($hitTla{$tla}); $data->[DATA]->{found} += 1; $data->[DATA]->{hit} += 1 if $hit; # ..and an annotation is there only where the annotation callback is, which # is where the date bins are return unless defined($annot); $self->callgraphCovCount($tla, 'age', $annot->{age}, 1); $self->callgraphCovCount('found', 'age', $annot->{age}, 1); $self->callgraphCovCount('hit', 'age', $annot->{age}, 1) if $hit; return unless defined($annot->{owner}); my $owner = $data->[OWNERS]->{$annot->{owner}} ||= {found => 0, hit => 0}; $owner->{$tla} += 1; $owner->{found} += 1; $owner->{hit} += 1 if $hit; } sub recountCallgraphSites { # The callgraph numbers of this file summary, counted again from the call # sites the call site pages show rather than from the records: '$sites' # is [category, {age, owner} or undef] of each of them - see # main::_countedCallSites. A page shows one row for the records of # one location which lead to one function. How a particular function's # alias groups are counted depends on 'show_callgraph_summary' (see # lcovrc(5)), so the two counts differ. Every number in the report # is derived from this one - the directory and top-level totals, the # coverage criteria callback, etc. # A site of the baseline which is not in the current version has no row, # and keeps the counts found in the .info records: of its category, # and - where its line is still there to be dated ('EUB', 'ECB') - of # the date and owner bins it is found (and possibly hit) in my ($self, $sites) = @_; my $old = $self->[CALLGRAPH_DATA]; my $data = [undef, [], {}]; _initCovData($data); $self->[CALLGRAPH_DATA] = $data; foreach my $tla (keys(%deletedTla), 'EUB', 'ECB') { # ..which only a differential report contains next unless defined($old->[DATA]->{$tla}); $data->[DATA]->{$tla} = $old->[DATA]->{$tla}; my @keys = ($tla); push(@keys, 'found', exists($hitTla{$tla}) ? 'hit' : ()) if $tlaLocation{$tla} & 0x1; for (my $bin = 0; $bin < scalar(@{$data->[AGE]}); ++$bin) { my $n = $old->[AGE]->[$bin]->{$tla}; next unless $n; $data->[AGE]->[$bin]->{$_} += $n foreach @keys; } while (my ($name, $owner) = each(%{$old->[OWNERS]})) { my $n = $owner->{$tla}; next unless $n; my $o = $data->[OWNERS]->{$name} ||= {found => 0, hit => 0}; $o->{$_} += $n foreach @keys; } } $self->countCallgraphDestination(@$_) foreach @$sites; } # deserialization: copy the coverage portion of the undumped data sub copyGuts { my ($self, $that) = @_; my @copy = (RELATIVE_DIR, FULL_DIR, @covTypes); if ($that->[TYPE] eq 'file') { push(@copy, FILE_DETAILS); } elsif ($that->[TYPE] eq 'directory') { push(@copy, IS_ABSOLUTE); } for my $key (@copy) { $self->[$key] = $that->[$key]; } } sub name { my $self = shift; return $self->[NAME]; } sub unsetDirs { my $self = shift; # need to reset after fork failure - we set the values # just before forking, but now have to put them back. die('bad usage: unsetDirs()') unless (defined($self->[FULL_DIR]) && defined($self->[RELATIVE_DIR])); $self->[FULL_DIR] = undef; $self->[RELATIVE_DIR] = undef; } sub relativeDir { my ($self, $dir_string) = @_; die("bad usage: relativeDir(" . (defined($dir_string) ? $dir_string : '') . ') current: ' . (defined($self->[RELATIVE_DIR]) ? $self->[RELATIVE_DIR] : '') ) unless ((!defined($dir_string) && defined($self->[RELATIVE_DIR])) || (defined($dir_string) && !defined($self->[RELATIVE_DIR]))); $self->[RELATIVE_DIR] = $dir_string if defined($dir_string); return $self->[RELATIVE_DIR]; } sub fullDir { my ($self, $dir_string) = @_; die("bad usage fullDir()") unless ((!defined($dir_string) && defined($self->[FULL_DIR])) || (defined($dir_string) && !defined($self->[FULL_DIR]))); $self->[FULL_DIR] = $dir_string if defined($dir_string); return $self->[FULL_DIR]; } sub type { my $self = shift; return $self->[TYPE]; } sub is_directory { my ($self, $is_absolute) = @_; return ( $self->type() eq 'directory' ? ((defined($is_absolute) && $is_absolute) ? $self->[IS_ABSOLUTE] : 1) : 0); } sub parent { my $self = shift; return $self->[PARENT]; } sub setParent { my ($self, $parent) = @_; die("expected parent dir") unless (ref($parent) eq "SummaryInfo" && ($main::flat ? 'top' : 'directory') eq $parent->type()); $self->[PARENT] = $parent; } sub sources { my $self = shift; die("bad usage") if $self->type() eq 'file'; return keys(%{$self->[SOURCES]}); } sub fileDetails { my ($self, $data) = @_; $self->type() eq 'file' or die("source details only available for file"); !(defined($data) && defined($self->[FILE_DETAILS])) or die("attempt to set details in initialized struct"); !defined($data) || ref($data) eq 'SourceFile' or die("unexpected data arg " . ref($data)); $self->[FILE_DETAILS] = $data if defined($data); return $self->[FILE_DETAILS]; } sub get_sorted_keys { # sort_type in ($SORT_FILE, $SORT_LINE, $SORT_FUNC, $SORT_BRANCH, # $SORT_MCDC, $SORT_CALLGRAPH) my ($self, $sort_type, $include_dirs) = @_; die("invalid usage") if $self->type() eq 'file'; my $sources = $self->[SOURCES]; my @keys = $self->sources(); my @l; foreach my $k (@keys) { my $data = $sources->{$k}; next if ($data->type() eq 'directory' && (!defined($include_dirs) || 0 == $include_dirs)); push(@l, $k); } if ($sort_type == $SORT_FILE) { # alphabetic return sort(@l); } my $covtype; if ($sort_type == $SORT_LINE) { # Sort by number of instrumented lines without coverage $covtype = LINE_DATA; } elsif ($sort_type == $SORT_FUNC) { # Sort by number of instrumented functions without coverage $covtype = FUNCTION_DATA; } elsif ($sort_type == $SORT_MCDC) { # Sort by number of MC/DC points without coverage $covtype = MCDC_DATA; } elsif ($sort_type == $SORT_CALLGRAPH) { # Sort by number of call sites without coverage $covtype = CALLGRAPH_DATA; } else { die("unexpected sort type $sort_type") unless ($sort_type == $SORT_BRANCH); # Sort by number of instrumented branches without coverage $covtype = BRANCH_DATA; } if ($main::opt_missed) { # sort by directory first then secondary key return sort({ my $da = $sources->{$a}; my $db = $sources->{$b}; # directories then files if list includes both $da->type() cmp $db->type() or $db->get_missed($covtype) <=> $da->get_missed($covtype) or # sort alphabetically in case of tie $da->name() cmp $db->name() } @l); } else { return sort({ my $da = $sources->{$a}; my $db = $sources->{$b}; $da->type() cmp $db->type() or $da->get_rate($covtype) <=> $db->get_rate($covtype) or $da->name() cmp $db->name() } @l); } } sub get_source { my ($self, $name) = @_; die("bad usage") if $self->type() eq 'file'; return exists($self->[SOURCES]->{$name}) ? $self->[SOURCES]->{$name} : undef; } sub remove_source { my ($self, $name) = @_; die("bad usage") if $self->type() eq 'file'; delete $self->[SOURCES]->{$name}; } sub get { my ($self, $key, $type) = @_; $type = LINE_DATA if !defined($type); my $hash = $self->[$type]->[DATA]; if ($key eq "missed") { my $missed = 0; foreach my $k ('UBC', 'UNC', 'UIC', 'LBC') { $missed += $hash->{$k} if (exists($hash->{$k})); } return $missed; } else { die("unexpected 'get' key $key") unless exists($hash->{$key}); return $hash->{$key}; } } # Return a relative value for the specified found&hit values # which is used for sorting the corresponding entries in a # file list. # sub get_rate { my ($self, $covtype) = @_; my $hash = $self->[$covtype]->[DATA]; my $found = $hash->{found}; my $hit = $hash->{hit}; if ($found == 0) { #return 100; return 1000; } #return (100.0 * $hit) / $found; return int($hit * 1000 / $found) * 10 + 2 - (1 / $found); } sub get_missed { my ($self, $covtype) = @_; my $hash = $self->[$covtype]->[DATA]; my $found = $hash->{found}; my $hit = $hash->{hit}; return $found - $hit; } sub contains_owner { my ($self, $owner) = @_; return exists($self->[LINE_DATA]->[OWNERS]->{$owner}); } sub owners { # return possibly empty list of line owners in this file # - filter only those which have 'missed' lines my ($self, $showAll, $covType) = @_; (!defined($covType) || $covType == LINE_DATA || $covType == BRANCH_DATA || $covType == MCDC_DATA || $covType == CALLGRAPH_DATA) or die("unsupported coverage type '$covType'"); my $hash = $self->[defined($covType) ? $covType : LINE_DATA]->[OWNERS]; return keys(%$hash) if $showAll; my @rtn; OWNER: foreach my $name (keys(%$hash)) { my $h = $hash->{$name}; foreach my $tla ('UNC', 'UBC', 'UIC', 'LBC') { if (exists($h->{$tla})) { die("unexpected 0 (zero) value for $tla of $name in " . $self->path()) if (0 == $h->{$tla}); push(@rtn, $name); next OWNER; } } } return @rtn; } sub owner_tlaCount { my ($self, $name, $tla, $covType) = @_; die("$name not found in owner data for" . $self->path()) unless exists($self->[LINE_DATA]->[OWNERS]->{$name}); return 0 # not supported, yet if $covType == FUNCTION_DATA; (!defined($covType) || $covType == LINE_DATA || $covType == BRANCH_DATA || $covType == MCDC_DATA || $covType == CALLGRAPH_DATA) or die("unsupported coverage type '$covType'"); my $hash = $self->[defined($covType) ? $covType : LINE_DATA]->[OWNERS]->{$name}; return $hash->{$tla} if (exists($hash->{$tla})); if ($tla eq "found") { my $total = 0; foreach my $k (keys(%$hash)) { # count only code that can be hit (ie., not excluded) $total += $hash->{$k} unless exists($excludedTla{$k}); } return $total; } elsif ($tla eq "hit") { my $hit = 0; foreach my $k ('CBC', 'GBC', 'GIC', 'GNC') { $hit += $hash->{$k} if (exists($hash->{$k})); } return $hit; } elsif ($tla eq "missed") { my $missed = 0; foreach my $k ('UBC', 'UNC', 'UIC', 'LBC') { $missed += $hash->{$k} if (exists($hash->{$k})); } return $tla eq "missed" ? $missed : -$missed; } die("unexpected TLA $tla") unless exists($tlaLocation{$tla}); return 0; } sub hasOwnerInfo { my $self = shift; return %{$self->[LINE_DATA]->[OWNERS]} ? 1 : 0; } sub hasDateInfo { my $self = shift; # we get date- and owner information at the same time from the # annotation-script - so, if we have owner info, then we have date info too. return %{$self->[LINE_DATA]->[OWNERS]} ? 1 : 0; } sub covCountMethod { # The per-metric bin accessor, as a code ref - so a caller which is already # iterating @covTypes does not need a five-armed conditional to pick one, # and 'buildDateSummaryTable' can be handed the same thing it always was. my $covType = shift; return \&lineCovCount if $covType == LINE_DATA; return \&functionCovCount if $covType == FUNCTION_DATA; return \&callgraphCovCount if $covType == CALLGRAPH_DATA; return \&branchCovCount if $covType == BRANCH_DATA; die("unexpected cover type $covType") unless $covType == MCDC_DATA; return \&mcdcCovCount; } sub metricEnabled { # Is this metric being reported at all? my $covType = shift; return 1 if $covType == LINE_DATA; return $lcovutil::func_coverage if $covType == FUNCTION_DATA; return $lcovutil::callgraph_coverage if $covType == CALLGRAPH_DATA; return $lcovutil::br_coverage if $covType == BRANCH_DATA; die("unexpected cover type $covType") unless $covType == MCDC_DATA; return $lcovutil::mcdc_coverage; } sub binCovData { # {covType => [missed, total, callback]} for every metric, built from a # factory which makes the per-metric callback. The disabled metrics get a # callback but no counts: the callback is used to render a table cell my $makeCallback = shift; my %bin; foreach my $covType (@covTypes) { my $cb = &$makeCallback($covType); my $enabled = metricEnabled($covType); $bin{$covType} = [$enabled ? $cb->get('missed') : 0, $enabled ? $cb->get('found') : 0, $cb ]; } return %bin; } sub findOwnerList { # return [ [owner, {covType => [missed, total, callback], ..}], ..] for each # owner, sorted in descending order of number of missed lines. # Keyed by coverage type rather than positional - easier to add new # types this way. # function counts in the MC/DC owner-bin table. my ($self, $callback_type, $truncate_me, $all) = @_; my @owners; foreach my $owner (keys(%{$self->[LINE_DATA]->[OWNERS]})) { my %bin = binCovData(sub { return OwnerDetailCallback->new($self, $owner, shift); }); # filter owners who have unexercised code, if requested. next unless $all || grep({ 0 != $bin{$_}->[0] } (LINE_DATA, FUNCTION_DATA, CALLGRAPH_DATA, BRANCH_DATA)); push(@owners, [$owner, \%bin]); } @owners = sort({ my ($x, $y) = ($a->[1]->{(LINE_DATA)}, $b->[1]->{(LINE_DATA)}); $y->[0] <=> $x->[0] || # missed $y->[1] <=> $x->[1] || # then total $a->[0] cmp $b->[0] } @owners); # then by name my $truncated; if ($truncate_me && defined($ownerTableElements) && $ownerTableElements < scalar(@owners) && (0 == scalar(@truncateOwnerTableLevels) || grep(/$callback_type/, @truncateOwnerTableLevels)) ) { # don't truncate the 'primary' key owner table $truncated = (scalar(@owners) - $ownerTableElements); #lcovutil::info("truncating $truncated elements in header table\n"); splice(@owners, $ownerTableElements); } else { $truncated = 0; } return (scalar(@owners) ? \@owners : undef, $truncated); } sub append { my ($self, $record) = @_; # keep track of the records that get merged into me.. defined($record->[NAME]) or die("attempt to append anonymous SummaryInfo record"); !exists($self->[SOURCES]->{$record->[NAME]}) or die("duplicate merge record " . $record->[NAME]); $self->[SOURCES]->{$record->[NAME]} = $record; die($record->name() . " already has parent " . $record->parent()->name()) if (defined($record->parent()) && $record->[PARENT] != $self); $record->[PARENT] = $self if !defined($record->parent()); foreach my $group (@covTypes) { my $mine = $self->[$group]->[DATA]; my $yours = $record->[$group]->[DATA]; while (my ($key, $value) = each(%$yours)) { $mine->{$key} += $yours->{$key}; } } # there will be no date info if we didn't also collect owner data # merge the date- and owner data, if we aren't going to display it # (In future, probably want to serialize the data for future processing) if (%{$record->[LINE_DATA]->[OWNERS]}) { foreach my $covType (@covTypes) { my $covCount = covCountMethod($covType); for (my $bin = 0; $bin <= $#ageGroupHeader; ++$bin) { foreach my $key (keys %{$self->[$covType]->[DATA]}) { # duplicate line-coverage buckets my $ageval = $self->age_sample($bin); $self->$covCount($key, "age", $ageval, $record->$covCount($key, "age", $ageval)); } } my $ownerList = $self->[$covType]->[OWNERS]; while (my ($name, $yours) = each(%{$record->[$covType]->[OWNERS]})) { if (!exists($ownerList->{$name})) { $ownerList->{$name} = {}; } my $mine = $ownerList->{$name}; while (my ($tla, $count) = each(%$yours)) { if (exists($mine->{$tla})) { $mine->{$tla} += $count; } else { $mine->{$tla} = $count; } } } } } return $self; } sub age_sample { my ($self, $i) = @_; my $bin = $self->[LINE_DATA]->[AGE]->[$i]; return ($i < $#ageGroupHeader) ? $bin->{_UB} : ($bin->{_LB} + 1); } sub lineCovCount { my ($self, $key, $group, $age, $delta) = @_; $delta = 0 unless defined($delta); if ($key eq 'missed') { my $found = $self->lineCovCount('found', $group, $age); my $hit = $self->lineCovCount('hit', $group, $age); return $found - $hit; } if ($group eq "age") { my $a = $self->[LINE_DATA]->[AGE]; my $bin = SummaryInfo::findAgeBin($age); exists($a->[$bin]) && exists($a->[$bin]->{$key}) or die("unexpected key '$key' for bin '$bin'"); $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } # every caller asks for an age bin - see covCountMethod and # buildDateSummaryTable - so the per-group totals below are never used # LCOV_UNREACHABLE_START my $d = $self->[$group]->[DATA]; defined($d) or die("SummaryInfo::value: unrecognized group $group\n"); defined($d->{$key}) or die("SummaryInfo::value: unrecognized key $key\n"); $d->{$key} += $delta; return $d->{$key}; # LCOV_UNREACHABLE_STOP } sub branchCovCount { my ($self, $key, $group, $age, $delta) = @_; $delta = 0 unless defined($delta); if ($key eq 'missed') { my $found = $self->branchCovCount('found', $group, $age); my $hit = $self->branchCovCount('hit', $group, $age); return $found - $hit; } my $branch = $self->[BRANCH_DATA]; if ($group eq "age") { my $a = $branch->[AGE]; my $bin = SummaryInfo::findAgeBin($age); unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected key '$key' for bin '$bin'"); return; # LCOV_UNREACHABLE_STOP } $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } # every caller asks for an age bin - see covCountMethod and # buildDateSummaryTable - so the per-group totals below are never used # LCOV_UNREACHABLE_START my $d = $branch->[DATA]; defined($d) or die("SummaryInfo::value: unrecognized branch group $group\n"); defined($d->{$key}) or die("SummaryInfo::value: unrecognized branch key $key\n"); $d->{$key} += $delta; return $d->{$key}; # LCOV_UNREACHABLE_STOP } sub mcdcCovCount { my ($self, $key, $group, $age, $delta) = @_; $delta = 0 unless defined($delta); if ($key eq 'missed') { my $found = $self->mcdcCovCount('found', $group, $age); my $hit = $self->mcdcCovCount('hit', $group, $age); return $found - $hit; } my $mcdc = $self->[MCDC_DATA]; if ($group eq "age") { my $a = $mcdc->[AGE]; my $bin = SummaryInfo::findAgeBin($age); unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected key '$key' for bin '$bin'"); return; # LCOV_UNREACHABLE_STOP } $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } # every caller asks for an age bin - see covCountMethod and # buildDateSummaryTable - so the per-group totals below are never used # LCOV_UNREACHABLE_START my $d = $mcdc->[DATA]; defined($d) or die("SummaryInfo::value: unrecognized MC/DC group $group\n"); defined($d->{$key}) or die("SummaryInfo::value: unrecognized MC/DC key $key\n"); $d->{$key} += $delta; return $d->{$key}; # LCOV_UNREACHABLE_STOP } sub callgraphCovCount { my ($self, $key, $group, $age, $delta) = @_; $delta = 0 unless defined($delta); if ($key eq 'missed') { my $found = $self->callgraphCovCount('found', $group, $age); my $hit = $self->callgraphCovCount('hit', $group, $age); return $found - $hit; } my $callee = $self->[CALLGRAPH_DATA]; if ($group eq "age") { my $a = $callee->[AGE]; my $bin = SummaryInfo::findAgeBin($age); unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected key '$key' for bin '$bin'"); return; # LCOV_UNREACHABLE_STOP } $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } # every caller asks for an age bin - see covCountMethod and # buildDateSummaryTable - so the per-group totals below are never used # LCOV_UNREACHABLE_START my $d = $callee->[DATA]; defined($d) or die("SummaryInfo::value: unrecognized callee group $group\n"); defined($d->{$key}) or die("SummaryInfo::value: unrecognized callee key $key\n"); $d->{$key} += $delta; return $d->{$key}; # LCOV_UNREACHABLE_STOP } sub functionCovCount { my ($self, $key, $group, $age, $delta) = @_; $delta = 0 unless defined($delta); if ($key eq 'missed') { my $found = $self->functionCovCount('found', $group, $age); my $hit = $self->functionCovCount('hit', $group, $age); return $found - $hit; } my $func = $self->[FUNCTION_DATA]; if ($group eq "age") { my $a = $func->[AGE]; my $bin = SummaryInfo::findAgeBin($age); unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected key '$key' for bin '$bin'"); return; # LCOV_UNREACHABLE_STOP } $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } # every caller asks for an age bin - see covCountMethod and # buildDateSummaryTable - so the per-group totals below are never used # LCOV_UNREACHABLE_START my $d = $func->[DATA]; defined($d) or die("SummaryInfo::value: unrecognized function group $group\n"); defined($d->{$key}) or die("SummaryInfo::value: unrecognized function key $key\n"); $d->{$key} += $delta; return $d->{$key}; # LCOV_UNREACHABLE_STOP } sub removeLine { my ($self, $lineData) = @_; my $l = $self->[LINE_DATA]->[DATA]; if ($lineData->in_curr()) { $l->{hit} -= $lineData->curr_count() != 0; $l->{found} -= 1; } if ($lcovutil::br_coverage) { my $b = $lineData->current_branch(); if (defined($b)) { my ($found, $hit) = $b->totals(); my $br = $self->[BRANCH_DATA]->[DATA]; $br->{hit} -= $hit; $br->{found} -= $found; } } if ($lcovutil::mcdc_coverage) { my $b = $lineData->current_mcdc(); if (defined($b)) { my ($found, $hit) = $b->totals(); my $br = $self->[MCDC_DATA]->[DATA]; $br->{hit} -= $hit; $br->{found} -= $found; } } if ($lcovutil::func_coverage) { my $f = $lineData->current_function(); if (defined($f)) { my $fn = $self->[FUNCTION_DATA]->[DATA]; $fn->{hit} -= $f->hit() != 0; $fn->{found} -= 1; } } if ($lcovutil::callgraph_coverage) { # only the sites which are in 'current' are in the totals - the '.info' # the totals came from knows nothing about the baseline my $c = $self->[CALLGRAPH_DATA]->[DATA]; foreach my $site ($lineData->call_sites()) { my $count = $site->curr_count(); next unless defined($count); $c->{hit} -= lcovutil::site_count_hit($count); $c->{found} -= 1; } } } sub is_empty { my $self = shift; my $t = $self->[LINE_DATA]->[DATA]; foreach my $category (keys %$t) { return 0 if 0 != $t->{$category}; } return 1; } sub type_count { my ($type, $status, $self, $delta) = @_; my $l = $self->[$type]->[DATA]; $l->{$status} += $delta if defined($delta); return $l->{$status}; } sub lines_found { return type_count(LINE_DATA, 'found', @_); } sub lines_hit { return type_count(LINE_DATA, 'hit', @_); } sub function_found { return type_count(FUNCTION_DATA, 'found', @_); } sub function_hit { return type_count(FUNCTION_DATA, 'hit', @_); } sub branch_found { return type_count(BRANCH_DATA, 'found', @_); } sub branch_hit { return type_count(BRANCH_DATA, 'hit', @_); } sub mcdc_found { return type_count(MCDC_DATA, 'found', @_); } sub mcdc_hit { return type_count(MCDC_DATA, 'hit', @_); } sub callgraph_found { return type_count(CALLGRAPH_DATA, 'found', @_); } sub callgraph_hit { return type_count(CALLGRAPH_DATA, 'hit', @_); } sub selected { my $rtn; eval { $rtn = $selectCallback->select(@_); }; if ($@) { my $err = lcovutil::unreported_error($@); $rtn = 1; # return everything lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, "select(..) failed: $err"); } return $rtn; } sub tlaSummary { my ($self, $t) = @_; my $type; if ($t eq 'line') { $type = LINE_DATA; } elsif ($t eq 'function') { $type = FUNCTION_DATA; } elsif ($t eq 'mcdc') { $type = MCDC_DATA; } elsif ($t eq 'callgraph') { $type = CALLGRAPH_DATA; } else { die("unknown type $t") unless 'branch' eq $t; $type = BRANCH_DATA; } my $d = $self->[$type]->[DATA]; return join(' ', map({ $_ . ':' . $d->{$_} } _nonZeroKeys($d, @tlaOrderNoExclude))); } sub _nonZeroKeys { # Those of @keys which $counts has a non-zero count for, in @keys order. # A key can be missing rather than zero: a date or owner bin holds only # the TLAs 'lineCovCount' knows about - not 'ECC' or 'EUC' - and the # whole-file counts hold whatever '_initCounts' put there. my ($counts, @keys) = @_; return grep({ exists($counts->{$_}) && 0 != $counts->{$_} } @keys); } sub _nonZeroCounts { # Those of @keys with a non-zero count, as a hashref - or undef when there # are none, so a caller can tell 'nothing to report' from 'reported zero'. my ($counts, @keys) = @_; my $nonZero; foreach my $k (_nonZeroKeys($counts, @keys)) { $nonZero->{$k} = $counts->{$k}; } return $nonZero; } sub _binCounts { # The non-zero counts of one age or owner bin, or undef if it has none. my $bin = shift; return _nonZeroCounts($bin, @tlaPriorityOrder, 'found', 'hit'); } sub checkCoverageCriteria { my $self = shift; my $type = $self->type(); if ($type eq 'top') { # simplistic top-level criteria.. CoverageCriteria::check_failUnder($main::current_data); } return unless ($CoverageCriteria::criteriaCallback && (0 == scalar(@CoverageCriteria::criteriaCallbackLevels) || grep(/$type/, @CoverageCriteria::criteriaCallbackLevels))); my $start = Time::HiRes::gettimeofday(); my %data; foreach my $t (@covTypes) { # the keys are 'hit', 'found' and the TLAs my $d = $self->[$t]->[DATA]; my $counts = _nonZeroCounts($d, keys(%$d)); $data{type2str($t)} = $counts if $counts; } if (grep(/^date$/, @CoverageCriteria::criteriaCallbackTypes)) { foreach my $t (@covTypes) { my $key = type2str($t); next unless exists($self->[$t]->[AGE]); my $ageBins = $self->[$t]->[AGE]; foreach my $i (0 .. $#cutpoints + 1) { my $counts = _binCounts($ageBins->[$i]); next unless $counts; $data{$key}->{$i} = $counts; } } } if (grep(/^owner$/, @CoverageCriteria::criteriaCallbackTypes)) { foreach my $t (LINE_DATA, CALLGRAPH_DATA, BRANCH_DATA, MCDC_DATA) { my $key = type2str($t); next unless exists($self->[$t]->[OWNERS]); my $ownerBins = $self->[$t]->[OWNERS]; while (my ($owner, $bin) = each(%$ownerBins)) { # every owner appears, even with nothing to say - as before $data{$key}->{$owner} = _binCounts($bin) || {}; } } } my $name = $self->type() eq 'top' ? 'top' : $self->name(); my $cmd = join(' ', @CoverageCriteria::coverageCriteriaScript) . ' \'' . $name . '\' ' . $self->type() . ' \'json_encoded_data\''; # command: script name (top|dir|file) jsonString args.. lcovutil::info(1, "criteria: '$cmd'\n"); CoverageCriteria::executeCallback($self->type(), $name, \%data); my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{criteria}{$name} = $end - $start; } package OwnerDetailCallback; # somewhat of a hack...I want a class which has a callback 'get' # that matches the SummaryInfo::get method - but returns owner-specific # information use constant { SUMMARY => 0, OWNER => 1, TYPE => 2, }; sub new { my ($class, $summary, $owner, $covType) = @_; die("missing type") unless defined($covType); my $self = [$summary, $owner, $covType]; bless $self, $class; return $self; } sub label { my $self = shift; return $self->owner(); } sub cb_type { my $self = shift; return 'owner'; } sub get { my ($self, $key, $type) = @_; my ($summary, $owner, $covType) = @$self; die("unexpected type $type") unless !defined($type) || ($type == $covType); return $summary->owner_tlaCount($owner, $key, $covType); } sub owner { my $self = shift; return $self->[OWNER]; } sub covType { my $self = shift; return $self->[TYPE]; } package DateDetailCallback; # as above: callback class to return date-specific TLA counts use constant { SUMMARY => 0, AGE => 1, TYPE => 2, BIN => 3, }; sub new { my ($class, $summary, $age, $covType) = @_; $covType = SummaryInfo::LINE_DATA unless defined($covType); my $self = [$summary, $age, $covType, SummaryInfo::findAgeBin($age)]; bless $self, $class; return $self; } sub get { my ($self, $key, $type) = @_; my ($summary, $age, $covType) = @$self; die("unexpected type $type") unless (!defined($type) || ($type == $covType)); my $covCount = SummaryInfo::covCountMethod($covType); return $summary->$covCount($key, 'age', $age); } sub label { my $self = shift; return $self->age(); } sub cb_type { my $self = shift; return 'date'; } sub age { my $self = shift; return $self->[AGE]; } sub bin { my $self = shift; return $self->[BIN]; } sub covType { my $self = shift; return $self->[TYPE]; } package FileOrDirectoryCallback; # callback class used by 'write_file_table' to retrieve count of the # various coverpoint categories in the file or directory (i.e., total # number). # Other callbacks classes are used to retrieve per-owner counts, etc. sub new { # dirSummary: SummaryInfo object my ($class, $path, $summary) = @_; my $self = [$path, $summary]; bless $self, $class; return $self; } # page_link is HTML reference to file table page next level down - # for top-level page: link to directory-level page # for directory-level page: link to source file details sub page_link { my $self = shift; my $data = $self->summary(); my $page_link = ''; $page_link = ($main::flat && '.' ne $data->relativeDir()) ? File::Spec->catfile($data->relativeDir(), $self->name()) : $self->name(); if ($data->type() eq 'file') { if ($main::no_sourceview) { return ""; } $page_link .= ".gcov"; $page_link .= ".frameset" if ($main::frames); } else { $page_link =~ s/^$lcovutil::dirseparator_re//; $page_link .= $lcovutil::dirseparator . "index"; } $page_link .= '.' . $main::html_ext; return $lcovutil::case_insensitive ? lc($page_link) : $page_link; } sub data { my $self = shift; # one (found, hit) pair per element of @SummaryInfo::covTypes, in that # order, then ($page_link, $fileSummary, $fileDetails) my $summary = $self->summary(); my @rtn; foreach my $type (@SummaryInfo::covTypes) { my $hash = $summary->[$type]->[SummaryInfo::DATA]; push(@rtn, $hash->{found}, $hash->{hit}); } my $link = $self->page_link(); my $sourceFile; $sourceFile = $summary->fileDetails() if 'file' eq $summary->type(); push(@rtn, $link, $summary, $sourceFile); return @rtn; } sub secondaryElementFileData { my ($self, $name) = @_; my $summary = $self->summary(); my $sourceFile; $sourceFile = $summary->fileDetails() if 'file' eq $summary->type(); return [$summary->name(), $summary, $sourceFile, $self->page_link()]; } sub name { my $self = shift; return $self->[0]; } sub summary { # return undef or SummaryInfo object my $self = shift; return $self->[1]; } sub findOwnerList { my $self = shift; # return [ [owner, {covType => [missed, total, callback], ..}], ..] for each # owner - sorted in descending order number of missed lines return $self->summary()->findOwnerList(@_); } sub dateDetailCallback { # callback to compute count in particular date bin my ($self, $ageval, $covtype) = @_; grep({ $_ == $covtype } @SummaryInfo::covTypes) or die("'$covtype' type not supported"); return DateDetailCallback->new($self->summary(), $ageval, $covtype); } sub totalCallback { my ($self, $covtype) = @_; # callback to compute total elements of 'covtype' in each TLA if (SummaryInfo::LINE_DATA == $covtype) { return $self->summary(); } else { return CovTypeSummaryCallback->new($self->summary(), $covtype); } } package FileOrDirectoryOwnerCallback; # callback class used by 'write_file_table' to retrieve owner- # specific coverage numbers (for all entries in the directory) sub new { my ($class, $owner, $dirSummary) = @_; my $self = [$owner, $dirSummary]; bless $self, $class; return $self; } sub name { my $self = shift; return $self->[0]; } sub summary { my $self = shift; return $self->[1]; } sub data { my $self = shift; # one (found, hit) pair per element of @SummaryInfo::covTypes, in that # order - this is the 'totals' callback for this owner, so there is no # associated file or summary info to follow them. # Function coverage has no owner, so its pair is 0, 0. my @rtn; foreach my $covType (@SummaryInfo::covTypes) { my $cb = OwnerDetailCallback->new($self->[1], $self->[0], $covType); push(@rtn, $cb->get('found'), $cb->get('hit')); } return @rtn; } sub totalCallback { # callback to compute total 'covtype' elements in each TLA my ($self, $covtype) = @_; die("$covtype not supported by OwnerDetail callback") unless ($covtype == SummaryInfo::LINE_DATA || $covtype == SummaryInfo::CALLGRAPH_DATA || $covtype == SummaryInfo::BRANCH_DATA || $covtype == SummaryInfo::MCDC_DATA); return OwnerDetailCallback->new($self->[1], $self->name(), $covtype); } sub findFileList { my ($self, $all) = @_; # return [skipped, [filename, {covType => [missed, total, callback], ..}], # ..] for each file in which this owner has at least one # coverpoint - sorted in descending order number of missed lines my $dirSummary = $self->[1]; my $owner = $self->[0]; my @files; my $skipped = 0; foreach my $file ($dirSummary->sources()) { my $source = $dirSummary->get_source($file); next unless $source->contains_owner($owner); my %bin = SummaryInfo::binCovData(sub { return OwnerDetailCallback->new($source, $owner, shift); }); next unless grep({ 0 != $bin{$_}->[1] } @SummaryInfo::covTypes); if ($all || grep({ 0 != $bin{$_}->[0] } @SummaryInfo::covTypes)) { push(@files, [$file, \%bin]); } else { ++$skipped; } } return [$skipped, @files]; } sub secondaryElementFileData { my ($self, $name) = @_; my $dirSummary = $self->[1]; my $file = File::Basename::basename($name); my $sourceSummary = $dirSummary->get_source($name); my $page_link; if ($sourceSummary->is_directory()) { $page_link = File::Spec->catfile($name, "index-bin_owner." . $main::html_ext); } elsif ($main::no_sourceview) { $page_link = ""; } else { $name = $file; $page_link = $name . ".gcov."; $page_link .= "frameset." if $main::frames; $page_link .= $main::html_ext; } $page_link = lc($page_link) if $lcovutil::case_insensitive; # pass owner in callback data my $sourceFile; $sourceFile = $sourceSummary->fileDetails() if 'file' eq $sourceSummary->type(); return [$name, $sourceSummary, $sourceFile, $page_link, $self->[0]]; } package FileOrDirectoryDateCallback; # callback class used by 'write_file_table' to retrieve date- # specific coverage numbers (for all entries in the directory) sub new { my ($class, $bin, $dirSummary) = @_; my $self = [$bin, $dirSummary->age_sample($bin), $dirSummary]; bless $self, $class; return $self; } sub name { my $self = shift; return $SummaryInfo::ageGroupHeader[$self->[0]]; } sub summary { my $self = shift; return $self->[2]; } sub data { my $self = shift; my @rtn; foreach my $covType (@SummaryInfo::covTypes) { my $cb = DateDetailCallback->new($self->[2], $self->[1], $covType); push(@rtn, $cb->get('found'), $cb->get('hit')); } # one (found, hit) pair per element of @SummaryInfo::covTypes - this is the # top-level 'total' callback, so no associated file or summary info return @rtn; } sub totalCallback { # callback to compute total elements of 'covtype' in each TLA my ($self, $covtype) = @_; return DateDetailCallback->new($self->[2], $self->[1], $covtype); } sub findFileList { my ($self, $all) = @_; # return [skipped, [filename, {covType => [missed, total, callback], ..}], # ..] for each file which has a coverpoint in this date bin # - sorted in descending order number of missed lines my $dirSummary = $self->[2]; my $ageval = $self->[1]; my @files; my $skipped = 0; foreach my $file ($dirSummary->sources()) { my $source = $dirSummary->get_source($file); my %bin = SummaryInfo::binCovData(sub { return DateDetailCallback->new($source, $ageval, shift); }); next unless grep({ 0 != $bin{$_}->[1] } @SummaryInfo::covTypes); if ($all || grep({ 0 != $bin{$_}->[0] } @SummaryInfo::covTypes)) { push(@files, [$file, \%bin]); } else { ++$skipped; } } return [$skipped, @files]; } sub secondaryElementFileData { my ($self, $name) = @_; my $dirSummary = $self->[2]; my $file = File::Basename::basename($name); my $sourceSummary = $dirSummary->get_source($name); my $page_link; if ($sourceSummary->is_directory()) { $page_link = File::Spec->catfile($name, "index-bin_date." . $main::html_ext); } elsif ($main::no_sourceview) { $page_link = ""; } else { $name = $file; $page_link = $name . ".gcov."; $page_link .= "frameset." if $main::frames; $page_link .= $main::html_ext; } $page_link = lc($page_link) if $lcovutil::case_insensitive; # pass bin index in callback data my $sourceFile; $sourceFile = $sourceSummary->fileDetails() if 'file' eq $sourceSummary->type(); return [$name, $sourceSummary, $sourceFile, $page_link, $self->[0]]; } package CovTypeSummaryCallback; # callback class to return total branches in each TLA category sub new { my ($class, $summary, $covType) = @_; defined($summary) or die("no summary"); die("$covType not supported yet") unless grep({ $_ == $covType } @SummaryInfo::covTypes); my $self = [$summary, $covType]; bless $self, $class; return $self; } sub get { my ($self, $key) = @_; return $self->[0]->get($key, $self->[1]); } sub owner { my $self = shift; # a call site is one line, so it has an owner - unlike a function, which # spans many lines with possibly many owners. die("CovTypeSummaryCallback::owner not supported for " . $self->[1]) unless ($self->[1] == SummaryInfo::BRANCH_DATA || $self->[1] == SummaryInfo::CALLGRAPH_DATA || $self->[1] == SummaryInfo::MCDC_DATA); return $self->[0]->owner(); } sub age { my $self = shift; return $self->[0]->age(); } sub bin { my $self = shift; return $self->[0]->bin(); } sub covType { my $self = shift; return $self->[1]; } package PrintCallback; # maintain some callback data from one line to the next use constant { FILE_INFO => 0, # SourceFile struct LINE_DATA => 1, # FileCoverageInfo struct TLA => 2, OWNER => 3, AGE => 4, NEXT_OWNER => 5, NEXT_AGE => 6, LINENO => 7, # the call sites of this file, keyed by source line, and the page # their groups are shown on - both undef unless '--callgraph' CALL_SITES => 8, CALLGRAPH_PAGE => 9, # the branch blocks and the MC/DC groups which are written at a # line other than the one which holds them - see 'locate' BRANCH_AT => 10, MCDC_AT => 11, }; sub new { my ($class, $sourceFileStruct, $lineCovInfo, $callSites, $callgraphPage) = @_; my $self = [$sourceFileStruct, $lineCovInfo, "", # current TLA "", # owner "", # age {}, # next header line for corresponding owner {}, # next header line for corresponding date bin undef, # line number $callSites, $callgraphPage, undef, # branch locations undef, # MC/DC locations ]; bless $self, $class; return $self; } sub _coverpointData { # the branch data (isMcdc == false) or the MC/DC data (isMcdc == true) # of line 'lineNo' shown in the source view and whether data is # differential my ($self, $isMcdc, $summary, $lineNo) = @_; if (defined($main::show_tla)) { my $lineData = $self->lineData()->line($lineNo); if (defined($lineData)) { my $data = $isMcdc ? $lineData->differential_mcdc() : $lineData->differential_branch(); return ($data, 1) if defined($data); } } return (defined($summary) ? $summary->value($lineNo) : undef, 0); } sub locate { # Find the branch blocks (isMcdc == false) or MC/DC groups (isMcdc == true) # in this file which are written at a line other than the one which # holds them - see BranchBlock::locationOffset and MCDC_Block::groupKey. # 'summary' is the file's summary BranchData or MCDC_Data. my ($self, $isMcdc, $summary) = @_; my %lines; $lines{$_} = 1 foreach (defined($summary) ? $summary->keylist() : ()); if (defined($main::show_tla)) { # ..and the lines whose data is only in the baseline my $lineMap = $self->lineData()->lineMap(); while (my ($line, $lineData) = each(%$lineMap)) { next if exists($lines{$line}) || '<' eq substr($line, 0, 1); $lines{$line} = 1 if defined($isMcdc ? $lineData->differential_mcdc() : $lineData->differential_branch()); } } my %at; # written line -> [[holding line, offset], ...] my %holds; # holding line -> 1 if some of its data is written elsewhere foreach my $line (sort({ $a <=> $b } keys(%lines))) { my ($data) = $self->_coverpointData($isMcdc, $summary, $line); next unless defined($data); my %offsets; $offsets{$_} = 1 foreach ($isMcdc ? $data->locationOffsets() : map({ $_->locationOffset() } $data->blocks())); delete($offsets{0}); next unless %offsets; $holds{$line} = 1; push(@{$at{$line + $_}}, [$line, $_]) foreach (sort({ $a <=> $b } keys(%offsets))); } $self->[$isMcdc ? MCDC_AT : BRANCH_AT] = [$summary, \%at, \%holds]; } sub coverpointsAt { # The branch (isMcdc == false) or MC/DC (isMcdc == true) data written at # line 'lineNo'. # Returns list of [data, offset, isDifferential], whose blocks or groups at # location 'offset' are shown there. An undef offset means "all of # them". 'summary' is the summary data of the line my ($self, $isMcdc, $lineNo, $summary) = @_; my $located = $self->[$isMcdc ? MCDC_AT : BRANCH_AT]; my @result; my ($data, $isDiff) = $self->_coverpointData($isMcdc, undef, $lineNo); $data = $summary unless $isDiff; if (defined($data)) { push(@result, [$data, (defined($located) && exists($located->[2]->{$lineNo})) ? 0 : undef, $isDiff ]); } if (defined($located) && exists($located->[1]->{$lineNo})) { foreach my $from (@{$located->[1]->{$lineNo}}) { my ($holder, $offset) = @$from; my ($d, $diff) = $self->_coverpointData($isMcdc, $located->[0], $holder); push(@result, [$d, $offset, $diff]) if defined($d); } } return @result; } sub callSites { # the call-site groups of this file, keyed by source line. An empty hash # rather than undef in a report which has no call data, so that the caller # can index it without checking my $self = shift; return defined($self->[CALL_SITES]) ? $self->[CALL_SITES] : {}; } sub callgraphPage { my $self = shift; return $self->[CALLGRAPH_PAGE]; } sub sourceDetail { my $self = shift; return $self->[FILE_INFO]; } sub lineData { my $self = shift; return $self->[LINE_DATA]; } sub lineNo { my ($self, $lineNo) = @_; $self->[LINENO] = $lineNo if defined($lineNo); return $self->[LINENO]; } sub tla { my ($self, $newTLA, $lineNo) = @_; # NOTE: 'undef' TLA means that this line is not code (it is a comment, # blank line, opening brace or something). # We return 'same' as previous line' in that case so the category # block can be larger (e.g., 1 CBC line, a 2 line comment, then 3 more # lines) can get just one label (first line). # This reduces visual clutter. # Note that the 'block finding' code has to do the same thing (else the # HTML links won't be generated correctly) if (defined($newTLA) && $newTLA ne $self->[TLA]) { $self->[TLA] = $newTLA; return $newTLA; } return " " x $main::tla_field_width; # same TLA as previous line. } sub age { my ($self, $newval, $lineNo) = @_; if (defined($newval) && $newval ne $self->[AGE]) { $self->[AGE] = $newval; return $newval; } return " " x $main::age_field_width; # same age as previous line. } sub owner { my ($self, $newval, $lineNo) = @_; if (defined($newval) && $newval ne $self->[OWNER]) { $self->[OWNER] = $newval; return $newval; } return " " x $main::owner_field_width; # same owner as previous line. } sub current { my ($self, $key) = @_; if ($key eq 'tla') { return $self->[TLA]; } elsif ($key eq 'owner') { return $self->[OWNER]; } elsif ($key eq 'age') { return $self->[AGE]; } else { ($key eq 'dateBucket') or die("unexpected key $key"); return SummaryInfo::findAgeBin($self->[AGE]); } } sub nextOwner { my ($self, $owner, $tla, $value) = @_; my $map = $self->[NEXT_OWNER]; my $key = $tla . ' ' . $owner; if (defined($value)) { $map->{$key} = $value; return $value; } return exists($map->{$key}) ? $map->{$key} : undef; } sub nextDate { my ($self, $date, $tla, $value) = @_; my $map = $self->[NEXT_AGE]; my $key = $tla . ' ' . $date; if (defined($value)) { $map->{$key} = $value; return $value; } return $map->{$key}; } package ReadBaselineSource; use base 'ReadCurrentSource'; sub new { my ($class, $diffData) = @_; my $self = $class->SUPER::new(); push(@$self, $diffData); return $self; } sub isRecoveredBaselineFile { my ($self, $filename) = @_; my $diffmap = $self->[1]; return 0 unless defined($diffmap); my @diff = $diffmap->baselineSource($filename); return scalar(@diff); } sub open { my ($self, $filename) = @_; my $diffmap = $self->[1]; my @diff = defined($diffmap) ? $diffmap->baselineSource($filename) : (); if (@diff) { # if there are any diffs, then need to load the current source, then # walk the diff to insert and remove changed lines. # '$current' is the name the current text lives under and '$key' the # name the diff data is filed under: neither is necessarily the # baseline's own name - see 'baselineSource' my ($key, $current) = @diff; my $currentSrc = $self->_load($current, 'baseline'); my $src = $diffmap->recreateBaseline($filename, $currentSrc, $key); return $self->parseLines($filename, $src); } # else no diff data here - just read the file return ReadCurrentSource::open($self, $filename, 'baseline'); } # one categorized call site - see 'FileCoverageInfo::_categorizeCallgraphCov'. # Unlike the other coverpoint types, there is no baseline/current/differential # triple: a site's identity is (line, column, ordinal, callee) and the two # counts fit in the one record, so the categorized record is all there is. package CallgraphSite; use constant { LINE => 0, COLUMN => 1, ORDINAL => 2, CALLEE => 3, # the function whose body contains this site. Kept because the # 'calls' table of a function detail page groups by it, and # because the containing function is not derivable from the line # once the line has been deleted CALLER => 4, BASELINE => 5, CURRENT => 6, TLA => 7, # the real 'current' FunctionEntry which contains this site, or # undef for a site only the baseline has. Kept so that 'set_tla' # can write the category through to the site record there, which # is where the 'called from' table of a function in some other # file reads it from - see '_categorizeCallgraphCov' ENTRY => 8, # the baseline line this site was read from, for a site only the # baseline has: 'LINE' is where it is displayed, which is the # current line the diff maps it to, and that is not the line the # baseline-side lookup has to be done with BASE_LINE => 9, # the file the call is *written* in, or undef when that is the file # the caller is defined in - which is the ordinary case. See # 'FNC:' in geninfo(1): a call in an '#include'd fragment is # written in the fragment and belongs to a function of the file # which includes it, so 'LINE' is a line of *this* file and not # of the file whose page displays the site FILE => 10, # ..and the line of the caller's own file this site is attached to, # which is 'LINE' for an ordinary site and the line the caller is # defined on for a site written in another file. A site is # counted by the file whose function contains it and is the # metric counted by FNCF and FNCH, see # TraceInfo::callgraph_found. The counting walks the file's # lines - so a site called from elewhere needs be registered # with a line in this file. # A site which appears on some middle line of a multiline # statement is attached to the multiline anchor. # 'LINE' is where the call is actually written, and is # where the hyperlink leads. HOME_LINE => 11, }; sub new { my $class = shift; # (line, column, ordinal, callee, caller, base_count, curr_count, tla, # entry, base_line, file, home_line) my @data = @_; my $self = \@data; bless $self, $class; return $self; } sub line { my $self = shift; return $self->[LINE]; } sub column { my $self = shift; return $self->[COLUMN]; } sub ordinal { my $self = shift; return $self->[ORDINAL]; } sub callee { my $self = shift; return $self->[CALLEE]; } sub caller { my $self = shift; return $self->[CALLER]; } sub base_line { # only a baseline-only site has one - see BASE_LINE my $self = shift; return $self->[BASE_LINE]; } sub file { # undef for a site written in the file its caller is defined in - see FILE my $self = shift; return $self->[FILE]; } sub home_line { my $self = shift; return $self->[HOME_LINE]; } sub base_count { my $self = shift; return $self->[BASELINE]; } sub set_base_count { my ($self, $count) = @_; $self->[BASELINE] = $count; } sub curr_count { my $self = shift; return $self->[CURRENT]; } sub tla { my $self = shift; return $self->[TLA]; } sub set_tla { my ($self, $tla) = @_; $self->[TLA] = $tla; $self->[ENTRY] ->setCallSiteTla($self->[LINE], $self->[COLUMN], $self->[ORDINAL], $self->[FILE], $tla) if defined($self->[ENTRY]); } package LineData; use constant { TYPE => 0, LINENO_BASE => 1, LINENO_CURRENT => 2, # location of this line in current data LINE_DATA => 3, BRANCH_DATA => 4, MCDC_DATA => 5, FUNCTION_DATA => 6, # list of CallgraphSite - the call sites displayed on this line, # which is not the same set as the sites of the function which # starts on this line CALLGRAPH_DATA => 7, # data elements in line data TLA => 0, LINE_BASELINE => 1, LINE_CURRENT => 2, # data elements for branch/function data DATA_BASELINE => 0, DATA_CURRENT => 1, DATA_DIFFERENTIAL => 2, }; sub new { my ($class, $type) = @_; # [ type, lineNo_base, lineNo_current, # bucket, base_count, curr_count <- line coverage count data # base_branch, curr_branch, differential_branch ] <- branch coverage count data # $type in ('insert', 'equal', 'delete') my $self = [$type, undef, undef, ['UNK', undef, undef], # line coverage data [], # branch coverage data [], # MCDC coverage data [], # function coverage [] ]; # call sites on this line bless $self, $class; return $self; } sub to_list { # used by script-level 'select' callback my $self = shift; my @rtn = ($self->[TYPE], [$self->tla(), $self->in_curr() ? $self->curr_count() : undef, $self->in_base() ? $self->base_count() : undef ]); # @todo perhaps visit MC/DC here too my $branch = $self->differential_branch(); if (defined($branch)) { my @data; push(@rtn, \@data); foreach my $block ($branch->blocks()) { my $elements = $block->elements(); my @b; push(@data, \@b); foreach my $br (@$elements) { my $tla = $br->tla(); my ($base_count, $curr_count) = $br->diff_count(); push(@b, [$tla, $curr_count, $base_count]); } } } else { $branch = $self->current_branch(); if (defined($branch)) { my @data; push(@rtn, \@data); foreach my $block ($branch->blocks()) { foreach my $br (@{$block->elements()}) { push(@data, $br->count()); } } } else { push(@rtn, undef); } } return \@rtn; } sub tla { my ($self, $tla) = @_; my $linecov = $self->[LINE_DATA]; $linecov->[TLA] = $tla if defined($tla); return $linecov->[TLA]; } sub type { my $self = shift; return $self->[TYPE]; } sub lineNo { my ($self, $which, $lineNo, $filename) = @_; my $loc; if ($which eq "current") { $loc = LINENO_CURRENT; } else { die("unknown key '$which' - should be 'base' or 'current'") unless $which eq "base"; $loc = LINENO_BASE; } lcovutil::ignorable_error( $lcovutil::ERROR_INCONSISTENT_DATA, "\"$filename\": inconsistent '$which' line location: " . $self->[$loc] . " -> $lineNo") if (defined($lineNo) && defined($self->[$loc]) && $self->[$loc] != $lineNo); $self->[$loc] = $lineNo if defined($lineNo); return $self->[$loc]; } sub in_base { # @return true or false: is this object present in the baseline? my $self = shift; # coverpoint is in baseline data if line coverage number is defined return defined($self->[LINE_DATA]->[LINE_BASELINE]); } sub in_curr { # @return true or false: is this object present in the current version? # storing negative number for 'current' location of deleted line - # first location above or below the deleted region my $self = shift; # coverpoint is in current data if line coverage number is defined # otherwise, the line may be present without coverage data associated # with it (say, excluded now - or possibly filtered out) return defined($self->[LINE_DATA]->[LINE_CURRENT]); } sub base_count { # return line hit count in baseline my ($self, $inc) = @_; die("non-zero count but not in base") if (defined($inc) && !defined($self->[LINENO_BASE])); my $linecov = $self->[LINE_DATA]; if (defined($inc)) { if (defined($linecov->[LINE_BASELINE])) { $linecov->[LINE_BASELINE] += $inc; } else { $linecov->[LINE_BASELINE] = $inc; } } return $linecov->[LINE_BASELINE]; } sub curr_count { # return line hit count in current my ($self, $inc) = @_; die("non-zero count but not in current") if (defined($inc) && (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0)); my $linecov = $self->[LINE_DATA]; if (defined($inc)) { if (defined($linecov->[LINE_CURRENT])) { $linecov->[LINE_CURRENT] += $inc; } else { $linecov->[LINE_CURRENT] = $inc; # first assignment; += undef warns } } return $linecov->[LINE_CURRENT]; } sub _mergeBranchData { my ($self, $loc, $branchData) = @_; my $branch = $self->[BRANCH_DATA]; die("unexpected $loc branch data") if defined($branch->[$loc]); $branch->[$loc] = Storable::dclone($branchData); } sub baseline_branch { my ($self, $branchData) = @_; die("has baseline branch data but not in baseline") if (defined($branchData) && !defined($self->[LINENO_BASE])); if (defined($branchData)) { $self->_mergeBranchData(DATA_BASELINE, $branchData); } my $branch = $self->[BRANCH_DATA]; return $branch->[DATA_BASELINE]; } sub current_branch { my ($self, $branchData) = @_; die("has current branch data but not in current") if (defined($branchData) && (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0)); if (defined($branchData)) { $self->_mergeBranchData(DATA_CURRENT, $branchData); } my $branch = $self->[BRANCH_DATA]; return $branch->[DATA_CURRENT]; } sub differential_branch { my ($self, $differential) = @_; my $branch = $self->[BRANCH_DATA]; if (defined($differential)) { $branch->[DATA_DIFFERENTIAL] = $differential; } return $branch->[DATA_DIFFERENTIAL]; } sub _mergeMcdcData { # this is nearly identical to 'function' implementation...maybe share my ($self, $loc, $mcdcData) = @_; my $mcdc = $self->[MCDC_DATA]; die("unexpected $loc MC/DC data") if defined($mcdc->[$loc]); $mcdc->[$loc] = Storable::dclone($mcdcData); } sub baseline_mcdc { my ($self, $mcdcData) = @_; die("has baseline MC/DC data but not in baseline") if (defined($mcdcData) && !defined($self->[LINENO_BASE])); if (defined($mcdcData)) { $self->_mergeMcdcData(DATA_BASELINE, $mcdcData); } my $mcdc = $self->[MCDC_DATA]; return $mcdc->[DATA_BASELINE]; } sub current_mcdc { my ($self, $mcdcData) = @_; die("has current MC/DC data but not in current") if (defined($mcdcData) && (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0)); if (defined($mcdcData)) { $self->_mergeMcdcData(DATA_CURRENT, $mcdcData); } my $mcdc = $self->[MCDC_DATA]; return $mcdc->[DATA_CURRENT]; } sub differential_mcdc { my ($self, $differential) = @_; my $mcdc = $self->[MCDC_DATA]; if (defined($differential)) { $mcdc->[DATA_DIFFERENTIAL] = $differential; } return $mcdc->[DATA_DIFFERENTIAL]; } sub _mergeFunctionData { my ($self, $loc, $functionData) = @_; die('expected FunctionEntry found ' . ref($functionData)) unless 'FunctionEntry' eq ref($functionData); my $function = $self->[FUNCTION_DATA]; die("unexpected $loc function data") if defined($function->[$loc]); # also clone hit count data $function->[$loc] = $functionData->cloneWithEndLine(1, 1); } sub baseline_function { my ($self, $functionData) = @_; die("has baseline function data but not in baseline") if (defined($functionData) && !defined($self->[LINENO_BASE])); if (defined($functionData)) { $self->_mergeFunctionData(DATA_BASELINE, $functionData); } my $function = $self->[FUNCTION_DATA]; return $function->[DATA_BASELINE]; } sub current_function { my ($self, $functionData) = @_; die("has current function data but not in current") if (defined($functionData) && (!defined($self->[LINENO_CURRENT]) || $self->[LINENO_CURRENT] < 0)); if (defined($functionData)) { $self->_mergeFunctionData(DATA_CURRENT, $functionData); } my $function = $self->[FUNCTION_DATA]; return $function->[DATA_CURRENT]; } sub differential_function { my ($self, $differential) = @_; my $function = $self->[FUNCTION_DATA]; if (defined($differential)) { $function->[DATA_DIFFERENTIAL] = $differential; } return $function->[DATA_DIFFERENTIAL]; } sub add_call_site { my ($self, $site) = @_; die('expected CallgraphSite found ' . ref($site)) unless 'CallgraphSite' eq ref($site); push(@{$self->[CALLGRAPH_DATA]}, $site); } sub call_sites { # the categorized call sites displayed on this line, in source order. The # empty list rather than undef when there are none: every caller of this # iterates my $self = shift; return @{$self->[CALLGRAPH_DATA]}; } sub num_call_sites { my $self = shift; return scalar(@{$self->[CALLGRAPH_DATA]}); } sub sort_call_sites { # sort into source order: by line, then column, then ordinal, then # callee name. # The line is the same for all of them unless some call is in the # middle of some multiline statement. The callee breaks the # tie between two baseline-only sites of different callers whose columns # happen to match. # A site may be attached to this line without being on it (see # CallgraphSite::HOME_LINE): one written on a later line of this # multiline statement sorts by its appearance line, and one written in # another file sorts after the line's own sites, by file and then by # its own line my $self = shift; @{$self->[CALLGRAPH_DATA]} = sort({ my $fileA = defined($a->file()) ? $a->file() : ''; my $fileB = defined($b->file()) ? $b->file() : ''; $fileA cmp $fileB or $a->line() <=> $b->line() or $a->column() <=> $b->column() or $a->ordinal() <=> $b->ordinal() or $a->callee() cmp $b->callee(); } @{$self->[CALLGRAPH_DATA]}); } # forward-declare some types package DiffMap; use constant { EQUAL => 0, INSERT => 1, DELETE => 2, }; our @type2str = ('equal', 'insert', 'delete'); # structure holding coverage data for a particular file: # - associated with a line number: # - line coverage # - branch coverage # - function coverage (not directly associated with line number package FileCoverageInfo; use constant { VERSION => 0, LINEMAP => 1, DELETED_LINE_LEADER => 2, FUNCTIONMAP => 3, # current line -> [first, last] line of the statement it is part # of, for the lines of the multi-line statements STATEMENTS => 4, }; sub new { my ($class, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; # [hash of lineNumber -> LineData struct, optional FunctionMap] my $self = [ [defined($base_data) ? $base_data->version() : undef, $current_data->version() ], {}, # the line data map {} # currentLineNo -> [deleted lines for which this is the leader] ]; bless $self, $class; $diffMap->show_map($filename) if ((defined($verbose) && $verbose) || (defined($lcovutil::verbose) && $lcovutil::verbose > 1)); $self->[STATEMENTS] = _statementMap($current_data); _foldBaselineStatements($filename, $base_data, $current_data, $diffMap); # the current lines of the statements which the diff changed my $changed = changedStatementLines($filename, $base_data, $current_data, $diffMap); # line coverage categorization includes date- and owner- bins in # the vanilla case when there is no baseline. my @args = ($filename, $base_data, $current_data, $diffMap, $verbose); $self->_categorizeLineCov(@args); $self->_categorizeBranchCov(@args) if ($lcovutil::br_coverage); $self->_categorizeMcdcCov(@args) if ($lcovutil::mcdc_coverage); $self->_recategorizeChangedStatements($changed, $verbose) if %$changed; $self->_categorizeFunctionCov(@args) if ($lcovutil::func_coverage); # after the function pass: a call site is contained in a function, and the # line it is on may have no coverpoint of any other kind, in which case the # function pass has already given that line its category $self->_categorizeCallgraphCov(@args, $changed) if ($lcovutil::callgraph_coverage); return $self; } sub version { my ($self, $which) = @_; return $which eq 'current' ? $self->[VERSION]->[1] : $self->[VERSION]->[0]; } sub lineMap { my $self = shift; return $self->[LINEMAP]; } sub functionMap { # simply a map of function leader name -> differential FunctionEntry my $self = shift; return (scalar(@$self) >= FUNCTIONMAP) ? $self->[FUNCTIONMAP] : undef; } sub line { my ($self, $lineNo) = @_; my $lineMap = $self->lineMap(); return exists($lineMap->{$lineNo}) ? $lineMap->{$lineNo} : undef; } sub deletedLineData { my ($self, $currentLineNumber) = @_; my $map = $self->[DELETED_LINE_LEADER]; return exists($map->{$currentLineNumber}) ? $map->{$currentLineNumber} : undef; } sub statement { # @return (first, last) line of the multi-line statement which current line # '$lineNo' is part of - or the empty list, if it is not part of one my ($self, $lineNo) = @_; my $stmt = $self->[STATEMENTS]->{$lineNo}; return defined($stmt) ? @$stmt : (); } sub statements { # hash of current line -> [first, last] - see 'statement' my $self = shift; return $self->[STATEMENTS]; } sub _statementMap { # line -> [first, last] for every line of every statement span of the # TraceInfo: the spans of the line coverpoints which it has my $traceInfo = shift; my %map; my $span = $traceInfo->span(); return \%map unless $span->entries(); my $lineCov = $traceInfo->sum(); foreach my $line ($span->keylist()) { next unless defined($lineCov->value($line)); my $stmt = [$line, $span->value($line)]; $map{$_} = $stmt foreach ($line .. $stmt->[1]); } return \%map; } sub _foldBaselineStatements { # The current data may have been captured with 'merge_continuation_lines' # and the baseline without: # - a multi-line 'current' statement will have a single coverpoint on # its first line - but the 'baseline' has a coverpoint on interior # lines as well. # - compared line by line, the continuation lines look deleted and the # parent function looks edited - even though the source code is # identical # View the baseline through the current statements first: # - the baseline line coverpoint of an unchanged continuation line # which has no coverpoint in the current data is folded into the # baseline line of the statement's first line, together with its # branch and MC/DC data - where the current data moved its data (see # TraceFile::foldContinuationLines). # - a line which starts a baseline function or a baseline statement # of its own is not modified. # A baseline captured with the 'merge_continuation_lines' option has # already been folded: its continuation lines have no coverpoints either. my ($filename, $base_data, $current_data, $diffMap) = @_; return unless defined($base_data); my $span = $current_data->span(); return unless $span->entries(); my $lineCov = $current_data->sum(); my $baseCov = $base_data->sum(); my $baseSpan = $base_data->span(); my $baseFunc = $base_data->func(); my $currBr = $lcovutil::br_coverage ? $current_data->sumbr() : undef; my $baseBr = $lcovutil::br_coverage ? $base_data->sumbr() : undef; my $currMcdc = $lcovutil::mcdc_coverage ? $current_data->mcdc() : undef; my $baseMcdc = $lcovutil::mcdc_coverage ? $base_data->mcdc() : undef; my $equal = sub { $diffMap->type($filename, $diffMap->NEW, $_[0]) == DiffMap::EQUAL; }; my %fold; foreach my $line ($span->keylist()) { next unless defined($lineCov->value($line)) && &$equal($line); my $to = $diffMap->lookup($filename, $diffMap->NEW, $line); foreach my $l ($line + 1 .. $span->value($line)) { next if defined($lineCov->value($l)) || !&$equal($l); my $from = $diffMap->lookup($filename, $diffMap->NEW, $l); next if ($from == $to || defined($baseSpan->value($from)) || (defined($baseFunc) && defined($baseFunc->findKey($from)))); my $branches = defined($baseBr) && defined($baseBr->value($from)) && !(defined($currBr) && defined($currBr->value($l))); my $conditions = defined($baseMcdc) && defined($baseMcdc->value($from)) && !(defined($currMcdc) && defined($currMcdc->value($l))); next unless defined($baseCov->value($from)) || $branches || $conditions; $fold{$from} = [$to, $branches, $conditions]; } } return unless %fold; my $one = 1 == scalar(keys(%fold)); lcovutil::info(2, "\"$filename\": baseline continuation line" . ($one ? ' ' : 's ') . join(',', sort({ $a <=> $b } keys(%fold))) . ' folded into ' . ($one ? 'its statement line' : 'their statement lines') . "\n"); TraceFile::foldContinuationLines($base_data, \%fold); } sub changedStatementLines { # The current lines of the multi-line statements which the diff changed, as # a hash of line -> 1. A statement is changed if any of its current lines # is inserted, or if any line of the baseline statement it maps to was # deleted - a changed line is both. Every coverpoint written on one of # these lines - line, branch, MC/DC and call site - is then new code. # The statement is the segment the coverpoint is in: a statement which # 'merge_continuation_lines' partitioned is several independent segments # because it couldn't figure out how to merge them consistently. # If the baseline data has no span anywhere, then it was captured without # continuation lines, and a baseline coverpoint represents the current # statement which starts on the line it maps to - whatever the extent of # that statement was in the baseline. # The (multi-line) statement is also marked 'changed' if a baseline line # between two of its 'current' lines was deleted. # If the baseline has a span anywhere then it was captured with # 'merge_continuation_lines' - so all spans have been identified: # in that case, a baseline statement without a span is a single line. # Without a baseline, every coverpoint is new code. my ($filename, $base_data, $current_data, $diffMap) = @_; my %changed; return \%changed unless defined($base_data); my $currSpan = $current_data->span(); my $baseSpan = $base_data->span(); return \%changed unless ($currSpan->entries() || $baseSpan->entries()); my $current = _statementMap($current_data); # mark the current statement which line '$line' is in - or the line alone, # if it is in none my $mark = sub { my $line = shift; my $stmt = $current->{$line}; if (defined($stmt)) { $changed{$_} = 1 foreach ($stmt->[0] .. $stmt->[1]); } else { $changed{$line} = 1; } }; my $lineCov = $current_data->sum(); foreach my $line ($currSpan->keylist()) { next unless defined($lineCov->value($line)); my $end = $currSpan->value($line); my $edited = grep( { $diffMap->type($filename, $diffMap->NEW, $_) == DiffMap::INSERT } ($line .. $end)); unless ($edited || $main::baseline_has_spans) { my $from = $diffMap->lookup($filename, $diffMap->NEW, $line); my $to = $diffMap->lookup($filename, $diffMap->NEW, $end); $edited = grep({ $diffMap->type($filename, $diffMap->OLD, $_) == DiffMap::DELETE } ($from .. $to)); } &$mark($line) if $edited; } my $baseCov = $base_data->sum(); foreach my $bline ($baseSpan->keylist()) { next unless defined($baseCov->value($bline)); my $deleted = 0; my @surviving; foreach my $l ($bline .. $baseSpan->value($bline)) { if ($diffMap->type($filename, $diffMap->OLD, $l) == DiffMap::DELETE) { $deleted = 1; } else { push(@surviving, $diffMap->lookup($filename, $diffMap->OLD, $l)); } } next unless $deleted; &$mark($_) foreach (@surviving); } return \%changed; } # the category of a coverpoint of a changed statement our %changedStatementTla = ('UBC' => 'UNC', 'UIC' => 'UNC', 'LBC' => 'UNC', 'GBC' => 'GNC', 'CBC' => 'GNC', 'GIC' => 'GNC',); sub _recategorizeChangedStatements { # the line, branch and MC/DC coverpoints of the changed statements are new # code - see 'changedStatementLines'. Call sites are categorized in # 'categorizeCallSites' my ($self, $changed, $verbose) = @_; my $lineMap = $self->lineMap(); foreach my $line (keys(%$changed)) { my $data = $lineMap->{$line}; next unless defined($data); my $tla = $data->tla(); if (exists($changedStatementTla{$tla})) { $data->tla($changedStatementTla{$tla}); print(" changed statement $line $tla -> " . $data->tla() . "\n") if $verbose; } if ($lcovutil::br_coverage) { my $br = $data->differential_branch(); if (defined($br)) { foreach my $block ($br->blocks()) { foreach my $b (@{$block->elements()}) { my $t = $b->tla(); $b->set_tla($changedStatementTla{$t}) if exists($changedStatementTla{$t}); } } } } if ($lcovutil::mcdc_coverage) { my $mcdc = $data->differential_mcdc(); if (defined($mcdc)) { # groups() materializes a fresh hash on each call - see # 'recategorizeTlaAsBaseline' my $groups = $mcdc->groups(); foreach my $group (values(%$groups)) { foreach my $cond (@$group) { foreach my $sense (0, 1) { my $t = $cond->count($sense)->[0]; $cond->set_tla($sense, $changedStatementTla{$t}) if exists($changedStatementTla{$t}); } } } } } } } sub recategorizeTlaAsBaseline { # intended use: this file appears to have been added to the "coverage" # suite - but the file itself is old/has been around for a long time. # - by default, we will see this as "Included Code" # - which means that 'un-exercised' code will be "UIC" # - but: non-zero UIC will fail our Jenkins coverage ratchet. # As a workaround: treat this file as if the baseline data was the same # as 'current' - so code will be categorized as "CBC/UBC" - which will not # trigger the coverage criteria. my $self = shift; my $lineMap = $self->lineMap(); my %remap = ('UIC' => 'UBC', 'GIC' => 'CBC'); while (my ($line, $data) = each(%$lineMap)) { die("unexpected $line 'in_base'") if $data->in_base(); my $lineTla = $data->tla(); if (exists($remap{$lineTla})) { # don't remap GNC, UNC, etc $data->tla($remap{$lineTla}); } # branch coverage... if ($lcovutil::br_coverage && defined($data->differential_branch())) { my $br = $data->differential_branch(); foreach my $block ($br->blocks()) { my $diff = $block->elements(); foreach my $b (@$diff) { my $tla = $b->tla(); if (exists($remap{$tla})) { $b->set_tla($remap{$tla}); } } } } # if branch data # MC/DC coverage... if ($lcovutil::mcdc_coverage && defined($data->differential_mcdc())) { my $mcdc = $data->differential_mcdc(); # groups() materializes a fresh hash on each call, so hold it in a # lexical: each() over a new hash restarts every iteration. my $mcdcGroups = $mcdc->groups(); while (my ($groupSize, $group) = each(%$mcdcGroups)) { foreach my $cond (@$group) { # remap both the true and false sense.. foreach my $sense (0, 1) { my $tla = $cond->count($sense)->[0]; if (exists($remap{$tla})) { # write it through the mutator, as the branch loop # above does: count() hands back a copy of the # differential triple in the XS backend, so # assigning to its TLA slot changes nothing there $cond->set_tla($sense, $remap{$tla}); } } } } } # if MCDC data # function coverage.. if ($lcovutil::func_coverage && defined($data->differential_function())) { my $func = $data->differential_function(); # through the mutator, as the branch and MC/DC loops above are: # 'hit()' hands back a copy of the differential pair in the XS # backend, so assigning to its TLA slot changes nothing there my $tla = $func->tla(); if (exists($remap{$tla})) { $func->set_tla($remap{$tla}); } foreach my $alias ($func->aliasNames()) { my $data = $func->alias($alias); my $tla = $data->tla(); if (exists($remap{$tla})) { $data->set_tla($remap{$tla}); } } } # if function data # callgraph coverage.. # This remap reaches the file's own bins and its own pages - which is # what the coverage ratchet this option exists for looks at. It does # not reach the 'callers of this function' table on the page of a # *callee* in some other file: that table shows the category which # 'main::categorize_all_call_sites' computed in the parent, because the # file it belongs to may be built in a different process, and this # decision cannot be made in the parent - it needs the file's # annotation, which is what the children are forked to do. So a site # in a file this option rewrites is 'CBC' where its own file displays # it and 'GIC' where its callee does if ($lcovutil::callgraph_coverage) { foreach my $site ($data->call_sites()) { my $tla = $site->tla(); $site->set_tla($remap{$tla}) if exists($remap{$tla}); } } } } sub _categorize { my ($baseCount, $currCount, $excluded) = @_; my $tla; if (0 == $baseCount) { $tla = (0 == $currCount) ? "UBC" : "GBC"; } elsif (0 == $currCount) { $tla = "LBC"; } else { $tla = "CBC"; } if (defined($excluded) && $excluded) { return $currCount == 0 ? 'EUC' : 'ECC'; } return $tla; } sub _categorizeIfExcluded { my ($count, $excluded, $missTla, $hitTla) = @_; unless ($excluded) { return $count == 0 ? $missTla : $hitTla; } else { return $count == 0 ? 'EUC' : 'ECC'; } } sub _findLineData { my ($self, $diffMap, $filename, $base_lineNo) = @_; my $current_lineNo = $diffMap->lookup($filename, $diffMap->OLD, $base_lineNo); my $type = $diffMap->type($filename, $diffMap->OLD, $base_lineNo); my $lineDataMap = $self->lineMap(); my $linedata; if ($type != DiffMap::DELETE) { if (!defined($lineDataMap->{$current_lineNo})) { $linedata = LineData->new($type); $lineDataMap->{$current_lineNo} = $linedata; $linedata->lineNo('current', $current_lineNo, $filename); } else { $linedata = $lineDataMap->{$current_lineNo}; } $linedata->lineNo('base', $base_lineNo, $filename); } else { # nothing walks the keylist so a prefix is sufficient to distinguish # records that should be summarized but not displayed my $dline = "<<<" . $base_lineNo; if (!exists($lineDataMap->{$dline})) { $linedata = LineData->new($type); $linedata->lineNo('base', $base_lineNo, $filename); $lineDataMap->{$dline} = $linedata; # look up and/or down to find the first baseline line # which is not deleted - and store that as the corresponding # 'current' line. # this way, we can know the extents of the deleted region my $c = $diffMap->find_deleted_line_leader($filename, $base_lineNo); $linedata->lineNo('current', -$c, $filename); } else { $linedata = $lineDataMap->{$dline}; } } return $linedata; } # categorize line coverage numbers sub _categorizeLineCov { my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; my $lineDataMap = $self->lineMap(); print("categorize lines $filename\n") if ($verbose); # $lineCovBase, $lineCovCurrent are CountData objects my $lineCovBase; $lineCovBase = $base_data->sum() if defined($base_data); my $lineCovCurrent = $current_data->sum(); if ($lcovutil::check_data_consistency) { # walk the branch coverpoints to check for data consistency: # - we expect a line coverpoint in every location which has branches # - if not found, the generate message and/or create a fake coverpoint # LLVM seems to like to generate inconsistent data. my $branchCurrent = $current_data->sumbr(); foreach my $line ($branchCurrent->keylist()) { # just ignore bogus data - we already warned when we read the data next if ($line <= 0); my $type = $diffMap->type($filename, $diffMap->NEW, $line); if ($type == DiffMap::DELETE) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "'current' line $filename:$line should not be marked 'delete'" ); delete($branchCurrent->{$line}); next; # LCOV_UNREACHABLE_STOP } } } # it is sufficient to just walk the 'global' (merged) line # coverage dataset because we only care/we only show total # coverage - not changed coverage per testcase. # (This observation is also true for branch and function # coverages.) foreach my $line ($lineCovCurrent->keylist()) { # just ignore bogus data - we already warned when we read the data next if ($line <= 0); my $type = $diffMap->type($filename, $diffMap->NEW, $line); if ($type == DiffMap::DELETE) { # can happen in some inconsistent case, when there are certain # out-of-range references in a file which contained diffs - and we # ignored the error check lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "'current' line $filename:$line should not be marked 'delete'"); $lineCovCurrent->remove($line); next; } my $linedata; if (!exists($lineDataMap->{$line})) { $linedata = LineData->new($type); $lineDataMap->{$line} = $linedata; } else { $linedata = $lineDataMap->{$line}; } # @todo support exclusion markers for linecov?? my $val = $lineCovCurrent->value($line); $linedata->lineNo("current", $line, $filename); $linedata->curr_count($val); $linedata->tla($val == 0 ? 'UNC' : 'GNC') if (!defined($lineCovBase)); } return unless (defined($lineCovBase)); foreach my $bline ($lineCovBase->keylist()) { # just ignore bogus data - we already warned when we read the data next if ($bline <= 0); my $linedata = $self->_findLineData($diffMap, $filename, $bline); my $val = $lineCovBase->value($bline); $linedata->base_count($val); } # walk the diff data here to mark deleted regions so we can highlight # them in the source view my $deletedLineData = $diffMap->compute_deleted_lines($filename); $self->[DELETED_LINE_LEADER] = $deletedLineData if $deletedLineData; if ($verbose) { print(" line data map:\n"); foreach my $line (sort keys %$lineDataMap) { my $data = $lineDataMap->{$line}; print(" $line: ", $data->type(), ' curr:', $data->in_curr() ? $data->lineNo('current') : '-', ' base:', $data->in_base() ? $data->lineNo('base') : '-', "\n"); } } foreach my $line (sort keys %$lineDataMap) { my $linedata = $lineDataMap->{$line}; my $tla; if ($linedata->type() == DiffMap::INSERT) { if (!$linedata->in_curr()) { # can get here if the 'diff' file is wrong with respect to # baseline vs. current coverage data - e.g., showing that # an unchanged line has a difference lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "$filename:$line: 'diff' data claims this line is inserted but it is not in 'current' coverage data" ); next; } $tla = ($linedata->curr_count() > 0) ? "GNC" : "UNC"; print(" insert $line $tla\n") if ($verbose); } elsif ($linedata->type() == DiffMap::DELETE) { if (!$linedata->in_base()) { # similarly: can get here if the diff vs baseline/current data # is inconsistent. lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "$filename:$line: 'diff' data claims this line is deleted but it is not in 'baseline' coverage data" ); next; } $tla = ($linedata->base_count() > 0) ? "DCB" : "DUB"; print(" delete $line $tla\n") if ($verbose); } else { die("FileCoverageInfo:: deleted segment line=$line file=$filename") unless $linedata->type() == DiffMap::EQUAL; if ($linedata->in_base() && $linedata->in_curr()) { $tla = _categorize($linedata->base_count(), $linedata->curr_count()); } elsif ($linedata->in_base()) { $tla = ($linedata->base_count() > 0) ? "ECB" : "EUB"; $linedata->tla($tla); } else { die("FileCoverageInfo:: non-executed line line=$line file=$filename" ) unless $linedata->in_curr(); $tla = ($linedata->curr_count() > 0) ? "GIC" : "UIC"; } print(" equal $line $tla in:" . ($linedata->in_base() ? ' base' : '') . ($linedata->in_curr() ? ' curr' : '') . "\n") if ($verbose); } $linedata->tla($tla); } } sub cloneBlock { my ($cloneInto, $cloneFrom, $missTla, $hitTla, $categorizeExcluded) = @_; # same location: it is part of the identity of the block $cloneInto->setLocationOffset($cloneFrom->locationOffset()); foreach my $br (@{$cloneFrom->elements()}) { my $count = $br->count(); my $tla = _categorizeIfExcluded($count, $categorizeExcluded && $br->is_excluded(), $missTla, $hitTla); $br->set_differential($tla, undef, $count); # undef == "not in baseline" $cloneInto->appendElement($br); } } sub _cloneBranchLocation { my ($cloneInto, $cloneFrom, $missTla, $hitTla, $categorizeExcluded) = @_; foreach my $code ($cloneFrom->codes(1)) { foreach my $from (@{$cloneFrom->getList($code)}) { my $to = BranchBlock->new(); cloneBlock($to, $from, $missTla, $hitTla, $categorizeExcluded); $cloneInto->insertBlock($to); } } } # categorize branch coverage numbers sub _categorizeBranchCov { my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; my $lineDataMap = $self->lineMap(); my $branchBaseline; $branchBaseline = $base_data->sumbr() if defined($base_data); my $branchCurrent = $current_data->sumbr(); my %branchCovLines; # look through the 'current' data, to find all the branch data # keep track of hit count in baseline, current - in element 2 of the # retained branch data. These counts are useful - for example, to report # the hit count in the baseline for LBC branches: # - e.g., in random testing: is this branch lost because it was a # low probability event (..baselinecount is a small number)> # Or because we did something bad and no longer reach what had been # a high probability event? foreach my $line ($branchCurrent->keylist()) { next if ($line <= 0); # ignore bogus unless (exists($lineDataMap->{$line})) { # unless ignored, should have been caught or fixed during # TraceInfo::_checkConsistency lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "\"$filename\":$line line has branchcov but no linecov data (skipping)." ); next; } my $data = $lineDataMap->{$line}; $branchCovLines{$line} = 1; # we expect that the line number matches... $data->lineNo("current", $line, $filename); # append this branch data for the line my $currBranchData = $branchCurrent->value($line); $data->current_branch($currBranchData); if (!defined($branchBaseline)) { my $categorized = BranchLocation->new($line); $data->differential_branch($categorized); die("no branch data for deleted $filename:$line") unless defined($currBranchData); _cloneBranchLocation($categorized, $currBranchData, 'UNC', 'GNC', 1); } } # foreach line in 'current' branch data return unless defined($branchBaseline); # now look through the baseline to find matching data foreach my $base_line ($branchBaseline->keylist()) { my $data = $self->_findLineData($diffMap, $filename, $base_line); my $curr_line = $data->lineNo('current'); my $type = $data->type(); if ($type != DiffMap::DELETE) { $branchCovLines{$curr_line} = 1; } else { # the line has been deleted...just record the data my $deleteKey = "<<<" . $base_line; $branchCovLines{$deleteKey} = 1; } my $baseBranchData = $branchBaseline->value($base_line); $data->baseline_branch($baseBranchData); } # foreach line in baseline data # go through all the branch data for each line, and categorize everything foreach my $line (keys(%branchCovLines)) { my $data = $self->lineMap()->{$line}; my $type = $data->type(); my $curr = $data->current_branch(); my $base = $data->baseline_branch(); my $categorized = BranchLocation->new($line); $data->differential_branch($categorized); if ($type == DiffMap::INSERT) { # can get here if diff data vs baseline/current is not consistent lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "baseline branch data should not be defined for inserted line $filename:$line" ) if defined($base); if (defined($curr)) { _cloneBranchLocation($categorized, $curr, 'UNC', 'GNC', 1); } else { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "current branch data should not be defined for inserted line $filename:$line" ); # LCOV_UNREACHABLE_STOP } } elsif ($type == DiffMap::DELETE) { # similarly: get here if diff data vs baseline/current is not consistent lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "current branch data should not be defined for deleted line $filename:$line" ) if defined($curr); if (defined($base)) { _cloneBranchLocation($categorized, $base, 'DUB', 'DCB'); } else { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "baseline branch data should not be defined for deleted line $filename:$line" ); # LCOV_UNREACHABLE_STOP } } else { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected branch coverage type $type at $filename:$line") if $type != DiffMap::EQUAL; # go through all the codes baseline - handle whether in # current or not my $numCurr; $numCurr = $curr->numBlocks() if defined($curr); # sort the set of codes - so blocks get created in an easily # explained order my @baselineCodes; @baselineCodes = $base->codes(1) if defined($base); foreach my $code (@baselineCodes) { my $baseList = $base->getList($code); if (defined($curr) && $curr->containsCode($code)) { my $currList = $curr->getList($code); my $blkIdx = 0; foreach my $baseBlock (@$baseList) { my $catBlock = BranchBlock->new(); # same code, so the same location as the current block $catBlock->setLocationOffset( $baseBlock->locationOffset()); if ($blkIdx <= $#$currList) { my $currBlock = $currList->[$blkIdx++]; my $elemIdx = 0; foreach my $base_br (@{$baseBlock->elements()}) { my $curr_br = $currBlock->getElement($elemIdx++); my $baseCount = $base_br->count(); my $currCount = $curr_br->count(); my $tla = _categorize($baseCount, $currCount, $curr_br->is_excluded()); $curr_br->set_differential($tla, $baseCount, $currCount); $catBlock->appendElement($curr_br); } } else { # this block not in current - clone and mark # that it was deleted cloneBlock($catBlock, $baseBlock, 'DUB', 'DCB'); } $categorized->insertBlock($catBlock); } # now handle any blocks that are in current but not base while ($blkIdx <= $#$currList) { my $catBlock = BranchBlock->new(); my $currBlock = $currList->[$blkIdx++]; cloneBlock($catBlock, $currBlock, 'UIC', 'GIC', 1); $categorized->insertBlock($catBlock); } } else { # this code not in current - so clone/mark as deleted foreach my $baseBlock (@$baseList) { my $b = BranchBlock->new(); cloneBlock($b, $baseBlock, 'DUB', 'DCB'); $categorized->insertBlock($b); } } } # now find all the codes in current that were not in base.. if (defined($curr)) { foreach my $code ($curr->codes(1)) { next if defined($base) && $base->containsCode($code); foreach my $currBlock (@{$curr->getList($code)}) { my $b = BranchBlock->new(); cloneBlock($b, $currBlock, 'UIC', 'GIC', 1); $categorized->insertBlock($b); } } } } } } sub _cloneMcdcEntry { my ($cloneInto, $cloneFrom, $missTla, $hitTla, $categorizeExcluded) = @_; # see the comment at the first each(%$...Groups) loop: groups() is not # cached, so it has to be hoisted out of the each(). my $fromGroups = $cloneFrom->groups(); while (my ($groupKey, $group) = each(%$fromGroups)) { foreach my $expr (@$group) { foreach my $sense (0, 1) { my $count = $expr->count($sense); my $excluded = $categorizeExcluded && $expr->is_excluded($sense); my $tla = _categorizeIfExcluded($count, $excluded, $missTla, $hitTla); $count = [$tla, undef, $count] ; # [TLA, baseline value, current value] $cloneInto->insertExpr('unknownFile', $groupKey, $sense, $count, $expr->index(), $expr->expression(), $excluded); } } } } sub _categorizeMcdcCov { my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; my $lineDataMap = $self->lineMap(); my $mcdcBaseline; $mcdcBaseline = $base_data->mcdc() if defined($base_data); my $mcdcCurrent = $current_data->mcdc(); my %mcdcCovLines; # look through the 'current' data, to find all the MC/DC data # keep track of hit count in baseline, current - in element 2 of the # retained MC/DC data. These counts are useful - for example, to report # the hit count in the baseline for LBC elements: # - e.g., in random testing: is this branch lost because it was a # low probability event (..baseline count is a small number)> # Or because we did something bad and no longer reach what had been # a high probability event? foreach my $line ($mcdcCurrent->keylist()) { next if ($line <= 0); # ignore bogus unless (exists($lineDataMap->{$line})) { # unless ignored, should have been caught or fixed during # TraceInfo::_checkConsistency lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "\"$filename\":$line line has MC/DC but no linecov data (skipping)." ); next; } my $data = $lineDataMap->{$line}; $mcdcCovLines{$line} = 1; # we expect that the line number matches... $data->lineNo("current", $line, $filename); # append this MC/DC data for the line (this is an MCDC_Block) my $currMcdcData = $mcdcCurrent->value($line); $data->current_mcdc($currMcdcData); if (!defined($mcdcBaseline)) { my $categorized = MCDC_Block->new($line); $data->differential_mcdc($categorized); _cloneMcdcEntry($categorized, $currMcdcData, 'UNC', 'GNC', 1); } } # foreach line in 'current' branch data return unless defined($mcdcBaseline); # now look through the baseline to find matching data foreach my $base_line ($mcdcBaseline->keylist()) { next if ($base_line <= 0); # ignore bogus my $data = $self->_findLineData($diffMap, $filename, $base_line); my $curr_line = $data->lineNo('current'); my $type = $data->type(); if ($type != DiffMap::DELETE) { $mcdcCovLines{$curr_line} = 1; } else { # the line has been deleted...just record the data my $deleteKey = "<<<" . $base_line; $mcdcCovLines{$deleteKey} = 1; } my $baseMcdcData = $mcdcBaseline->value($base_line); $data->baseline_mcdc($baseMcdcData); } # foreach line in baseline data # go through all the MC/DC data for each line, and categorize everything foreach my $line (keys(%mcdcCovLines)) { my $data = $self->lineMap()->{$line}; my $type = $data->type(); my $curr = $data->current_mcdc(); my $base = $data->baseline_mcdc(); my $categorized = MCDC_Block->new($line); $data->differential_mcdc($categorized); # handle case that baseline and/or current do not contain MC/DC data my $currGroups = defined($curr) ? $curr->groups() : (); my $baseGroups = defined($base) ? $base->groups() : (); if ($type == DiffMap::INSERT) { # can get here if diff data vs baseline/current is not consistent lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "baseline MC/DC data should not be defined for inserted line $filename:$line" ) if defined($base); _cloneMcdcEntry($categorized, $curr, 'UNC', 'GNC', 1); } elsif ($type == DiffMap::DELETE) { # similarly: get here if diff data vs baseline/current is not consistent lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "current MC/DC data should not be defined for deleted line $filename:$line" ) if defined($curr); _cloneMcdcEntry($categorized, $base, 'DUB', 'DCB'); } else { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected MC/DC coverage type $type at $filename:$line") if $type != DiffMap::EQUAL; # group might or might not be in both baseline and current while (my ($groupKey, $bgroup) = each(%$baseGroups)) { if (defined($currGroups) && exists($currGroups->{$groupKey})) { my $cgroup = $currGroups->{$groupKey}; for (my $i = 0; $i < scalar(@$bgroup); ++$i) { my $b = $bgroup->[$i]; my $c = $cgroup->[$i]; die("mismatched expressions") unless $b->expression() eq $c->expression(); foreach my $sense (0, 1) { my $b_count = $b->count($sense); my $c_count = $c->count($sense); my $excluded = $c->is_excluded($sense); my $tla = _categorize($b_count, $c_count, $excluded); $categorized->insertExpr( 'unknownFile', $groupKey, $sense, [$tla, $b_count, $c_count], $i, $c->expression(), $excluded); } } } else { # not found in current foreach my $b (@$bgroup) { foreach my $sense (0, 1) { my $b_count = $b->count($sense); my $tla = $b_count ? 'ECB' : 'EUB'; $categorized->insertExpr('unknownFile', $groupKey, $sense, [$tla, $b_count, undef], $b->index(), $b->expression()); } } } } #foreach group in base # ..and now the groups which are in 'current' but not in the # baseline - which is every group of a file that is not in the # baseline at all. Without this, the coverpoints are dropped: # the 'MCF/MCH' totals count them but no TLA bin does, and the # source view shows no MC/DC data for the line. # See the corresponding loop in '_categorizeBranchCov'. if (defined($currGroups)) { while (my ($groupKey, $cgroup) = each(%$currGroups)) { next if (defined($baseGroups) && exists($baseGroups->{$groupKey})); foreach my $c (@$cgroup) { foreach my $sense (0, 1) { my $c_count = $c->count($sense); my $excluded = $c->is_excluded($sense); my $tla = _categorizeIfExcluded($c_count, $excluded, 'UIC', 'GIC'); $categorized->insertExpr( 'unknownFile', $groupKey, $sense, [$tla, undef, $c_count], $c->index(), $c->expression(), $excluded); } } } } } # endif deleted line } # foreach line } sub _cloneFunctionEntry { my ($cloneInto, $cloneFrom, $missTla, $hitTla) = @_; my $hit = $cloneFrom->hit(); my $tla = (0 == $hit) ? $missTla : $hitTla; $cloneInto->setCountDifferential([$hit, $tla]); foreach my $alias ($cloneFrom->aliasNames()) { $hit = $cloneFrom->alias($alias)->count(); $tla = (0 == $hit) ? $missTla : $hitTla; $cloneInto->addAliasDifferential($alias, [$hit, $tla]); # ..and the names it represents - see _mergedNameNote $cloneInto->alias($alias) ->add_merged(%{$cloneFrom->alias($alias)->merged() // {}}); } } sub _categorizeFunctionCov { my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; die("map should not be defined yet") unless !defined($self->functionMap()); my $differentialMap = {}; $self->[FUNCTIONMAP] = $differentialMap; my $lineDataMap = $self->lineMap(); my $funcBase; $funcBase = $base_data->func() if defined($base_data); my $funcCurrent = $current_data->func(); # use merged line coverage to categorize function by checking for # edits in function range # $lineCovBase and $lineCovCurrent are 'CountData' objects # - of lineNo->hit count my $lineCovBase; $lineCovBase = $base_data->sum() if defined($base_data); my $lineCovCurrent = $current_data->sum(); # the lines of the multi-line statements of each version: a line of one # of them is code, though only its first line has a coverpoint my $stmtCurrent = $self->[STATEMENTS]; my $stmtBase = defined($base_data) ? _statementMap($base_data) : {}; my %funcCovLines; foreach my $key ($funcCurrent->keylist()) { my $func = $funcCurrent->findKey($key); my $line = $func->line(); next if ($line <= 0); # ignore bogus my $type = $diffMap->type($filename, $diffMap->NEW, $line); $funcCovLines{$line} = 1; lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "'current' line $filename:$line should not be marked 'delete'") if $type == DiffMap::DELETE; my $data; if (!exists($lineDataMap->{$line})) { $data = LineData->new($type); $lineDataMap->{$line} = $data; } else { $data = $lineDataMap->{$line}; lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "type mismatch " . $data->type() . " -> $type for $filename:$line" ) unless $data->type() == $type; } # we expect that the line number matches... $data->lineNo("current", $line, $filename); # function data for the line $data->current_function($func); if (!defined($funcBase)) { my $name = $func->name(); my $categorized = FunctionEntry->new($name, $funcCurrent, $line, $func->end_line()); $differentialMap->{$name} = $categorized; $data->differential_function($categorized); _cloneFunctionEntry($categorized, $func, 'UNC', 'GNC'); if ('UNK' eq $data->tla()) { # There is a function here but no line coverage data for the # line it starts on, so '_categorizeLineCov' (which ran # before this) left the line's category at its initial 'UNK' # - and 'UNK' is not a key of '%SummaryInfo::tlaLocation', so # counting the line later is an ERROR_UNKNOWN_CATEGORY. Take # the category from the function, exactly as the baseline path # does at the end of this function. Reachable input: a gcov # which does not emit function end lines leaves lcov to derive # them, and an inline or template function then arrives as an # 'FN:' record on a line with no 'DA:' record. # The count is left alone: 'in_curr' is false for a line with # no line coverage data, which is the only case this arm can # see, and the baseline path likewise sets a count only when # the line is in 'current'. my ($hit, $funcTla) = @{$categorized->hit()}; $data->tla($funcTla); $data->curr_count($hit) if $data->in_curr(); } } } # foreach function in current data return unless (defined($funcBase)); # A function is matched to the one which starts at the same line of the # other version. Two compilers might disagree on which line of an # unchanged declaration is the first line of the function - say, # the return type or the declarator - and then no start line matches. # Thus a baseline function which starts on an unchanged line where the # current version doesn't have a function is paired with a current # function of the same kind: # - it starts on an unchanged line where no baseline function starts, # - has a alias in common, and # - has a range which overlaps the baseline function's range in # current line numbers. # The closest start wins. # Whether its body changed is then decided from the diff - the same as # any other pair my %baseStart; # current line -> 1 if a baseline function maps there my @unpaired; # [baseline function, current start, current end] foreach my $key ($funcBase->keylist()) { my $func = $funcBase->findKey($key); my $line = $func->line(); next if ($line <= 0); next if $diffMap->type($filename, $diffMap->OLD, $line) != DiffMap::EQUAL; my $curr_line = $diffMap->lookup($filename, $diffMap->OLD, $line); $baseStart{$curr_line} = 1; my $end = $func->end_line(); next if (!defined($end) || (exists($lineDataMap->{$curr_line}) && defined($lineDataMap->{$curr_line}->current_function()))); push(@unpaired, [$func, $curr_line, $diffMap->lookup($filename, $diffMap->OLD, $end) ]); } my %paired; # baseline function start line -> current LineData if (@unpaired) { my @candidates; foreach my $key ($funcCurrent->keylist()) { my $func = $funcCurrent->findKey($key); my $line = $func->line(); next if ($line <= 0 || exists($baseStart{$line}) || !defined($func->end_line()) || $diffMap->type($filename, $diffMap->NEW, $line) != DiffMap::EQUAL); push(@candidates, [$func, {map({ $_ => 1 } $func->aliasNames())}]); } foreach my $u (sort { $a->[1] <=> $b->[1] } @unpaired) { my ($func, $start, $end) = @$u; my $best; foreach my $c (@candidates) { next unless defined($c); my $curr = $c->[0]; next if ($curr->line() > $end || $curr->end_line() < $start || !grep({ exists($c->[1]->{$_}) } $func->aliasNames())); $best = $c if (!defined($best) || abs($curr->line() - $start) < abs($best->[0]->line() - $start)); } next unless defined($best); my $curr = $best->[0]; $paired{$func->line()} = $lineDataMap->{$curr->line()}; @candidates = map({ (defined($_) && $_ != $best) ? $_ : undef } @candidates); lcovutil::info(1, $func->name() . " at $filename:" . $func->line() . " is paired with the function at line " . $curr->line() . "\n"); } } # look through the baseline to find matching data foreach my $key ($funcBase->keylist()) { my $func = $funcBase->findKey($key); my $line = $func->line(); next if ($line <= 0); # ignore bogus if (exists($paired{$line})) { my $data = $paired{$line}; # the line of the current function is unchanged, so it has a # baseline line - though maybe no coverpoint there $data->lineNo('base', $diffMap->lookup($filename, $diffMap->NEW, $data->lineNo('current')), $filename) unless defined($data->lineNo('base')); $data->baseline_function($func); next; } my $data = $self->_findLineData($diffMap, $filename, $line); my $type = $data->type(); my $curr_line = $data->lineNo('current'); if ($type != DiffMap::DELETE) { $funcCovLines{$curr_line} = 1; } else { # the line has been deleted...just record the data my $deleteKey = "<<<" . $line; $funcCovLines{$deleteKey} = 1; } $data->baseline_function($func); } # foreach function in baseline data # go through function data for each line and categorize... foreach my $line (keys %funcCovLines) { my $data = $lineDataMap->{$line}; my $type = $data->type(); my $curr = $data->current_function(); my $base = $data->baseline_function(); my $categorized; if (defined($curr)) { # also copy the end line $categorized = $curr->cloneWithEndLine(1); } else { # not in current - don't copy end line # @todo if needed, could compute where the end line of the deleted # function is now $categorized = $base->cloneWithEndLine(0); } my $name = $categorized->name(); $differentialMap->{$name} = $categorized; $data->differential_function($categorized); if (!defined($base)) { # either this line was inserted or the line hasn't changed but # wasn't recognized as a function before (e.g., unused template) lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "$filename:$line: unexpected undef baseline function data for deleted $name" ) if $type == DiffMap::DELETE; _cloneFunctionEntry($categorized, $curr, $type == DiffMap::INSERT ? 'UNC' : 'UIC', $type == DiffMap::INSERT ? 'GNC' : 'GIC'); } elsif (!defined($curr)) { lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "$filename:$line: unexpected undef current function data for inserted $name" ) if $type == DiffMap::INSERT; _cloneFunctionEntry($categorized, $base, $type == DiffMap::DELETE ? 'DUB' : 'EUB', $type == DiffMap::DELETE ? 'DCB' : 'ECB'); } else { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected function coverage type $type at $filename:$line") if $type != DiffMap::EQUAL; # if we know end lines for this function, then check if there # have been any changes in the function body. If any changes, # then mark GNC or UNC my $changed; my $end_line = $curr->end_line(); if (defined($end_line)) { # we keep list of functions and branch coverpoints contained # in the function - and can report per-function coverage # in the function detail view. Maybe user will prioritize # functions which are hit but whose coverage is low $changed = 0; for (my $line = $curr->line(); $line <= $end_line; ++$line) { # data for this line my $type = $diffMap->type($filename, $diffMap->NEW, $line); # claim a change if the line is new and is source code: # it has a coverpoint, or is part of a multiline # statement which has a coverpoint. # Neither: # - a blank or comment line outside a statement, or # - an unchanged line, whether or not it has a # coverpoint in each version: # - the source is the same, and a coverpoint which # only one of the two compilations emitted - say, # on a closing brace - is an artifact of the # compiler, not an edit of the body # count as a change. if ($type == DiffMap::INSERT && (defined($lineCovCurrent->value($line)) || exists($stmtCurrent->{$line})) ) { $changed = 1; last; } } # end for each current line in current function $end_line = $base->end_line(); if (defined($end_line) && !$changed) { # check for baseline lines which were deleted for (my $bline = $base->line(); $bline <= $end_line; ++$bline) { # data for this line my $type = $diffMap->type($filename, $diffMap->OLD, $bline); # claim a change if line is deleted and was code # before - in the same sense as above if ($type != DiffMap::EQUAL && # line is in old but not in new (defined($lineCovBase->value($bline)) || exists($stmtBase->{$bline})) ) { $changed = 1; last; } } # end for each current line in current function } lcovutil::info(1, "$name body at $filename:$line is changed\n") if ($changed); } my $tla = _categorize($base->hit(), $curr->hit()); if (defined($changed) && $changed) { if ($tla eq 'UBC') { $tla = 'UNC'; } elsif ($tla eq 'GBC' || $tla eq 'CBC') { $tla = 'GNC'; } # else is LBC - leave it that way lcovutil::info(2, "$name recategorized to $tla at $filename:$line\n"); } $categorized->setCountDifferential([$curr->hit(), $tla]); # particular alias may be in both versions foreach my $alias ($base->aliasNames()) { my $hit = $base->alias($alias)->count(); my $tla; my $currAlias = $curr->alias($alias); if (defined($currAlias)) { my $hitCurr = $currAlias->count(); $tla = _categorize($hit, $hitCurr); $hit = $hitCurr; # adjust TLA if function range known.. if (defined($changed) && $changed) { if ($tla eq 'UBC') { $tla = 'UNC'; } elsif ($tla eq 'GBC' || $tla eq 'CBC') { $tla = 'GNC'; } # else is LBC - leave it that way lcovutil::info(2, "$name alias $alias recategorized to $tla at $filename:$line\n" ); } } else { $tla = (0 == $hit) ? 'EUB' : 'ECB'; } $categorized->addAliasDifferential($alias, [$hit, $tla]); # ..representing the aliases merged into it in the version whose # count it shows: the two are merged each on its own, so they # need not agree, and a name the other version did not merge # is row of its own there $categorized->alias($alias) ->add_merged( %{($currAlias // $base->alias($alias))->merged() // {}}); } # now look for aliases that are in current but not in baseline foreach my $alias ($curr->aliasNames()) { next if defined($base->alias($alias)); my $hit = $curr->alias($alias)->count(); my $tla = (0 == $hit) ? "UIC" : "GIC"; $categorized->addAliasDifferential($alias, [$hit, $tla]); $categorized->alias($alias) ->add_merged(%{$curr->alias($alias)->merged() // {}}); } } if ('UNK' eq $data->tla()) { # there is a function here - but no line - manufacture some data my $d = $categorized->hit(); my ($hit, $funcTla) = @$d; $data->tla($funcTla); if (defined($base) && $data->in_base()) { $data->base_count($base->hit()); } if (defined($curr) && $data->in_curr()) { $data->curr_count($hit); } } } } sub _findCurrentLineData { # the LineData of a line of the current version, created if that line has no # coverpoint of any other kind. '_findLineData' is the baseline-side # equivalent, and has to do more work because a baseline line may have been # deleted my ($self, $filename, $diffMap, $line) = @_; my $lineDataMap = $self->lineMap(); my $data = $lineDataMap->{$line}; return $data if defined($data); my $type = $diffMap->type($filename, $diffMap->NEW, $line); lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "'current' line $filename:$line should not be marked 'delete'") if $type == DiffMap::DELETE; $data = LineData->new($type); $lineDataMap->{$line} = $data; $data->lineNo('current', $line, $filename); return $data; } sub categorizeCallSites { # Categorize the call sites of one file: answer every site of either version # as a CallgraphSite whose category is set. This is the 'per file' view # of the metric - the sites contained in this file's functions, which is # what 'FNCF'/'FNCH' count - so it is driven by the FunctionMap of each # version. # # A free function rather than a method, and it touches no line data, because # it has two callers: '_categorizeCallgraphCov' below, which attaches the # sites to the lines of one file, and 'categorizeAllCallSites' in the # parent, which runs it for every file for the side effect alone. That side # effect is 'set_tla' writing each category through to the real 'current' # FunctionEntry, which is where the 'callers of this function' table of a # *different* file reads it from - and that table is written while some # other file is being processed, possibly in a different process. Nothing # here is order-dependent or destructive, so running it twice is harmless. # '$changed' is the set of the current lines of the changed statements - # see 'changedStatementLines' my ($filename, $funcBase, $funcCurrent, $diffMap, $changed) = @_; # Every site of either version, categorized at the end. Two passes and then # a third rather than categorizing in place, because a site's category # depends on whether the other version has it my @sites; # The current sites, indexed for the baseline match by # (caller, current line, column, callee) - deliberately not by the site # key, whose 'ordinal' is a position among the sites at one (line, column) # and so shifts when an edit adds or removes a call there. Each bucket is # in ordinal order and is paired positionally, which is an exact match in # the ordinary case of one site per (line, column, callee). my %currentSites; foreach my $key ($funcCurrent->keylist()) { my $func = $funcCurrent->findKey($key); my $caller = $func->name(); foreach my $siteKey ($func->sortedCallSiteKeys()) { my ($line, $column, $ordinal, $count, $callee, $siteFile, undef, $statement) = $func->callSiteData($siteKey); # ignore bogus line numbers - already warned about when read next if ($line <= 0); # the file the line belongs to, which is not '$filename' for a call # written in an '#include'd fragment: every question below about # what the diff did to the line has to be asked about that file my $where = defined($siteFile) ? $siteFile : $filename; my $site = CallgraphSite->new($line, $column, $ordinal, $callee, $caller, undef, $count, undef, $func, undef, $siteFile, defined($siteFile) ? $func->line() : $statement // $line); push(@sites, $site); # ..and into the bucket the baseline side looks its own sites up in my $bucket = "$where:$line:$column:$callee"; push(@{$currentSites{$caller}->{$bucket}}, $site); } } if (defined($funcBase)) { foreach my $key ($funcBase->keylist()) { my $func = $funcBase->findKey($key); my $caller = $func->name(); foreach my $siteKey ($func->sortedCallSiteKeys()) { my ($line, $column, $ordinal, $count, $callee, $siteFile, undef, $statement) = $func->callSiteData($siteKey); # ignore bogus line numbers, as on the current side next if ($line <= 0); # ..the file the line is in, as on the current side above - but # spelled the way the current version spells it, since that is # the name the diff is keyed by and the name the current sites # above are bucketed under. The two spellings differ when the # diff renames the file the call is written in, and a site # which went looking for the baseline's name would find no # diff data and no bucket: every site would read as inserted # code my $siteIn = defined($siteFile) ? $diffMap->current_file_name($siteFile) : undef; my $in = defined($siteIn) ? $siteIn : $filename; my $type = $diffMap->type($in, $diffMap->OLD, $line); my $matched; if ($type != DiffMap::DELETE && exists($currentSites{$caller})) { my $mapped = $diffMap->lookup($in, $diffMap->OLD, $line); my $bucket = $currentSites{$caller}->{"$in:$mapped:$column:$callee"}; $matched = shift(@$bucket) if defined($bucket) && @$bucket; } if (defined($matched)) { $matched->set_base_count($count); } else { # A site the baseline has and the current version does not. # It is shown where the code that made the call was: on # the baseline line itself if that line was deleted (the # source view recreates the baseline text for a deleted # region), and otherwise on the surviving current line my $where = $type == DiffMap::DELETE ? $line : $diffMap->lookup($in, $diffMap->OLD, $line); # ..and the line of *this* file it is counted on, which for a # site written elsewhere is where its caller was. A # baseline line, like BASE_LINE beside it: the attachment # goes through '_findLineData', which is what maps it # ..carrying the current spelling of the file it is written # in, as the match above used: the categorization below # asks the diff about it again, and the page which displays # it is the current one push(@sites, CallgraphSite->new($where, $column, $ordinal, $callee, $caller, $count, undef, undef, undef, $line, $siteIn, defined($siteIn) ? $func->line() : $statement // $line)); } } } } # ..and now categorize. The arms are the ones '_categorizeLineCov' uses for # a line, because a call site is a point on one line: unlike a function, # it has no body whose edits could recategorize it. Only the statement # it is written in can - as it does the line's own coverpoint foreach my $site (@sites) { # a count of '-' - the call is there and its count is not known, see # FunctionEntry::SITE_COUNT - reads as 0 here, which is what # 'BranchElement::count' does for a branch which was never evaluated. # There is no category for "unknown", and of the two which exist the # only honest one is the not-hit one: the marker is kept for display, # which is where the difference can actually be told to the reader my $base = $site->base_count(); my $curr = $site->curr_count(); $base = lcovutil::site_count_value($base) if defined($base); $curr = lcovutil::site_count_value($curr) if defined($curr); # ..and the questions about the diff are about the file the site's line # belongs to, which is not this file for a site written elsewhere my $in = defined($site->file()) ? $site->file() : $filename; my $tla; if (defined($curr)) { if (defined($base)) { $tla = _categorize($base, $curr); } elsif (!defined($funcBase) || $diffMap->type($in, $diffMap->NEW, $site->line()) == DiffMap::INSERT) { $tla = $curr ? 'GNC' : 'UNC'; } else { $tla = $curr ? 'GIC' : 'UIC'; } # a site of this file, written in a changed statement $tla = $changedStatementTla{$tla} if (defined($changed) && !defined($site->file()) && $changed->{$site->line()} && exists($changedStatementTla{$tla})); } else { # ..a deleted category if the line the site is attached to was # deleted, too: that is a deleted record (see '_findLineData'), # whose category is not one of the current version. The site's # own line need not be deleted with it - a call on an unchanged # later line of a statement whose first line was edited, or one # in an '#include'd fragment called from a deleted function my $deleted = $diffMap->type($in, $diffMap->OLD, $site->base_line()) == DiffMap::DELETE; $deleted ||= $diffMap->type($filename, $diffMap->OLD, $site->home_line()) == DiffMap::DELETE; $tla = $deleted ? ($base ? 'DCB' : 'DUB') : ($base ? 'ECB' : 'EUB'); } # 'set_tla' also stores the category on the real 'current' FunctionEntry - # see the header comment. A baseline-only site has no current record to # store it on, and so does not appear in that table: it is counted and # displayed by the file it belongs to $site->set_tla($tla); } return @sites; } sub _categorizeCallgraphCov { # Attach this file's categorized call sites to the lines they are displayed # on. The 'per function' view (which functions call this one) is cross-file # and has no per-file baseline to compare against; it reads the categories # which 'categorizeCallSites' stored. my ($self, $filename, $base_data, $current_data, $diffMap, $verbose, $changed) = @_; my $funcBase; $funcBase = $base_data->func() if defined($base_data); print("categorize call sites $filename\n") if ($verbose); foreach my $site (categorizeCallSites($filename, $funcBase, $current_data->func(), $diffMap, $changed )) { # a baseline-only site is looked up with the line it was read from, since # that is the side of the diff the lookup has to start on - and a # deleted line has no current line to be keyed by at all # ..and the line is always one of *this* file, which for a site written # in another file is not the line the site is on - see # CallgraphSite::HOME_LINE my $data = defined($site->curr_count()) ? $self->_findCurrentLineData($filename, $diffMap, $site->home_line()) : $self->_findLineData($diffMap, $filename, $site->home_line()); $data->add_call_site($site); lcovutil::info(2, " call site " . $site->caller() . ' -> ' . $site->callee() . ' at ' . (defined($site->file()) ? $site->file() : $filename ) . ':' . $site->line() . ' is ' . $site->tla() . "\n"); } # the sites of one line, in source order: 'categorizeCallSites' answered # them in whatever order the FunctionMap iterated my $lineDataMap = $self->lineMap(); foreach my $data (values(%$lineDataMap)) { next unless $data->num_call_sites(); $data->sort_call_sites() if $data->num_call_sites() > 1; next unless 'UNK' eq $data->tla(); # A call site on a line with no coverpoint of any other kind, so # '_findCurrentLineData' above created the line and left it at its # initial 'UNK' - which is not a key of '%SummaryInfo::tlaLocation', # making the line an ERROR_UNKNOWN_CATEGORY when it is counted. Take # the category from the first site on the line, exactly as the function # path in '_categorizeFunctionCov' takes it from the function. # Reachable input: an '.info' whose line records were filtered away # while its 'FNC:' records were kept - which is what a capture by a # version of this tool older than the filter fix looks like - or any # other producer of a call on a line it emits no 'DA:' for. A capture # by this version does not have it: a line with a call site is not a # line the filters remove my @sites = $data->call_sites(); $data->tla($sites[0]->tla()); } } package DiffMap; # @todo Could convert this use a callback API - similar to annotate, etc. # that would be more consistent and might execute faster. use constant { LINEMAP => 0, FILEMAP => 1, BASELINE => 2, # keep track of line number where file entry is found in diff file # - use line number in error messages. DIFF_FILENAME => 3, LOCATION => 4, UNCHANGED => 5, ALIASES => 6, DIFF_ROOT => 7, # old_filename->new_filename, for the files the diff renames RENAMED => 8, # the names used by the 'current' coverage data, and the diff # entries indexed by basename - see 'baselineSource' CURRENT => 9, BASENAMES => 10, OLD => 0, NEW => 1, TYPE => 2, _START => 0, _END => 1, }; sub new { my $class = shift; # every slot has to be initialized here: 'findName' is called for every # file in the report, whether a diff was read or not, and 'lc(undef)' - # unlike the '.=' the case-sensitive arm uses - warns my $self = [{}, # linemap {}, # filemap: new_filename->old_filename {}, # baseline: filename -> baseline lineno -> text undef, # diff filename [{}, {}], # def location # element 0: old filename -> line number where this # entry starts # element 1: new filename -> line number {}, # unchanged {}, # aliases Cwd::cwd(), # diff root: default, overridden by the diff's # 'Git Root:' comment - and not '$main::cwd', # which is not set until after this constructor # has run {}, # renamed: old_filename->new_filename {}, # current: the names the 'current' data uses {} # basenames: basename->[diff entry, ..] ]; bless $self, $class; return $self; } sub load { my ($self, $path, $info, $buildDirs) = @_; $self->_read_udiff($path); # the names the 'current' coverage data uses, and the diff entries indexed # by basename: 'baselineSource' needs both, to decide whether a baseline # file whose name is not in the diff is nevertheless described by it. # 'check_path_consistency' makes the same decision for the categorization # - but not until after the baseline source has been read. $self->[CURRENT] = {}; $self->[BASENAMES] = {}; if (defined($info)) { foreach my $f ($info->files()) { $self->[CURRENT]->{$lcovutil::case_insensitive ? lc($f) : $f} = 1; } } foreach my $d (keys(%{$self->[LINEMAP]})) { push(@{$self->[BASENAMES]->{File::Basename::basename($d)}}, $d); } # find list of soft links in [buildDirs] which may point to files in $info # we need to keep all the aliases as there may be files in baseline # which are not in current if (@$buildDirs) { my @stack = @$buildDirs; while (@stack) { my $dir = pop(@stack); die("unexpected non-directory '$dir'") unless -d $dir; $dir = File::Spec->catdir($main::cwd, $dir) unless File::Spec->file_name_is_absolute($dir); opendir(my $dh, $dir) or die("can't open directory $dir: $!"); while (my $entry = readdir($dh)) { next if $entry eq '.' || $entry eq '..'; my $path = File::Spec->catfile($dir, $entry); if (-d $path) { push(@stack, $path); } elsif (-l $path) { my $l = Cwd::realpath($path); next if (!-e $l || TraceFile::skipCurrentFile($l)); # may need to turn $l into relative path?? $self->[ALIASES]->{$path} = $l if (-f $l); #lcovutil::info("add alias '$path' -> '$l'\n"); # really, this should be a file... die("unexpected soft link $path to directory") unless -f $l; } # else just ignore file entry } closedir($dh); } } return $self; } sub empty { my $self = shift; return !scalar(%{$self->[LINEMAP]}); } sub findName { my ($self, $file) = @_; my $f = $lcovutil::case_insensitive ? lc($file) : $file; $f = $self->[ALIASES]->{$f} if exists($self->[ALIASES]->{$f}); if (File::Spec->file_name_is_absolute($f) && !exists($self->[LINEMAP]->{$f})) { my $p = $lcovutil::case_insensitive ? lc($self->[DIFF_ROOT]) : $self->[DIFF_ROOT]; $p .= $lcovutil::dirseparator; my $l = length($p); if (length($f) > $l && $p eq substr($f, 0, $l)) { my $s = substr($f, $l); if (exists($self->[LINEMAP]->{$s})) { $f = $s; } elsif (exists($self->[ALIASES]->{$s})) { $f = $self->[ALIASES]->{$s}; } } } return $f; } sub containsFile { my ($self, $file) = @_; $file = $self->findName($file); return exists($self->[LINEMAP]->{$file}); } sub baselineSource { # The baseline text of $filename has to be recreated out of the patch when # the diff describes the file. Return the name the diff data is filed # under, together with the name to read the current source from - or the # empty list, when the diff says nothing about this file. # The two names are not always the baseline's own: a rename files the # chunks under the new name, and '--elide-path-mismatch' says that a diff # entry whose basename matches IS this file. Recreating the baseline out # of the wrong entry - or, worse, silently handing back the CURRENT text # as the baseline's - makes every filter and every exclusion marker see # the wrong source. my ($self, $filename) = @_; my $f = $lcovutil::case_insensitive ? lc($filename) : $filename; my $key = $self->findName($filename); return () if exists($self->[UNCHANGED]->{$key}); if (!exists($self->[LINEMAP]->{$key})) { if (exists($self->[RENAMED]->{$key})) { # the diff renames the file: the baseline knows it by the old name $key = $self->[RENAMED]->{$key}; } else { # 'check_path_consistency' additionally requires the file to be # missing from both .info files, which it cannot know here: the # baseline is what is being read. It does not need to be that # strict - a diff entry which no 'current' file claims is the # only candidate either way. return () unless $main::elide_path_mismatch; my $candidates = $self->[BASENAMES]->{File::Basename::basename($key)}; return () unless defined($candidates); my @unused = grep(!exists($self->[CURRENT]->{$_}), @$candidates); return () unless 1 == scalar(@unused); $key = $unused[0]; } } # read the current text under the name the 'current' coverage data uses: # the name in hand is the baseline's, which need not exist any more, and # the diff's spelling of an elided path need not exist at all return ($key, exists($self->[CURRENT]->{$f}) ? $filename : $key); } sub recreateBaseline { my ($self, $filename, $currentSrcLines, $key) = @_; # $key is the name the diff data is filed under, which is not necessarily # the baseline's own name - see 'baselineSource' $key = $self->findName($filename) unless defined($key); my $diffs = $self->[LINEMAP]->{$key}; my $lineNo = $self->[LOCATION]->[NEW]->{$key}; my $location = $self->[DIFF_FILENAME] . (defined($lineNo) ? ":$lineNo" : ''); my $also = $key eq $filename ? '' : " (as '$key')"; unless (defined($diffs)) { # the only caller - ReadBaselineSource::open - passes the name which # 'baselineSource' found in the diff data # LCOV_UNREACHABLE_START lcovutil::ignorable_error( $lcovutil::ERROR_PATH, "no 'diff' data for baseline file '$filename'$also in " . $self->[DIFF_FILENAME] . ": treating the file as unchanged"); # message ignored: the best we can do is use the current text return $currentSrcLines; # LCOV_UNREACHABLE_STOP } # the deleted text is filed under the name the diff's '---' line used, which # is not the name the chunks are filed under when the file was renamed my $baseName = $self->baseline_file_name($key); my $deleted = $self->[BASELINE]->{$baseName}; my @lines; my $didWarn = 0; # the last line of the current file which some chunk accounted for: a # unified diff says nothing at all about the text after its last hunk, so # the chunk list stops there - see 'lookup' and 'type', which treat a line # past the last chunk as unchanged. We have to do the same, below. my $lastCurrent = 0; foreach my $chunk (@$diffs) { $lastCurrent = $chunk->[NEW]->[_END] if $chunk->[NEW]->[_END] > $lastCurrent; if ($chunk->[TYPE] == EQUAL) { my ($from, $to) = @{$chunk->[NEW]}; my $f = $from - 1; my $t = $to - 1; if ($t <= $#$currentSrcLines) { push(@lines, @{$currentSrcLines}[$f .. $t]); } else { lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "$filename: inconsistent diff data vs current source code: diff refers to 'current' line range [$f:$t] but source code has only " . scalar(@$currentSrcLines) . " lines") unless $didWarn; $didWarn = 1; # if error is ignored, then append up to the data we have while ($f <= $#$currentSrcLines) { push(@lines, $currentSrcLines->[$f++]); } } } elsif ($chunk->[TYPE] == DiffMap::DELETE) { my $r = $chunk->[OLD]; for (my $i = $r->[_START]; $i <= $r->[_END]; ++$i) { unless (defined($deleted) && exists($deleted->{$i})) { # every line of a DELETE chunk is a '-' line, whose text # was filed under the '---' name which 'baseline_file_name' # returns. The one name which breaks that - the alias # 'check_path_consistency' adds for an elided path - is # made after the baseline has been read # LCOV_UNREACHABLE_START lcovutil::ignorable_error( $lcovutil::ERROR_INCONSISTENT_DATA, "$location: no baseline text for line $i of '$baseName'" . ($baseName eq $filename ? '' : " (baseline of '$filename')") . ": do the diff's '---' and '+++' names differ?"); # message ignored: keep the line numbering intact - every # line after this one would otherwise shift push(@lines, ''); next; # LCOV_UNREACHABLE_STOP } my $d = $deleted->{$i}; die("undef deleted line $filename:$i") unless defined($d); push(@lines, $d); } } # else 'insert': nothing to do/those lines are not in baseline } # everything after the last hunk is identical in the two versions, so the # tail of the current file is the tail of the baseline. Without this, the # recreated baseline ended at the last hunk: every exclusion marker after # that point was missed and every baseline coverage point past it was # reported as an out-of-range (ERROR_RANGE) line - which is what a # differential report against a baseline whose last change is not at the # end of the file looks like. push(@lines, @{$currentSrcLines}[$lastCurrent .. $#$currentSrcLines]) if $lastCurrent <= $#$currentSrcLines; return \@lines; } sub _findChunk { my ($lineMap, $vers, $line, $filename) = @_; my $notThis = $vers == OLD ? INSERT : DELETE; # LCOV_EXCL_START my $linearSearch = sub { my $idx = 0; while ($idx <= $#$lineMap) { my $chunk = $lineMap->[$idx]; my $v = $chunk->[$vers]; last if ($v->[_START] <= $line && $line <= $v->[_END] && $chunk->[TYPE] != $notThis); ++$idx; } return $idx; }; # LCOV_EXCL_STOP # might be marginally faster to use linear search up to some small # list size threshold...maybe 5 or 10? my $max = $#$lineMap; my $min = 0; my $mid; while (1) { $mid = int(($max + $min) / 2); my $chunk = $lineMap->[$mid]; my $d = $chunk->[$vers]; if ($line < $d->[_START]) { $max = $mid; } elsif ($line > $d->[_END]) { $min = $mid; } else { # this entry is in range - match is either this one # or the one on one side or the other. # There are at most 2 entries for any particular line - # and there are two only if this is the insert or delete point if ($chunk->[TYPE] != $notThis) { die("bad lookup") unless (!$lcovutil::debug || $mid == &$linearSearch()); return $chunk; } # is it one side or the other? die("unexpectedly at beginning") if $mid == 0; my $prev = $lineMap->[$mid - 1]; my $pd = $prev->[$vers]; if ($pd->[_END] >= $line) { die("unexpected prev element") unless ($pd->[_START] <= $line && $prev->[TYPE] != $notThis); die("bad lookup 2") unless (!$lcovutil::debug || $mid - 1 == &$linearSearch()); return $prev; } if ($mid == $#$lineMap) { # we are at last element die("wrong place") unless (!$lcovutil::debug || &$linearSearch() == $#$lineMap + 1); return undef; } my $next = $lineMap->[$mid + 1]; my $nd = $next->[$vers]; die("expected 'next' to match but did not") unless ($nd->[_START] <= $line && $nd->[_END] >= $line && $next->[TYPE] != $notThis); die("bad lookup 3") unless (!$lcovutil::debug || $mid - 1 == &$linearSearch()); return $next; } my $diff = $max - $min; if ($diff <= 1) { $mid = $min; $mid = $max if $line > $lineMap->[$min]->[$vers]->[_END]; last; } } my $chunk = $lineMap->[$mid]; my $d = $chunk->[$vers]; if ($d->[_START] <= $line && $d->[_END] >= $line) { if ($chunk->[TYPE] != $notThis) { die("bad lookup 4") unless (!$lcovutil::debug || $mid == &$linearSearch()); return $chunk; } if ($mid <= $#$lineMap) { # we aren't at the last element in the list my $next = $lineMap->[$mid + 1]; my $nd = $next->[$vers]; if ($nd->[_START] <= $line && $nd->[_END] >= $line && $next->[TYPE] != $notThis) { die("wrong lookup") unless (!$lcovutil::debug || $mid + 1 == &$linearSearch()); return $next; } } } die("didn't find element for $filename:$line: $#$lineMap") unless (!$lcovutil::debug || $#$lineMap + 1 == &$linearSearch()); return undef; } sub lookup { # given line in OLD or NEW version, return corresponding line in the # other. # $vers == OLD if $line is in OLD (ie., we are looking for NEW # and # $vers == NEW if $line is in NEW and we are looking for line number in OLD my ($self, $file, $vers, $line) = @_; $file = $self->findName($file); if (!exists($self->[LINEMAP]->{$file})) { #mapping is identity when no diff was read return $line; } # search from end of list to find last chunk which starts before this line my $lineMap = $self->[LINEMAP]->{$file}; my $chunk = _findChunk($lineMap, $vers, $line, $file); my $alt = ($vers == OLD) ? NEW : OLD; my $c; if (defined($chunk)) { my $delta = $line - $chunk->[$vers]->[_START]; $c = $chunk->[$alt]->[_START] + $delta; $c += $chunk->[TYPE] == DiffMap::DELETE; } else { # this element is past the last chunk my $v = $lineMap->[-1]; my $delta = $line - $v->[$vers]->[_END]; $c = $v->[$alt]->[_END] + $delta; } return $c; } sub type { my ($self, $file, $vers, $line) = @_; $file = $self->findName($file); if (!defined($self->[LINEMAP]->{$file})) { #mapping is identity when no diff was read if (defined($main::show_tla) && (@main::base_filenames || $main::diff_filename) ) { return DiffMap::EQUAL; # categories will be "GIC", "UIC" } else { return DiffMap::INSERT; # categories will be "GNC", "UNC" } } if (!defined($self->[FILEMAP]->{$file})) { #mapping with no filemap when baseline file was deleted return DiffMap::DELETE; } my $lineMap = $self->[LINEMAP]->{$file}; my $chunk = _findChunk($lineMap, $vers, $line, $file); # '$line' is past the end of the last chunk in the diff - # all the lines which weren't mentioned must be equal return EQUAL unless defined($chunk); die("DiffMap::type(): got undef type at $file, $vers, $line") unless defined($chunk->[TYPE]); return $chunk->[TYPE]; } sub find_deleted_line_leader { my ($self, $filename, $base_lineNo) = @_; # find the 'delete' chunk which contains this line - # - the line number of the leader in the baseline is the line # immediately before the first line in the chunk (in the 'OLD' # section # - the line number of the leader in the current file is the # first line in the 'NEW' section of this chunk. # - we expect the 'NEW' begin and end lines are the same # - we also expect the leader to be the 'end' line of the # 'NEW' section in the previous chunk. my $file = $self->findName($filename); my $lineMap = $self->[LINEMAP]->{$file}; # there must be a lineMap, if we have a deleted line die("lineMap must exist $filename:$base_lineNo") unless defined($lineMap); my $chunk = _findChunk($lineMap, OLD, $base_lineNo, $filename); # should not fall off end of 'diff' - there must be a 'delete' chunk die("missing 'delete' chunk in diff $filename:$base_lineNo") unless defined($chunk); die("unexpected chunk $filename:$base_lineNo") unless ($chunk->[TYPE] == DELETE && $chunk->[NEW]->[_START] == $chunk->[NEW]->[_END]); return $chunk->[NEW]->[_START]; } sub compute_deleted_lines { my ($self, $filename) = @_; my $file = $self->findName($filename); my $lineMap = $self->[LINEMAP]->{$file}; return undef unless defined($lineMap); my %hash; foreach my $chunk (@$lineMap) { next unless $chunk->[TYPE] == DELETE; $hash{$chunk->[NEW]->[_START]} = $chunk->[OLD]; } return %hash ? \%hash : undef; } sub baseline_file_name { # file may have been moved between baseline and current... my ($self, $current_name) = @_; my $key = $self->findName($current_name); if (exists($self->[FILEMAP]->{$key})) { return $self->[FILEMAP]->{$key}; } return $current_name; } sub current_file_name { # ..and the other direction: the name the 'current' version of a file the # baseline calls '$baseline_name' is known by, which is the name every # other method here takes - the chunks of a file the diff renames are # filed under its new name (see 'baselineSource'). The name itself when # the diff does not rename it, which is the ordinary case, and also when # the file is gone in the current version: a deleted file has no current # name, and answering its own is what leaves 'type' free to say DELETE. # Wanted where a name out of the baseline data has to be matched against one # out of the current data: a per-file lookup can go through # 'baseline_file_name' instead, but the file a *call site* is written in is # recorded in each version's own spelling - see 'categorizeCallSites' my ($self, $baseline_name) = @_; my $key = $self->findName($baseline_name); return exists($self->[RENAMED]->{$key}) ? $self->[RENAMED]->{$key} : $baseline_name; } sub files { my $self = shift; return keys(%{$self->[FILEMAP]}); } sub dump_map { my $self = shift; foreach my $file (keys %{$self->[FILEMAP]}) { my $currfile = defined($self->[FILEMAP]->{$file}) ? $self->[FILEMAP]->{$file} : "[deleted]"; printf("In $file (was: $currfile):\n"); foreach my $chunk (@{$self->[LINEMAP]->{$file}}) { _printChunk($chunk); } } return $self; } sub check_version_match { my ($self, $baseline, $current) = @_; return unless $lcovutil::versionCallback; # skip files which were dropped (no longer in project) or were # just added to the project (was not in the baseline - so we aren't # looking for differences. foreach my $curr ($current->files()) { next unless $baseline->file_exists($curr); my $currData = $current->data($curr); my $curr_version = $currData->version(); my $baseData = $baseline->data($curr); my $base_version = $baseData->version(); # silently compare version... my $versionMatch = lcovutil::checkVersionMatch($curr, $base_version, $curr_version, "diff entry compare", 1); if ($self->containsFile($curr)) { # file is in 'diff' data: we expect the version to be different # between baseline/current if ($versionMatch) { lcovutil::ignorable_error( $lcovutil::ERROR_INCONSISTENT_DATA, "File \"$curr\" appears in 'diff' data file '" . $self->[DIFF_FILENAME] . "' but 'baseline' and 'current' versions '" . ($curr_version ? $curr_version : '') . "' match"); } } else { # not in 'diff': we expect the versions to be identical if (!$versionMatch) { lcovutil::ignorable_error( $lcovutil::ERROR_INCONSISTENT_DATA, "File \"$curr\" version changed from '" . ($base_version ? $base_version : '') . "' but file not found in 'diff' data file '" . $self->[DIFF_FILENAME] . "'."); } } } } sub check_path_consistency { # check that paths which appear in diff also appear in baseline or current # .info files - if not, then there is likely a path consistency issue # $baseline and $current are both TraceFile structs - # return 0 if inconsistency found my ($self, $baseline, $current) = @_; (ref($baseline) eq 'TraceFile' && ref($current) eq 'TraceFile') or die("wrong arg types"); # check that files which are in both baseline and current and are NOT # in the 'diff' data have the same version. # That is: files whose version differs should appear in the diff data $self->check_version_match($baseline, $current); my %diffMap; # current_filename -> where_used my %diffBaseMap; # current_directory -> [] # for every 'current' filename in the udiff file foreach my $f ($self->files()) { $diffMap{$f} = 0; # this file not used in baseline or current (yet) my $b = File::Basename::basename($f); $diffBaseMap{$b} = [[], {}] unless exists($diffBaseMap{$b}); push(@{$diffBaseMap{$b}->[0]}, $f); } # foreach unchanged file in udiff data foreach my $f (keys %{$self->[UNCHANGED]}) { # unchanged in baseline and current $diffMap{$f} = 3; } my %missed; # for each file in 'current' info: foreach my $curr ($current->files()) { my $b = File::Basename::basename($curr); if ($self->containsFile($curr)) { my $alias = exists($self->[ALIASES]->{$curr}) ? $self->[ALIASES]->{$curr} : $curr; $diffMap{$alias} |= 1; # used in current $diffBaseMap{$b}->[1]->{$alias} = 0 unless exists($diffBaseMap{$b}->[1]->{$alias}); ++$diffBaseMap{$b}->[1]->{$alias}; } elsif (!exists($self->[UNCHANGED]->{$self->findName($curr)})) { $missed{$curr} = 1; # in current but not in diff } } # for each file in 'baseline' info: foreach my $base ($baseline->files()) { my $b = File::Basename::basename($base); if ($self->containsFile($base)) { my $alias = exists($self->[ALIASES]->{$base}) ? $self->[ALIASES]->{$base} : $base; $diffMap{$alias} |= 2; # used in baseline $diffBaseMap{$b}->[1]->{$alias} = 0 unless exists($diffBaseMap{$b}->[1]->{$alias}); ++$diffBaseMap{$b}->[1]->{$alias}; } elsif (!exists($self->[UNCHANGED]->{$self->findName($base)})) { # in baseline but not in diff if (exists($missed{$base})) { $missed{$base} |= 2; } else { $missed{$base} = 2; } } } my $ok = 1; foreach my $f (sort keys(%missed)) { my $b = File::Basename::basename($f); next unless exists($diffBaseMap{$b}); # this basename is in the diff file and didn't match any other # trace filename entry (i.e., same filename in more than one # source code directory) - then warn about possible pathname issue my ($diffFiles, $sourceFiles) = @{$diffBaseMap{$b}}; # find the files which appear in the 'diff' list which have the # same basename and were not matched - those might be candidates my @unused; for my $d (@$diffFiles) { # my $location = $self->[DIFF_FILENAME] . ':' . $self->[LOCATION]->[NEW]->{$d}; push(@unused, $d) unless exists($sourceFiles->{$d}); } next unless @unused; # my $baseData = $baseline->data($f); # my $baseLocation = join(":", ${$baseData->location()}); # my $currData = $current->data($f); # my $currLocation = join(":", ${$currData->location()}); my $type; if (2 == $missed{$f}) { $type = "baseline"; } elsif (1 == $missed{$f}) { $type = "current"; } else { $type = "both baseline and current"; } my $single = 1 == scalar(@unused); # @todo could print line numbers in baseline, current .info files and # in diff file .. if (lcovutil::warn_once($lcovutil::ERROR_MISMATCH, $f)) { my $suffix = $main::elide_path_mismatch ? ' (elided)' : lcovutil::explain_once( 'elide-path-mismatch', ' Perhaps see "--elide-path-mismatch", "--substitute" and "--build-directory" options in \'man ' . $lcovutil::tool_name . '\''); lcovutil::ignorable_warning( $lcovutil::ERROR_MISMATCH, "source file '$f' (in $type .info file" . ($missed{$f} == 3 ? "s" : "") . ") has same basename as 'diff' " . ($single ? 'entry ' : "entries:\n\t" ) . "'" . join("'\n\t", @unused) . "' - but a different path." . ($single ? " " : "\n\t") . 'Possible pathname mismatch?' . $suffix); } if ($main::elide_path_mismatch && $missed{$f} == 3 && $single) { $self->[FILEMAP]->{$f} = $f; $self->[LINEMAP]->{$f} = $self->[LINEMAP]->{$unused[0]}; } else { $ok = 0; } } return $ok; } sub _printChunk { my $chunk = shift; printf(" %6s\t[%d:%d]\t[%d:%d]\n", $DiffMap::type2str[$chunk->[TYPE]], $chunk->[OLD]->[_START], $chunk->[OLD]->[_END], $chunk->[NEW]->[_START], $chunk->[NEW]->[_END]); } sub _newChunk { my ($type, $baseline_start, $current_start) = @_; # new chunk starts and ends on the same line - until we see more lines and # extend either the old or new range return [[$baseline_start, $baseline_start], [$current_start, $current_start], $type ]; } sub show_map { my ($self, $file) = @_; $file = $self->findName($file); return $self unless exists($self->[FILEMAP]->{$file}); my $currfile = defined($self->[FILEMAP]->{$file}) ? $self->[FILEMAP]->{$file} : "[deleted]"; print("In $file" . ($currfile ne $file ? " (was: $currfile)" : '') . ":\n"); foreach my $chunk (@{$self->[LINEMAP]->{$file}}) { _printChunk($chunk); } return $self; } sub _read_udiff { my $self = shift; my $diff_file = shift; # Name of diff file my $line; # Contents of current line my $file_old; # Name of old file in diff section my $file_new; # Name of new file in diff section my $filename; # Name of common filename of diff section # Check if file exists and is readable stat($diff_file); if (!(-r _)) { die("cannot read udiff file $diff_file!\n"); } $self->[DIFF_FILENAME] = $diff_file; my $diffFile = InOutFile->in($diff_file); my $diffHdl = $diffFile->hdl(); my $chunk; my $old_block = 0; my $new_block = 0; # would like to use Regexp::Common::time - but module not installed #my $time = $RE{time}{iso}; my $time = '[1-9]{1}[0-9]{3}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]*)?( .[0-9]+)?'; # Parse diff file line by line my $verbose = 0; my $skipCurrentFile = 0; while (<$diffHdl>) { chomp($_); s/\r//g; $line = $_; # the 'diff' new/old file name line may be followed by a timestamp # If so, remove it so our regexp matches more easily. # p4 and git diff outputs do not have the timestamp if ($line =~ /^[-+=]{3} \S.*(\s+$time)$/) { $line =~ s/\Q$1\E$//; } foreach ($line) { /^Git Root: (.+)$/ && do { # save match: 'check_path_separator' matches further # regexps and overwrites $1 my $root = $1; $root = lcovutil::check_path_separator($root, $diff_file, $., 'diff'); $self->[DIFF_ROOT] = ReadCurrentSource::resolve_path($root, 1); last; }; # Filename of unchanged file: # === /^=== (.+)$/ && do # note: filename may contain whitespace { if ($filename && !$skipCurrentFile) { die("not case insensitive") unless !$lcovutil::case_insensitive || ($filename eq lc($filename)); push(@{$self->[LINEMAP]->{$filename}}, $chunk); undef $filename; } my $file = $1; # a name written on the other platform is put into this # platform's terms first: everything below - the directory # stripping, '--substitute', and the comparison against the # names in the coverage data - works in host-style paths $file = lcovutil::check_path_separator($file, $diff_file, $., 'diff'); $file = lcovutil::strip_directories($file, $main::strip); my $key = ReadCurrentSource::resolve_path($file, 1); # skip diff data for excluded files - but skip quietly $skipCurrentFile = TraceFile::skipCurrentFile($key, 'diff'); last if $skipCurrentFile; $key = lc($key) if $lcovutil::case_insensitive; if (exists($self->[UNCHANGED]->{$key})) { # unchanged entry flag value should be 1 die("$diff_file:$.: $key already in linemap - marked unchanged" . ($file eq $key ? '' : " (substituted '$file')")) unless $self->[UNCHANGED]->{$key}; lcovutil::ignorable_error($lcovutil::ERROR_FORMAT, "$diff_file:$.: duplicate 'unchanged' entry for $key\n" ); last; } $verbose = (defined($main::verboseScopeRegexp) && $key =~ m/$main::verboseScopeRegexp/); print(" $key: unchanged\n") if $verbose; if (exists($self->[LINEMAP]->{$key})) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "$diff_file:$.: $key 'unchanged' entry found but diff records exist" . ($file eq $key ? '' : " (substituted '$file')")); # message ignored - delete the diff records delete($self->[LINEMAP]->{$key}); # LCOV_UNREACHABLE_STOP } $self->[UNCHANGED]->{$key} = 1; last; }; next if $skipCurrentFile; # Filename of old file: # --- /^--- (.+)$/ && do { if ($filename) { die("not case insensitive") unless !$lcovutil::case_insensitive || ($filename eq lc($filename)); push(@{$self->[LINEMAP]->{$filename}}, $chunk); undef $filename; } $file_old = $1; $file_old = lcovutil::check_path_separator($file_old, $diff_file, $., 'diff'); $file_old = lcovutil::strip_directories($file_old, $main::strip); $file_old = ReadCurrentSource::resolve_path($file_old, 1); $file_old = lc($file_old) if $lcovutil::case_insensitive; $self->[LOCATION]->[OLD]->{$file_old} = $.; last; }; # Filename of new file: # +++ /^\+\+\+ (.+)$/ && do { # Add last file to resulting hash $file_new = $1; $file_new = lcovutil::check_path_separator($file_new, $diff_file, $., 'diff'); $file_new = lcovutil::strip_directories($file_new, $main::strip); my $key = ReadCurrentSource::resolve_path($file_new, 1); $key = lc($key) if $lcovutil::case_insensitive; my $notNull = $file_new ne $lcovutil::devnull; $filename = $notNull ? $key : undef; if ($filename) { if (exists($self->[UNCHANGED]->{$key})) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error( $lcovutil::ERROR_INTERNAL, "$diff_file:$.: $filename marked unchanged but diff record found" . ($file_new eq $key ? '' : " (substituted '$file_new')")); # OK - error ignored...remove the 'unchanged' marker delete($self->[UNCHANGED]->{$key}); # LCOV_UNREACHABLE_STOP } $self->[LINEMAP]->{$key} = []; } $verbose = (defined($main::verboseScopeRegexp) && $key =~ m/$main::verboseScopeRegexp/); print("file $key\n") if $verbose; if ($notNull) { $self->[FILEMAP]->{$key} = $file_old; # ..and the reverse, for a file the diff renames: the # baseline coverage data knows it by its old name, while # the chunks are filed under the new one - see # 'baselineSource' $self->[RENAMED]->{$file_old} = $key if (defined($file_old) && $file_old ne $key && $file_old ne $lcovutil::devnull); # keep track of location where this file was found $self->[LOCATION]->[NEW]->{$key} = $.; # record original name too.. $file_new } # new file - chunk starts here $chunk = _newChunk(EQUAL, 1, 1); last; }; # Start of diff block: # @@ -old_start[,old_num] +new_start[,new_num] @@ # both counts are optional: a range whose length is 1 is written # as the start line alone ('@@ -4 +4 @@') /^\@\@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+\@\@.*$/ && do { if ($1 > ($chunk->[OLD]->[_END])) { # old start skips "equal" lines if ($chunk->[TYPE] != EQUAL) { if ($filename) { push(@{$self->[LINEMAP]->{$filename}}, $chunk); _printChunk($chunk) if ($verbose); } $chunk = _newChunk(EQUAL, $chunk->[OLD]->[_END] + 1, $chunk->[NEW]->[_END] + 1); } } # "END" will be incremented on content lines $chunk->[OLD]->[_END] = $1 - 1; $chunk->[NEW]->[_END] = $3 - 1; # an omitted count means 1 - and 0 is a legal count ('@@ -0,0 # +1,3 @@' for a created file), so the default has to be # applied on 'defined', not on truth $old_block = defined($2) ? $2 : 1; $new_block = defined($4) ? $4 : 1; #printf "equal [%d:%d] [%d:%d]\n", $l[0], $l[1], $l[2], $l[3]; last; }; # ..and a '@@' line this reader does not understand is a malformed # patch: falling through to the content arms below would leave # the hunk's lines attributed to whatever the previous header said /^\@\@/ && do { lcovutil::ignorable_error($lcovutil::ERROR_FORMAT, "$diff_file:$.: unexpected 'diff' hunk header '$line': expected '\@\@ -[,] +[,] \@\@'" ); last; }; # Unchanged line # /^ / && do { if ($old_block == 0 && $new_block == 0) { last; } if ($chunk->[TYPE] != EQUAL) { if ($filename) { push(@{$self->[LINEMAP]->{$filename}}, $chunk); _printChunk($chunk) if ($verbose); } # next chunk starts right after current one $chunk = _newChunk(EQUAL, $chunk->[OLD]->[_END] + 1, $chunk->[NEW]->[_END] + 1); } else { $chunk->[NEW]->[_END] += 1; $chunk->[OLD]->[_END] += 1; } last; }; # Line as seen in old file # /^-(.*)$/ && do { if ($old_block == 0 && $new_block == 0) { last; } my $baseline_lineno = $chunk->[OLD]->[_END] + 1; # really only need to keep track of baseline content if user # is planning on doing any filtering my $lines; if (exists($self->[BASELINE]->{$file_old})) { $lines = $self->[BASELINE]->{$file_old}; } else { $lines = {}; $self->[BASELINE]->{$file_old} = $lines; } $lines->{$baseline_lineno} = $1; if ($chunk->[TYPE] != DELETE) { if ($filename) { push(@{$self->[LINEMAP]->{$filename}}, $chunk); _printChunk($chunk) if ($verbose); } $chunk = _newChunk(DELETE, $baseline_lineno, $chunk->[NEW]->[_END]); } else { $chunk->[OLD]->[_END] = $baseline_lineno; } last; }; # Line as seen in new file # /^\+/ && do { if ($old_block == 0 && $new_block == 0) { last; } if ($chunk->[TYPE] != INSERT) { if ($filename) { push(@{$self->[LINEMAP]->{$filename}}, $chunk); _printChunk($chunk) if ($verbose); } $chunk = _newChunk(INSERT, $chunk->[OLD]->[_END], $chunk->[NEW]->[_END] + 1); } else { $chunk->[NEW]->[_END] += 1; } last; }; # Empty line /^$/ && do { if ($old_block == 0 && $new_block == 0) { last; } if ($chunk->[TYPE] != EQUAL) { if ($filename) { push(@{$self->[LINEMAP]->{$filename}}, $chunk); _printChunk($chunk) if ($verbose); } $chunk = _newChunk(EQUAL, $chunk->[OLD]->[_END] + 1, $chunk->[NEW]->[_END] + 1); } else { $chunk->[NEW]->[_END] += 1; $chunk->[OLD]->[_END] += 1; } last; }; } } # Add final diff file section to resulting hash if ($filename) { push(@{$self->[LINEMAP]->{$filename}}, $chunk); _printChunk($chunk) if ($verbose); } # (the default root is applied by the constructor: it is needed whether a # diff was read or not) if ($self->empty()) { # this is probably OK - there are no differences between 'baseline' and current. lcovutil::ignorable_error($lcovutil::ERROR_EMPTY, "'diff' data file $diff_file contains no differences (this may be OK, if there are no difference between 'baseline' and 'current').\n" . "Make sure to use 'diff -u' when generating the diff file."); } return $self; } package InInterestingRegion; use constant { NEXT => 0, PREV => 1, STACK => 2, ANNOTATE_OBJ => 3, LINECOV_HASH => 4, # non-code line in a contiguous region before or after the # 'interesting' code lie - should also be included - # keep track of the length of that region, above and below LINES_BEFORE => 5, LINES_AFTER => 6, }; our $num_context_lines = 5; sub new { my ($class, $srcFileStruct, $lineCovHash) = @_; my $self = [undef, undef, Storable::dclone($srcFileStruct->interesting_lines()), $srcFileStruct, $lineCovHash ]; # we will call the $annotateObj->line(int) to collect annotate data # for certain lines - to find out of a non-code line is in a # contiguous region by the same author, in the same SHA or etc. $self->[NEXT] = shift(@{$self->[STACK]}); if (defined($self->[NEXT]) && $self->[NEXT] != 1) { _computeContextBefore($self, 0); } return bless $self, $class; } sub _computeContextBefore { my ($self, $prev) = @_; my $count = 0; my $data = $self->[LINECOV_HASH]; my $annotate = $self->[ANNOTATE_OBJ]; # find contiguous source region which matches the select criteria. # That might include non-code lined (e.g., comments) for (my $l = $self->[NEXT] - 1; $l > $prev; --$l) { my $lineData; $lineData = $data->{$l} if exists($data->{$l}); my $annotateData = $annotate->line($l); last unless SummaryInfo::selected($lineData, $annotateData, $self->[ANNOTATE_OBJ]->path(), $l); ++$count; } # region of interest is the (possibly empty) contiguous region we found # plus the number of context lines $self->[LINES_BEFORE] = $num_context_lines + $count; } sub interesting { my ($self, $lineNum) = @_; die("unexpected lineNum '$lineNum'") if $lineNum < 1; if (defined($self->[NEXT])) { if ($lineNum == $self->[NEXT]) { $self->[PREV] = $self->[NEXT]; $self->[NEXT] = shift(@{$self->[STACK]}); # compute following context lines - starting from here # and going to either end of file or next interesting line my $max = defined($self->[NEXT]) ? $self->[NEXT] : $self->[ANNOTATE_OBJ]->num_lines(); my $count = 0; my $data = $self->[LINECOV_HASH]; my $annotate = $self->[ANNOTATE_OBJ]; for (my $l = $self->[PREV] + 1; $l < $max; ++$l) { my $lineData; $lineData = $data->{$l} if exists($data->{$l}); my $annotateData = $annotate->line($l); last if !SummaryInfo::selected($lineData, $annotateData, $self->[ANNOTATE_OBJ]->path(), $l); ++$count; } $self->[LINES_AFTER] = $num_context_lines + $count; # and how many lines before the next interesting one? if (defined($self->[NEXT])) { $self->_computeContextBefore($self->[PREV]); } else { $self->[LINES_BEFORE] = 0; } return 1; } elsif ($lineNum >= $self->[NEXT] - $self->[LINES_BEFORE]) { return 1; } } if (defined($self->[PREV])) { if ($lineNum <= $self->[PREV] + $self->[LINES_AFTER]) { return 1; } $self->[PREV] = undef; } return 0; } package SourceLine; use constant { LINE => 0, TEXT => 1, OWNER => 2, FULL_NAME => 3, DATE => 4, AGE => 5, COMMIT => 6, LINE_TLA => 7, BRANCH_TLA => 8, MCDC_TLA => 9, FUNCTION_TLA => 10, # the LineData of this line, when it has call sites: the source # view annotates each site individually, so what is kept is the # whole list rather than one category CALL_SITES => 11, }; sub new { my $class = shift; # [lineNo, text, abbrev_name, full_name, date, age, commitID, lineTLA, branchTLA, # MCDC_TLA, functionTLA, callSites] my @data = @_; my $self = \@data; bless $self, $class; return $self; } sub to_list { # used by 'select' callback - if not a package my $self = shift; if ($#$self >= OWNER) { my @rtn = @{$self}[OWNER, FUNCTION_TLA]; return \@rtn; } return undef; } sub owner { my $self = shift; return $#$self >= OWNER ? $self->[OWNER] : undef; } sub full_name { my $self = shift; return $#$self >= FULL_NAME ? $self->[FULL_NAME] : undef; } # line coverage TLA sub tla { my ($self, $tla) = @_; $self->[LINE_TLA] = $tla if (defined($tla)); return $#$self >= LINE_TLA ? $self->[LINE_TLA] : undef; } sub branchElem { my ($self, $branchElem) = @_; $self->[BRANCH_TLA] = $branchElem if defined($branchElem); return $#$self >= BRANCH_TLA ? $self->[BRANCH_TLA] : undef; } sub mcdcElem { my ($self, $mcdcElem) = @_; $self->[MCDC_TLA] = $mcdcElem if defined($mcdcElem); return $#$self >= MCDC_TLA ? $self->[MCDC_TLA] : undef; } sub functionElem { my ($self, $funcElem) = @_; $self->[FUNCTION_TLA] = $funcElem if defined($funcElem); return $#$self >= FUNCTION_TLA ? $self->[FUNCTION_TLA] : undef; } sub callSiteElem { my ($self, $callSiteElem) = @_; $self->[CALL_SITES] = $callSiteElem if defined($callSiteElem); return $#$self >= CALL_SITES ? $self->[CALL_SITES] : undef; } sub commit { my $self = shift; return $#$self >= COMMIT ? $self->[COMMIT] : undef; } sub date { my $self = shift; return $#$self >= DATE ? $self->[DATE] : undef; } sub age { my $self = shift; return $#$self >= AGE ? $self->[AGE] : undef; } sub line { my $self = shift; return $self->[LINE]; } sub text { my $self = shift; return $self->[TEXT]; } package SourceFile; our @annotateScript; our $annotateCallback; our $annotateTooltip = 'Line %l: commit %C on %d by %F'; our $annotatedFiles = 0; our $totalFiles = 0; use constant { PATH => 0, LINES => 1, INTERESTING_LINES => 2, LINE_OWNERS => 3, LINE_CATEGORIES => 4, BRANCH_OWNERS => 5, BRANCH_CATEGORIES => 6, MCDC_OWNERS => 7, MCDC_CATEGORIES => 8, # a call site is one line, so - unlike a function - it has one # owner and one date CALLGRAPH_OWNERS => 9, CALLGRAPH_CATEGORIES => 10, # line -> the SourceLine which gives the date and owner of the # coverpoints on that line, for the lines of the multi-line # statements whose most recent edit is not on the line itself ATTRIBUTION => 11, }; # the coverpoint types which keep a (owner, category) pair of slots, so that the # allocation in 'new' and the pruning in 'simplify' do not each carry their own # copy of the list our @ownerCategorySlots = (LINE_OWNERS, LINE_CATEGORIES, BRANCH_OWNERS, BRANCH_CATEGORIES, MCDC_OWNERS, MCDC_CATEGORIES, CALLGRAPH_OWNERS, CALLGRAPH_CATEGORIES); sub new { my ($class, $filepath, $fileSummary, $fileCovInfo, $hasNoBaselineData) = @_; (ref($fileSummary) eq 'SummaryInfo' && ref($fileCovInfo) eq "FileCoverageInfo") or die("unexpected input args"); my $self = [$filepath, [], # lines undef, # list of interesting lines ]; foreach my $t (@ownerCategorySlots) { # table of owner -> hash of TLAs->list of lines for coverpoint type # table of TLA -> list of lines holding that TLA for coverpoint push(@$self, {}); } push(@$self, {}); # attribution bless $self, $class; $fileSummary->fileDetails($self); # use the line coverage count to synthesize a fake file, if we can't # find an actual file $self->_load($fileCovInfo); $self->_attributeStatements($fileCovInfo); if ($hasNoBaselineData) { my $fileAge = $self->age(); if (defined($fileAge) && (!defined($main::age_basefile) || $fileAge > $main::age_basefile) ) { # go through the fileCov data and change UIC->UBC, GIC->CBC # - pretend that we already saw this file data - this is the first # coverage report which contains this data. $fileCovInfo->recategorizeTlaAsBaseline(); } } # sort lines in ascending numerical order - we want the 'owner' # and 'tla' line lists to be sorted - and it is probably faster to # sort the file list once than to sort each of the sub-lists # individually afterward. # DCB, DUB category keys have leading "<<<" characters - which we strip # in order to compare my $currentTla; my $regionStartLine; my $lineCovData = $fileCovInfo->lineMap(); my @lineArray; # interesting line numbers my $inRegion; if (defined($selectCallback)) { # This implementation looks for all the 'line' coverpoints - then # expands the selected region around those points - e.g., by # adding noncode lines which are in the selected changelist and # surrounding the selected region with 'num_context_lines' of # context. # A side effect of this criteria is that disjoint noncode regions - # e.g., comments or unused #ifdef code - will not be selected. # A different implementation would go through the annotated source # and mark all selected lines, then go through again to add # context. # It isn't clear which approach is best - but the current approach # seems to match user expectations - so I'm going with it, at # least for now. # Not too hard to do - but significant overkill - to make the # approach configurable. Let's see if there is user demand. while (my ($line, $lne) = each(%$lineCovData)) { # ignore deleted lines as they don't appear in source listing my $tla = $lne->tla(); my $deleted = exists($SummaryInfo::deletedTla{$tla}); # no annotations for deleted line my $annotateData; $annotateData = $self->line($line) unless $deleted; # callback arguments are (LineData, SourceLine) if (SummaryInfo::selected($lne, $annotateData, $self->path(), $line)) { # A deleted line is keyed '<<lineNo('current') : $line); } } # unique as well as ascending: 'interesting' advances its stack only on # an exact match, so a repeated entry would stick there my %seenLine; @lineArray = sort({ $a <=> $b } grep({ !$seenLine{$_}++ } @lineArray)); $self->[INTERESTING_LINES] = \@lineArray; $inRegion = InInterestingRegion->new($self, $lineCovData); } # deleted line associated with first 'current' line above - # or line below, if there is no current line above. The lines are visited # in the order of the line they are displayed at, which is what the region # is a stream over: 'interesting' only moves forward, and so does the # source view which asks it the same question again. A deleted line's own # number is a line of the baseline, which says nothing about where it is # among the current ones my %displayedAt; # category lists which hold a coverpoint out of order - see # _countBranchTlaData my %displaced; while (my ($line, $lne) = each(%$lineCovData)) { $displayedAt{$line} = exists($SummaryInfo::deletedTla{$lne->tla()}) ? -$lne->lineNo('current') : $line; } foreach my $line (sort({ my $ka = ("<" ne substr($a, 0, 1)) ? $a : substr($a, 3); my $kb = ("<" ne substr($b, 0, 1)) ? $b : substr($b, 3); $displayedAt{$a} <=> $displayedAt{$b} || $ka <=> $kb } keys(%{$lineCovData})) ) { # is this line within N of an interesting one? my $lne = $lineCovData->{$line}; my $tla = $lne->tla(); my $lineNum = $displayedAt{$line}; if (defined($inRegion) && !$inRegion->interesting($lineNum)) { lcovutil::info(1, " drop $line" . ($line eq $lineNum ? '' : " ($lineNum)") . "\n"); $fileSummary->removeLine($lne); delete($lineCovData->{$line}); next; } $self->_countLineTlaData($line, $lne, $fileSummary); $displaced{+BRANCH_CATEGORIES} = 1 if ($lcovutil::br_coverage && defined($lne->differential_branch()) && $self->_countBranchTlaData($line, $lne, $fileSummary)); $displaced{+MCDC_CATEGORIES} = 1 if ($lcovutil::mcdc_coverage && defined($lne->differential_mcdc()) && $self->_countMcdcTlaData($line, $lne, $fileSummary)); $self->_countFunctionTlaData($line, $lne, $fileSummary) if ($lcovutil::func_coverage && defined($lne->differential_function())); $self->_countCallgraphTlaData($line, $lne, $fileSummary) if ($lcovutil::callgraph_coverage && $lne->num_call_sites()); } foreach my $category (keys(%displaced)) { foreach my $list (values(%{$self->[$category]})) { # the order and the uniqueness binarySearchLine expects my %seen; @$list = map({ $_->[1] } sort({ $a->[0] <=> $b->[0] } map({ [/^<*([0-9]+)$/, $_] } grep({ !$seen{$_}++ } @$list)))); } } if (defined($main::show_dateBins) && %$lineCovData && # haven't filtered out everything !$self->isProjectFile() ) { lcovutil::info("no owner/date info for '$filepath'\n"); } return $self; } sub simplify { my $self = shift; # retain only the information required to populate the hyperlinks in # the parent 'directory details' table # This struct can be huge, for a large, complicated file $self->[INTERESTING_LINES] = undef; my %keep; # keep the first line in each date bin.. for (my $bin = 0; $bin <= $#ageGroupHeader; ++$bin) { foreach my $tla (keys %{$self->[LINE_CATEGORIES]}) { # at least for the moment, skip deleted lines next if exists($SummaryInfo::deletedTla{$tla}); my $line = $self->nextInDateBin($bin, $tla); $keep{$line} = 1 if defined($line); } if ($lcovutil::br_coverage) { foreach my $tla (keys %{$self->[BRANCH_CATEGORIES]}) { next if exists($SummaryInfo::unbinnedTla{$tla}); my $line = $self->nextBranchInDateBin($bin, $tla); $keep{$line} = 1 if defined($line); } } if ($lcovutil::mcdc_coverage) { foreach my $tla (keys %{$self->[MCDC_CATEGORIES]}) { next if exists($SummaryInfo::unbinnedTla{$tla}); my $line = $self->nextMcdcInDateBin($bin, $tla); $keep{$line} = 1 if defined($line); } } if ($lcovutil::callgraph_coverage) { foreach my $tla (keys %{$self->[CALLGRAPH_CATEGORIES]}) { next if exists($SummaryInfo::unbinnedTla{$tla}); my $line = $self->nextCallgraphInDateBin($bin, $tla); $keep{$line} = 1 if defined($line); } } } # retain first location of each TLA (globally) and each TLA for each owner foreach my $bin (@ownerCategorySlots) { while (my ($key, $list) = each(%{$self->[$bin]})) { if ('ARRAY' eq ref($list)) { next if exists($SummaryInfo::unbinnedTla{$key}); # retain the first entry and any interesting entries $keep{$list->[0]} = 1; @$list = grep(exists($keep{$_}), @$list); } else { die("unexpected type") unless 'HASH' eq ref($list); while (my ($tla, $l) = each(%$list)) { next if exists($SummaryInfo::unbinnedTla{$tla}); # retain the first entry $keep{$l->[0]} = 1; @$l = grep(exists($keep{$_}), @$l); } } } } # throw away everything we aren't interested in for (my $l = $self->num_lines(); $l > 0; --$l) { if (!exists($keep{$l})) { $self->[LINES]->[$l - 1] = undef; } } } sub _countBranchTlaData { my ($self, $line, $lineData, $fileSummary) = @_; my $differentialData = $lineData->differential_branch(); my ($src_age, $developer, $srcLine); my $lineTla = $lineData->tla(); if ($SummaryInfo::tlaLocation{$lineTla} & 0x1) { # skip check if line was deleted - no age/owner data for deleted code $srcLine = $self->line($line); if (!defined($srcLine)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no data for 'branch' line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); } else { $srcLine->branchElem($differentialData); if (@SourceFile::annotateScript && !grep(/^$lineTla$/, ('DUB', 'DCB'))) { # deleted lines don't have owner data... # if this line is not in the project (e.g., from some 3rd party # library - then we might not have file history for it. # ..of the statement the line is part of my $who = $self->attribution($line); $src_age = $who->age(); $developer = $who->owner(); if (defined($developer)) { my $shash = $self->[BRANCH_OWNERS]; if (!exists($shash->{$developer})) { $shash->{$developer} = {}; $shash->{$developer}->{lines} = []; } my $l = $shash->{$developer}->{lines}; # NOTE: string compare as either line number could be # "<<[-1] eq $line; } } } } # the navigation links go to the line the block is written at - see # BranchBlock::locationOffset. Such a line is out of order in the # category lists, which the caller sorts if required my $displaced = 0; foreach my $block ($differentialData->blocks()) { my $diff = $block->elements(); my $blockId = $block->idx(); my $at = $line; my $offset = $block->locationOffset(); if ($offset && '<' ne substr($line, 0, 1)) { $at += $offset; $displaced = 1; } foreach my $br (@$diff) { my $tla = $br->tla(); # don't count excluded coverpoints unless (defined($tla)) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef TLA for branch $blockId of " . $self->path() . ":$at - lineTLA:$lineTla taken:" . $br->data()); next; # LCOV_UNREACHABLE_STOP } next if exists($SummaryInfo::excludedCoverpointTla{$tla}); $fileSummary->[SummaryInfo::BRANCH_DATA]->[SummaryInfo::DATA] ->{$tla} += 1; # keep track of all the branch TLAs found on this line... $self->[BRANCH_CATEGORIES]->{$tla} = [] unless exists($self->[BRANCH_CATEGORIES]->{$tla}); my $l = $self->[BRANCH_CATEGORIES]->{$tla}; # see 'string compare' comment above push(@$l, $at) unless @$l && $l->[-1] eq $at; next if (0 == ($SummaryInfo::tlaLocation{$tla} & 0x1)); # skip "DUB' and 'DCB' categories - which are not in current # and thus have no line associated # and the age... #lcovutil::info("$l: $tla" . $lineData->in_curr() . "\n"); unless (defined($srcLine) && defined($src_age)) { # No annotation data, so there is no age bin to update. # The per-TLA total was already incremented above - do NOT # count it again here (that double-counted every branch # coverpoint in the TLA columns). next; } # increment count of branches of this age we found for this TLA $fileSummary->branchCovCount($tla, "age", $src_age, 1); # HGC: could clean this up...no need to keep track # of 'hit' as we can just compute from CBC + GNC + ... # found another line... my $hit = exists($SummaryInfo::hitTla{$tla}); $fileSummary->branchCovCount("found", "age", $src_age, 1); $fileSummary->branchCovCount("hit", "age", $src_age, 1) if $hit; next unless defined($developer); # add this line to those that belong to this owner.. # first: increment line count in 'file summary' my $ohash = $fileSummary->[SummaryInfo::BRANCH_DATA]->[SummaryInfo::OWNERS]; if (!exists($ohash->{$developer})) { my %data = ('hit' => $hit ? 1 : 0, 'found' => 1, $tla => 1); $ohash->{$developer} = \%data; } else { my $d = $ohash->{$developer}; $d->{$tla} = 0 unless exists($d->{$tla}); $d->{$tla} += 1; $d->{found} += 1; $d->{hit} += 1 if $hit; } # now append this branchTLA to the owner... my $dhash = $self->[BRANCH_OWNERS]->{$developer}; die("expected initialized") unless defined($dhash); $dhash->{$tla} = [] unless exists($dhash->{$tla}); my $ll = $dhash->{$tla}; # see 'string compare' comment above push(@$ll, $line) unless @$ll && $ll->[-1] eq $line; } } return $displaced; } sub _countMcdcTlaData { my ($self, $line, $lineData, $fileSummary) = @_; my $differentialData = $lineData->differential_mcdc(); my ($src_age, $developer, $srcLine); my $lineTla = $lineData->tla(); if ($SummaryInfo::tlaLocation{$lineTla} & 0x1) { # skip check if line was deleted - no age/owner data for deleted code $srcLine = $self->line($line); if (!defined($srcLine)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no data for 'MC/DC' line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); } else { $srcLine->mcdcElem($differentialData); if (@SourceFile::annotateScript && !grep(/^$lineTla$/, ('DUB', 'DCB'))) { # deleted lines don't have owner data... # if this line is not in the project (e.g., from some 3rd party # library - then we might not have file history for it. # ..of the statement the line is part of my $who = $self->attribution($line); $src_age = $who->age(); $developer = $who->owner(); if (defined($developer)) { my $shash = $self->[MCDC_OWNERS]; if (!exists($shash->{$developer})) { $shash->{$developer} = {}; $shash->{$developer}->{lines} = []; } my $l = $shash->{$developer}->{lines}; # see 'string compare' comment in branch method, above push(@$l, $line) unless @$l && $l->[-1] eq $line; } } } } # see the 'displaced' comment in _countBranchTlaData my $displaced = 0; my $mcdcGroups = $differentialData->groups(); while (my ($groupKey, $group) = each(%$mcdcGroups)) { my $at = $line; my ($size, $offset) = MCDC_Block::parseGroupKey($groupKey); if ($offset && '<' ne substr($line, 0, 1)) { $at += $offset; $displaced = 1; } foreach my $expr (@$group) { foreach my $sense (0, 1) { my $count = $expr->count($sense); my $tla = $count->[0]; # don't count excluded coverpoints next if exists($SummaryInfo::excludedCoverpointTla{$tla}); unless (defined($tla)) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error( $lcovutil::ERROR_INTERNAL, "undef TLA for sense $sense of MC/DC " . $expr->index() . ' of ' . $self->path() . ":$at - lineTLA:$lineTla taken:" . $count->[1]); next; # LCOV_UNREACHABLE_STOP } $fileSummary->[SummaryInfo::MCDC_DATA]->[SummaryInfo::DATA] ->{$tla} += 1; # keep track of all the branch TLAs found on this line... if (!exists($self->[MCDC_CATEGORIES]->{$tla})) { $self->[MCDC_CATEGORIES]->{$tla} = []; } my $l = $self->[MCDC_CATEGORIES]->{$tla}; # see 'string compare' comment in branch method, above push(@$l, $at) unless @$l && $l->[-1] eq $at; next if (0 == ($SummaryInfo::tlaLocation{$tla} & 0x1)); # skip "DUB' and 'DCB' categories - which are not in current # and thus have no line associated # and the age... #lcovutil::info("$l: $tla" . $lineData->in_curr() . "\n"); unless (defined($srcLine) && defined($src_age)) { # see comment in _countBranchTlaData: the per-TLA total # was already incremented above, and there is no age # bin to update without annotation data next; } # increment count of branches of this age we found for this TLA $fileSummary->mcdcCovCount($tla, "age", $src_age, 1); my $hit = exists($SummaryInfo::hitTla{$tla}); $fileSummary->mcdcCovCount("found", "age", $src_age, 1); $fileSummary->mcdcCovCount("hit", "age", $src_age, 1) if $hit; next unless defined($developer); # add this line to those that belong to this owner.. # first: increment line count in 'file summary' my $ohash = $fileSummary->[SummaryInfo::MCDC_DATA] ->[SummaryInfo::OWNERS]; if (!exists($ohash->{$developer})) { my %data = ('hit' => $hit ? 1 : 0, 'found' => 1, $tla => 1); $ohash->{$developer} = \%data; } else { my $d = $ohash->{$developer}; $d->{$tla} = 0 unless exists($d->{$tla}); $d->{$tla} += 1; $d->{found} += 1; $d->{hit} += 1 if $hit; } # now append this mcdc TLA to the owner... my $dhash = $self->[MCDC_OWNERS]->{$developer}; die("expected initialized") unless defined($dhash); $dhash->{$tla} = [] unless exists($dhash->{$tla}); my $ll = $dhash->{$tla}; # see 'string compare' comment in branch method, above push(@$ll, $line) unless @$ll && $ll->[-1] eq $line; } } } return $displaced; } sub _countCallgraphTlaData { # The per-line accumulator for call sites. # The call site is associated with a particular line in the source - # so has annotation data. (In contrast: a function isn't associated # with a single line - so we don't have an author or age. my ($self, $line, $lineData, $fileSummary) = @_; my ($src_age, $developer, $srcLine); my $lineTla = $lineData->tla(); if ($SummaryInfo::tlaLocation{$lineTla} & 0x1) { # skip check if line was deleted - no age/owner data for deleted code $srcLine = $self->line($line); if (!defined($srcLine)) { # A call site beyond the end of the source is caught by the range # check before this point - an error, or with '--ignore range' the # source is padded out to it, and '--filter range' drops the site - # and a missing source is an error or is synthesized in full. So # every current-source line with a site has source text here # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no data for 'callgraph' line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); # LCOV_UNREACHABLE_STOP } else { $srcLine->callSiteElem($lineData); if (@SourceFile::annotateScript && !grep(/^$lineTla$/, ('DUB', 'DCB'))) { # deleted lines don't have owner data... # if this line is not in the project (e.g., from some 3rd party # library - then we might not have file history for it. # ..of the statement the line is part of my $who = $self->attribution($line); $src_age = $who->age(); $developer = $who->owner(); if (defined($developer)) { my $shash = $self->[CALLGRAPH_OWNERS]; if (!exists($shash->{$developer})) { $shash->{$developer} = {}; $shash->{$developer}->{lines} = []; } my $l = $shash->{$developer}->{lines}; # see 'string compare' comment in branch method, above push(@$l, $line) unless @$l && $l->[-1] eq $line; } } } } foreach my $site ($lineData->call_sites()) { my $tla = $site->tla(); $fileSummary->[SummaryInfo::CALLGRAPH_DATA]->[SummaryInfo::DATA]->{$tla} += 1; # keep track of all the callee TLAs found on this line... $self->[CALLGRAPH_CATEGORIES]->{$tla} = [] unless exists($self->[CALLGRAPH_CATEGORIES]->{$tla}); my $l = $self->[CALLGRAPH_CATEGORIES]->{$tla}; # see 'string compare' comment in branch method, above push(@$l, $line) unless @$l && $l->[-1] eq $line; next if (0 == ($SummaryInfo::tlaLocation{$tla} & 0x1)); # skip 'DUB' and 'DCB' categories - which are not in current and thus # have no line associated unless (defined($srcLine) && defined($src_age)) { # see comment in _countBranchTlaData: the per-TLA total was already # incremented above, and there is no age bin to update without # annotation data next; } # increment count of call sites of this age we found for this TLA $fileSummary->callgraphCovCount($tla, "age", $src_age, 1); my $hit = exists($SummaryInfo::hitTla{$tla}); $fileSummary->callgraphCovCount("found", "age", $src_age, 1); $fileSummary->callgraphCovCount("hit", "age", $src_age, 1) if $hit; next unless defined($developer); # add this line to those that belong to this owner.. # first: increment site count in 'file summary' my $ohash = $fileSummary->[SummaryInfo::CALLGRAPH_DATA]->[SummaryInfo::OWNERS]; if (!exists($ohash->{$developer})) { my %data = ('hit' => $hit ? 1 : 0, 'found' => 1, $tla => 1); $ohash->{$developer} = \%data; } else { my $d = $ohash->{$developer}; $d->{$tla} = 0 unless exists($d->{$tla}); $d->{$tla} += 1; $d->{found} += 1; $d->{hit} += 1 if $hit; } # now append this callee TLA to the owner... my $dhash = $self->[CALLGRAPH_OWNERS]->{$developer}; die("expected initialized") unless defined($dhash); $dhash->{$tla} = [] unless exists($dhash->{$tla}); my $ll = $dhash->{$tla}; # see 'string compare' comment in branch method, above push(@$ll, $line) unless @$ll && $ll->[-1] eq $line; } } sub _countLineTlaData { my ($self, $line, $lineData, $fileSummary) = @_; # there is differential line coverage data... my $tla = $lineData->tla(); if (!exists($SummaryInfo::tlaLocation{$tla})) { # this case can happen if the line number annotations are # wrong in the .info file - so the first line of some function # or some branch coverage line number turns out not to be an # executable source code line lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY, "unexpected category $tla for line " . $self->path() . ":$line"); return; } # one more line in this bucket... $fileSummary->[SummaryInfo::LINE_DATA]->[SummaryInfo::DATA]->{$tla} += 1; # create the category list, if necessary $self->[LINE_CATEGORIES]->{$tla} = [] unless exists($self->[LINE_CATEGORIES]->{$tla}); push(@{$self->[LINE_CATEGORIES]->{$tla}}, $line); # and the age data... if ($SummaryInfo::tlaLocation{$tla} & 0x1) { # skip "DUB' and 'DCB' categories - which are not in current # and thus have no line associated # @todo if desired, we _could_ enhance the 'annotate' callback # to look up the baseline SHA/CL and get age/author information # from that. Seems unnecessary, though. #lcovutil::info("$l: $tla" . $lineData->in_curr() . "\n"); my $l = $self->line($line); if (!defined($l)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no data for line:$line, TLA:$tla, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); return; } # set the TLA of this line... $l->tla($tla); # the date and owner are those of the statement the line is part of $l = $self->attribution($line); my $src_age = $l->age(); # if this line is not in the project (e.g., from some 3rd party # library - then we might not have file history for it. return unless defined($src_age); # increment count of lines of this age we found for this TLA $fileSummary->lineCovCount($tla, "age", $src_age, 1); if ($lineData->in_curr()) { # HGC: could clean this up...no need to keep track # of 'hit' as we can just compute from CBC + GNC + ... # found another line... $fileSummary->lineCovCount("found", "age", $src_age, 1); if ($lineData->curr_count() > 0) { $fileSummary->lineCovCount("hit", "age", $src_age, 1); } } if (defined($l->owner())) { # add this line to those that belong to this owner.. my $developer = $l->owner(); # first: increment line count in 'file summary' my $ohash = $fileSummary->[SummaryInfo::LINE_DATA]->[SummaryInfo::OWNERS]; $ohash->{$developer} = {} unless exists($ohash->{$developer}); my $d = $ohash->{$developer}; $d->{$tla} = 0 unless exists($d->{$tla}); $d->{$tla} += 1; # now push this line onto the list of in this file, belonging # to this owner $self->[LINE_OWNERS]->{$developer} = {} unless exists($self->[LINE_OWNERS]->{$developer}); my $dhash = $self->[LINE_OWNERS]->{$developer}; $dhash->{lines} = [] unless exists($dhash->{lines}); push(@{$dhash->{lines}}, $line); $dhash->{$tla} = [] unless exists($dhash->{$tla}); # and the list of lines with this TLA, belonging to this user push(@{$dhash->{$tla}}, $line); } } } sub _accountFunction { my ($fileSummary, $tla, $src_age) = @_; unless (defined($tla)) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef function TLA for age '$src_age' of " . $fileSummary->name()); return 1; # error # LCOV_UNREACHABLE_STOP } $fileSummary->[SummaryInfo::FUNCTION_DATA]->[SummaryInfo::DATA]->{$tla} += 1; if (defined($src_age)) { $fileSummary->functionCovCount($tla, 'age', $src_age, 1); my $hit = exists($SummaryInfo::hitTla{$tla}); $fileSummary->functionCovCount("found", "age", $src_age, 1); $fileSummary->functionCovCount("hit", "age", $src_age, 1) if $hit; } return 0; } sub _countFunctionTlaData { my ($self, $line, $lineData, $fileSummary) = @_; my $func = $lineData->differential_function(); my %foundFunctionTlas; my ($src_age, $developer, $srcLine); my $h = $func->hit(); my $mergedTla = $h->[1]; if (!defined($mergedTla)) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef TLA for function '" . $func->name() . "' hit " . $h->[0] . " at line " . $line . " (" . $lineData->tla() . ' ' . $lineData->curr_count() . ")"); #die("this should not happen"); # This is new code - somehow miscategorized. $mergedTla = $h->[0] == 0 ? 'UNC' : 'GNC'; $h->[1] = $mergedTla; #return; # LCOV_UNREACHABLE_STOP } if (!grep(/^$mergedTla$/, ('DUB', 'DCB'))) { # deleted lines don't have owner data... $srcLine = $self->line($line); # info might not be available, if no annotations if (defined($srcLine)) { # should probably look at the source code to find the open/close # parens - then claim the age is the youngest line $src_age = $srcLine->age(); $srcLine->functionElem($func); } } if ($main::merge_function_aliases) { _accountFunction($fileSummary, $mergedTla, $src_age); } else { foreach my $alias ($func->aliasNames()) { my $data = $func->alias($alias); my $tla = $data->tla(); if (!defined($tla)) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef TLA for alias:'$alias' hit:" . $data->count() . " of function '" . $func->name() . "' hit " . $h->[0] . " at line " . $line . " (" . $lineData->tla() . ' ' . $lineData->curr_count() . ")"); # die("this should not happen either"); $tla = $data->count() == 0 ? 'UNC' : 'GNC'; $data->set_tla($tla); # LCOV_UNREACHABLE_STOP } _accountFunction($fileSummary, $tla, $src_age); } } } sub path { my $self = shift; return $self->[PATH]; } sub isProjectFile { # return 'true' if no owner/date information for this file. my $self = shift; return scalar(%{$self->[LINE_OWNERS]}); } sub locationHref { # what a link from the page header to one line of this file says - see # CallgraphPageIndex::locationHref, which is the same question asked of a # callees page my ($self, $line) = @_; return "#L$line"; } sub line { my $self = shift; my $i = shift; die("bad line index '$i'") unless ($i =~ /^[0-9]+$/); return $self->[LINES]->[$i - 1]; } sub spanAnnotation { # @return the SourceLine of the most recent edit of lines 'first'..'last': # the one which supplies the date and owner of a statement written over # those lines. Of two edits made at the same time, the lower line's wins. # A line with no annotation does not count, nor does a synthesized one if # any real line has an annotation. Undef if no line has one my ($self, $first, $last) = @_; my ($best, $bestWhen, $synthetic); foreach my $i ($first .. $last) { my $l = $self->line($i); next unless defined($l) && defined($l->age()); if (defined($l->owner()) && $l->owner() eq 's.n.thetic') { $synthetic = $l unless defined($synthetic); next; } if (defined($best)) { next if $l->age() > $best->age(); if ($l->age() == $best->age()) { # the same day: compare the time of the edit $bestWhen = lcovutil::parse_w3cdtf($best->date())->epoch() unless defined($bestWhen); my $when = lcovutil::parse_w3cdtf($l->date())->epoch(); next if $when <= $bestWhen; $bestWhen = $when; } else { $bestWhen = undef; } } $best = $l; } return defined($best) ? $best : $synthetic; } sub _attributeStatements { # find the SourceLine which gives the date and owner of each line of each # multi-line statement - see 'spanAnnotation' my ($self, $fileCovInfo) = @_; my $map = $self->[ATTRIBUTION]; my %seen; while (my ($line, $stmt) = each(%{$fileCovInfo->statements()})) { next if $line > $self->num_lines(); $seen{$stmt} = $self->spanAnnotation(@$stmt) unless exists($seen{$stmt}); my $l = $seen{$stmt}; $map->{$line} = $l if (defined($l) && $l != $self->line($line)); } } sub attribution { # the SourceLine which gives the date and owner of the coverpoints written on # line '$line': that of its statement if it is part of a multi-line one, # and otherwise its own my ($self, $line) = @_; my $l = $self->[ATTRIBUTION]->{$line}; return defined($l) ? $l : $self->line($line); } # how old is the oldest (or youngest) line in this file? # returns undef if this file has no annotation data at all. # Note that we cannot use 'isProjectFile' as the guard here: the owner # bins which it looks at are not filled in until '_countLineTlaData' has # visited every line - which happens after the constructor's # 'recategorizeTlaAsBaseline' check calls us. Look at the per-line # annotation data which '_load' has already stored, instead. sub age { my ($self, $youngest) = @_; my $age; foreach my $line (@{$self->lines()}) { my $a = $line->age(); next unless defined($a); if (!defined($age) || (defined($youngest) ? $a < $age : $a > $age)) { $age = $a; } } return $age; } sub lines { my $self = shift; return $self->[LINES]; } sub num_lines { my $self = shift; return scalar(@{$self->[LINES]}); } sub is_empty { my $self = shift; # anything interesting here? return defined($self->interesting_lines()) && 0 == @{$self->interesting_lines()}; } sub interesting_lines { my $self = shift; # return list of lines containing coverpoints, which are marked by # the $selectionCallback - see the --select-script parameter return $self->[INTERESTING_LINES]; } sub binarySearchLine { my ($list, $after) = @_; defined($list) && 0 != scalar(@$list) or die("invalid location list"); my $max = $#$list; my $min = 0; my $mid; while (1) { $mid = int(($max + $min) / 2); my ($v) = $list->[$mid] =~ /^<*([0-9]+)$/; # handle deleted line markers if ($v > $after) { $max = $mid; } elsif ($v < $after) { $min = $mid; } else { while ($list->[$mid] =~ /<< $#$list; } return $mid; } my $diff = $max - $min; if ($diff <= 1) { $mid = $min; my ($v) = $list->[$min] =~ /^<*([0-9]+)$/; $mid = $max if $v < $after; last; } } while ($list->[$mid] =~ /<< $#$list; } return $list->[$mid] >= $after ? $mid : undef; } sub nextTlaGroup { # return number of first line of next group of specified linecov TLA - # for example, if line [5:8] and [13:17] are 'CNC', then: # 5 = nextTlaGroup('CBC') : "after == undef" # 13 = nextTlaGroup('CBC', 5) : skip contiguous group of CBC lines # undef = nextTlaGroup('CBC', 13) : no following group my ($self, $tla, $after) = @_; if (!exists($SummaryInfo::tlaLocation{$tla})) { lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY, "unknown linecov TLA '$tla'"); return undef; } # note that the "$self->line(...)" argument is 1-based (not zero-base) my $line; if (defined($after) && defined($self->line($after)->tla())) { die("$after is not in $tla group") unless ($self->line($after)->tla() eq $tla); # skip to the end of the current section... # if the TLA of this group is unset (non-code line: comment, # blank, etc) - then look for next code line. If that line's # TLA matches, then treat as a contiguous group. # This way, we avoid visual clutter from having a lot of single-line # TLA segments. my $lastline = scalar(@{$self->[LINES]}); for ($line = $after + 1; $line <= $lastline; ++$line) { my $t = $self->line($line)->tla(); last if (defined($t) && $t ne $tla); } } else { $line = 1; } my $locations = $self->[LINE_CATEGORIES]->{$tla}; my $idx = binarySearchLine($locations, $line); return defined($idx) ? $locations->[$idx] : undef; } sub nextCategoryTlaGroup { # return number of first line of next group of specified branchcov TLA - # note that all branch lines are independent - so we will # report and go the next branch, even if it is on the adjacent line my ($type, $category, $self, $tla, $after) = @_; if (!exists($SummaryInfo::tlaLocation{$tla})) { lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY, "unknown $type TLA '$tla'"); return undef; } die("no $type data for TLA '$tla'") unless exists($self->[$category]->{$tla}); my $locations = $self->[$category]->{$tla}; $after = defined($after) ? $after + 1 : 1; my $idx = binarySearchLine($locations, $after); return defined($idx) ? $locations->[$idx] : undef; } sub nextBranchTlaGroup { # ($self, $tla, $after_line) return nextCategoryTlaGroup('branch', BRANCH_CATEGORIES, @_); } sub nextMcdcTlaGroup { return nextCategoryTlaGroup('MC/DC', MCDC_CATEGORIES, @_); } sub nextCallgraphTlaGroup { return nextCategoryTlaGroup('callgraph', CALLGRAPH_CATEGORIES, @_); } sub nextInDateBin { my ($self, $dateBin, $tla, $after) = @_; if (!exists($SummaryInfo::tlaLocation{$tla})) { lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY, "unknown linecov TLA '$tla'"); return undef; } $dateBin <= $#SummaryInfo::ageGroupHeader or die("unexpected age group $dateBin"); # note that the "$self->line(...)" argument is 1-based (not zero-base) my $line; if (defined($after)) { die("$after is not in $tla group") unless ($self->line($after)->tla() eq $tla); my $lastline = scalar(@{$self->[LINES]}); # skip to the end of the current section... for ($line = $after + 1; $line <= $lastline; ++$line) { my $t = $self->line($line)->tla(); my $a = $self->attribution($line)->age(); if (!defined($a)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no age data for line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); return undef; } last if (defined($t) && ($t ne $tla || $dateBin != SummaryInfo::findAgeBin($a))); } } else { $line = 1; } # the data isn't stored by date bin (at least for now) - so the # only way to find it currently is by searching forward. my $tlaLocations = $self->[LINE_CATEGORIES]->{$tla}; my $idx = binarySearchLine($tlaLocations, $line); return undef unless defined($idx); my $max = scalar(@$tlaLocations); for (; $idx < $max; ++$idx) { $line = $tlaLocations->[$idx]; my $l = $self->line($line); if (!defined($l)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no data for line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); return undef; } my $a = $self->attribution($line)->age(); if (!defined($a)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no age data for line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); return undef; } my $bin = SummaryInfo::findAgeBin($a); if ($bin == $dateBin) { my $t = $l->tla(); return $line if (defined($t) && $t eq $tla); } } return undef; } sub nextInOwnerBin { my ($self, $owner, $tla, $after) = @_; if (!exists($SummaryInfo::tlaLocation{$tla})) { lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY, "unknown linecov TLA '$tla'"); return undef; } unless (exists($self->[LINE_OWNERS]->{$owner}) && exists($self->[LINE_OWNERS]->{$owner}->{$tla})) { # LCOV_UNREACHABLE_START lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "$owner not responsible for any $tla lines in" . $self->path()); return undef; # LCOV_UNREACHABLE_STOP } # note that the "$self->line(...)" argument is 1-based (not zero-base) my $line; if (defined($after)) { ($self->line($after)->tla() eq $tla) or die("$after is not in $tla group"); my $lastline = scalar(@{$self->[LINES]}); # skip to the end of the current section... for ($line = $after + 1; $line <= $lastline; ++$line) { my $l = $self->line($line); if (!defined($l)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no data for line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); return undef; } my $t = $l->tla(); my $o = $self->attribution($line)->owner(); if (!defined($o)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no owner data for line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); return undef; } last if (defined($t) && ($t ne $tla || $o ne $owner)); } } else { $line = 1; } my $locations = $self->[LINE_OWNERS]->{$owner}->{$tla}; my $idx = binarySearchLine($locations, $line); return defined($idx) ? $locations->[$idx] : undef; } sub nextCategoryInDateBin { my ($type, $category, $self, $dateBin, $tla, $after) = @_; if (!exists($SummaryInfo::tlaLocation{$tla})) { lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY, "unknown $type TLA '$tla'"); return undef; } $dateBin <= $#SummaryInfo::ageGroupHeader or die("unexpected age group $dateBin"); # note that the "$self->line(...)" argument is 1-based (not zero-base) $after = defined($after) ? $after + 1 : 1; die("no $type $tla in " . $self->path()) unless exists($self->[$category]->{$tla}); my $lines = $self->[$category]->{$tla}; my $idx = binarySearchLine($lines, $after); return undef unless defined($idx); my $max = scalar(@$lines); for (; $idx < $max; ++$idx) { my $line = $lines->[$idx]; my $l = $self->line($line); if (!defined($l)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no data for line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); next; } my $a = $self->attribution($line)->age(); if (!defined($a)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no age data for line:$line, file:" . $self->path()) if (!$lcovutil::warn_once_per_file || lcovutil::warn_once($lcovutil::ERROR_UNMAPPED_LINE, $self->path())); return undef; } my $bin = SummaryInfo::findAgeBin($a); if ($bin == $dateBin) { return $line; } } return undef; } sub nextBranchInDateBin { # ($self, $dateBin, $tla, $after) = @_; return nextCategoryInDateBin('branch', BRANCH_CATEGORIES, @_); } sub nextMcdcInDateBin { return nextCategoryInDateBin('MC/DC', MCDC_CATEGORIES, @_); } sub nextCallgraphInDateBin { return nextCategoryInDateBin('callgraph', CALLGRAPH_CATEGORIES, @_); } sub nextCategoryInOwnerBin { my ($type, $category, $self, $owner, $tla, $after) = @_; if (!exists($SummaryInfo::tlaLocation{$tla})) { lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY, "unknown $type TLA '$tla'"); return undef; } # note that the "$self->line(...)" argument is 1-based (not zero-base) $after = defined($after) ? $after + 1 : 1; if (exists($self->[$category]->{$owner})) { my $od = $self->[$category]->{$owner}; if (exists($od->{$tla})) { my $l = $od->{$tla}; my $idx = binarySearchLine($l, $after); return defined($idx) ? $l->[$idx] : undef; } } return undef; } sub nextBranchInOwnerBin { # ($self, $owner, $tla, $after) = @_; return nextCategoryInOwnerBin('branch', BRANCH_OWNERS, @_); } sub nextMcdcInOwnerBin { # ($self, $owner, $tla, $after) = @_; return nextCategoryInOwnerBin('MC/DC', MCDC_OWNERS, @_); } sub nextCallgraphInOwnerBin { # ($self, $owner, $tla, $after) = @_; return nextCategoryInOwnerBin('callgraph', CALLGRAPH_OWNERS, @_); } sub _computeAge { my ($when, $path) = @_; # if SOURCE_DATE_EPOCH is set, then use that as 'now': age of this # file is as of the epoch date. # if this file is newer than that ($when is in the future of epoch) - then # warn and put this file into the 'zero age' bin. my $now = exists($ENV{SOURCE_DATE_EPOCH}) ? DateTime->from_epoch(epoch => $ENV{SOURCE_DATE_EPOCH}) : DateTime->now(); my $then = lcovutil::parse_w3cdtf($when); if ($then > $now) { if (lcovutil::warn_once($lcovutil::ERROR_INCONSISTENT_DATA, $path)) { # issue annotation warning at most once per file # also attempt to clarify where the date comes from my $data = exists($ENV{SOURCE_DATE_EPOCH}) ? ( lcovutil::warn_once($lcovutil::ERROR_INCONSISTENT_DATA, 'SOURCE_DATE_EPOCH') ? "computed from your 'SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH}' environment variable - see 'man genhtml'" : "'SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH}'") : "'now'"; lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "File \"$path\": '$now' ($data) is older than annotate time '$when'" ); } return 0; } return $then->delta_days($now)->in_units('days'); } sub _load { my ($self, $fileCovInfo) = @_; my $start = Time::HiRes::gettimeofday(); ++$totalFiles; my $path = ReadCurrentSource::resolve_path($self->path()); # try to simplify path - 'realpath' will fail if the file does # not exist - but that might be fine if it exists in the repo my $repo_path = Cwd::realpath($path); if (!defined($repo_path)) { if (!defined($annotateCallback) && !$main::synthesizeMissingFile) { my $suffix = lcovutil::explain_once('synthesize', ' (see --synthesize-missing option to work around)'); lcovutil::ignorable_error($lcovutil::ERROR_SOURCE, "\"" . $self->path() . "\" does not exist: $!" . $suffix); } $repo_path = $self->path(); } # check for version mismatch... if (@lcovutil::extractVersionScript) { my $currentVersion = $fileCovInfo->version('current'); my $version = lcovutil::extractFileVersion($repo_path); if (defined($version) && '' ne $version) { if (defined($currentVersion)) { lcovutil::checkVersionMatch($repo_path, $version, $currentVersion, 'load'); } else { my $suffix = lcovutil::explain_once('missingVersionMsg', "\n\tSee the 'compute_file_version' section in man lcovrc(5)." ); lcovutil::ignorable_error($lcovutil::ERROR_VERSION, "'$repo_path': computed '$version' but version not defined in 'current' data file " . $main::info_filenames[0] . $suffix); } } elsif (defined($currentVersion)) { lcovutil::ignorable_error($lcovutil::ERROR_VERSION, "'$repo_path': version mismatch: your '--version-script' returned empty but 'current' data in " . $main::info_filenames[0] . " defines '$currentVersion'"); } my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{check_version}{$self->path()} = $end - $start; } if (defined($annotateCallback) && # also skip if we already emitted 'missing file' error (defined($lcovutil::versionCallback) || !lcovutil::fileExistenceBeforeCallbackError($repo_path)) ) { my $begin = Time::HiRes::gettimeofday(); my ($status, $lines); eval { ($status, $lines) = $annotateCallback->annotate($repo_path); }; if ($@) { my $context = MessageContext::context(); lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, 'annotate(' . $self->path() . ") failed$context: $@"); $status = 1; # set $lines so exit status error below prints something $lines = [[$@]]; } if (!$status && defined($lines)) { my $found; # check that either all lines are annotated or none are my $lineNum = 0; foreach my $line (@$lines) { my ($text, $abbrev, $full, $when, $commit) = @$line; ++$lineNum; # source line numbers start at 1 (one) my $age = _computeAge($when, $path); if ($commit ne 'NONE') { die("inconsistent 'annotate' data for '$repo_path': both 'commit' and 'no commit' lines" ) if (defined($found) && !$found); $found = 1; defined($abbrev) or die("owner is undef for $repo_path:$lineNum"); } else { die("inconsistent 'annotate' data for '$repo_path': both 'no commit' and 'commit' lines" ) if (defined($found) && $found); $found = 0; $abbrev = "no.body"; } $full = $abbrev unless defined($full); push @{$self->[LINES]}, SourceLine->new($lineNum, $text, $abbrev, $full, $when, $age, $commit); } my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{annotate}{$self->path()} = $end - $begin; ++$annotatedFiles if $found; $self->_synthesize($fileCovInfo, 1); # fake annotations too return $self; } else { # non-zero exit status: something bad happened in annotation # if we ignore the error - then fall through and just try to # load the file my $text = ''; $text = ': ' . $lines->[0]->[0] . '...' if $lines && @$lines; # might be useful to provide more than one line of context - if # there is more than one line? lcovutil::report_exit_status($lcovutil::ERROR_ANNOTATE_SCRIPT, "annotate command failed", $status, '', $text); } # end if error } # end if annotate script exists # Check if file exists and is readable my $begin = Time::HiRes::gettimeofday(); if (!-r $path) { # perhaps this error should be ignorable: if the source is not # found and the 'source' error is ignored, then synthesize # Note that we can't extract version data from synthesized code. if ($main::synthesizeMissingFile || # don't complain if we weren't going to do anything with the file - # not generating source view and not checking versions or annotating ($main::no_sourceview && 0 == scalar(@lcovutil::extractVersionScript) && !lcovutil::is_filter_enabled()) ) { $self->_synthesize($fileCovInfo, defined($SourceFile::annotateCallback)); my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{synth}{$self->path()} = $end - $begin; return $self; } die($self->path() . " is not readable or doesn't exist. See the '--substitute', '--filter missing' and '--synthesize-missing' $lcovutil::tool_name options for methods to fix the path or ignore the problem." ); } $self->_bare_load($path); $self->_synthesize($fileCovInfo, defined($SourceFile::annotateCallback)); my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{load}{$self->path()} = $end - $begin; return $self; } sub _synthesize { my ($self, $fileCovInfo, $annotate) = @_; my $lineData = $fileCovInfo->lineMap(); my $currentLast = scalar(@{$self->[LINES]}); my $last_line = 0; while (my ($l, $d) = each(%$lineData)) { $last_line = $l if ('<' ne substr($l, 0, 1) && $l > $last_line); } my %functionDecls; if ($lcovutil::func_coverage) { while (my ($fnName, $funcEntry) = each(%{$fileCovInfo->functionMap()})) { my $tla = $funcEntry->hit()->[1]; # deleted function doesn't count next if exists($SummaryInfo::deletedTla{$tla}); my $line = $funcEntry->line(); my $end = $funcEntry->end_line(); $last_line = $line if $line > $last_line; $functionDecls{$line} = "/* BEGIN: function \"$fnName\" */" if ($line >= $currentLast); if (defined($end)) { $functionDecls{$end} = "/* END: function \"$fnName\" */" if ($end >= $currentLast); if ($end > $last_line) { # function end is not an executable line, # but is after last executable line $last_line = $end; } } } } return $self if ($last_line < 1 || $currentLast >= $last_line); my $why; if (0 == $currentLast) { # Synthesize source data from coverage trace to replace unreadable file my $suffix = $lcovutil::ignore[$lcovutil::ERROR_SOURCE] ? ' - synthesizing fake content.' : '.'; lcovutil::ignorable_error($lcovutil::ERROR_SOURCE, "cannot read '" . $self->path() . "' - file is empty or unreadable$suffix" ) unless $main::no_sourceview; $why = 'empty or unreadable'; } else { my $suffix = ''; if ($lcovutil::ignore[$lcovutil::ERROR_RANGE]) { my $num = $last_line - $currentLast; my $plural = $num == 1 ? 'line' : "$num lines"; $suffix = ' ... synthesizing fake content for last ' . $plural; } lcovutil::ignorable_error( $lcovutil::ERROR_RANGE, $self->path() . ' contains only ' . $currentLast . ' lines but coverage data refers to line ' . $last_line . $suffix); $why = 'not long enough'; } # Simulate gcov behaviour my $notFound = "/* " . $self->path() . " $why */"; my $synth = "/* (content generated from coverage data) */"; my $idx = 1; my @fakeline = (undef, # line number undef); # source text if ($annotate) { my $now = exists($ENV{SOURCE_DATE_EPOCH}) ? DateTime->from_epoch(epoch => $ENV{SOURCE_DATE_EPOCH}) : DateTime->now(); push(@fakeline, 's.n.thetic', # owner 'faked synthetic user', # full name $now, # when _computeAge($now, $self->path()), # age 'synthesized'); # commit ID } for (my $line = $currentLast + 1; $line <= $last_line; $line++) { my $mod = $idx++ % 20; my $text; # if there is function decl here...mark it. if (exists($functionDecls{$line})) { $text = $functionDecls{$line}; } else { $text = (($mod == 1) ? $notFound : ($mod == 2) ? $synth : "/* ... */"); } splice(@fakeline, 0, 2, $line, $text); push(@{$self->[LINES]}, SourceLine->new(@fakeline)); } return $self; } sub _bare_load { my ($self, $path) = @_; my $lineno = 0; local *HANDLE; # File handle for reading the diff file open(HANDLE, "<", $path) or die("unable to open '" . $self->path() . "': $!\n"); while (my $line = ) { chomp $line; $line =~ s/\r//g; # Also remove CR from line-end $lineno++; push @{$self->[LINES]}, SourceLine->new($lineno, $line); } close(HANDLE) or die("unable to close $path: $!\n"); return $self; } # a class to hold either line or branch counts for each testcase - # used by the "--show-details" feature. package TestcaseTlaCount; sub new { # $testcaseCounts: either 'CountData' or 'BranchData' structure # - the data for this testcase # $fileDetails: SourceFile structure - data for the filename # $covtype: 'line' or 'branch' my ($class, $testcaseCounts, $fileDetails, $covtype) = @_; my %tlaData; if ($covtype != SummaryInfo::FUNCTION_DATA) { $tlaData{found} = $testcaseCounts->found(); $tlaData{hit} = $testcaseCounts->hit(); } else { $tlaData{found} = $testcaseCounts->numFunc($main::merge_function_aliases); $tlaData{hit} = $testcaseCounts->numHit($main::merge_function_aliases); } if ($main::show_tla && defined($fileDetails)) { for my $tla (("CBC", "GBC", "GIC", "GNC")) { $tlaData{$tla} = 0; } if (SummaryInfo::LINE_DATA == $covtype) { foreach my $line ($testcaseCounts->keylist()) { # skip uncovered lines... next if $testcaseCounts->value($line) == 0; my $lineData = $fileDetails->line($line); # "SourceLine" structure my $tla = $lineData->tla(); die("unexpected TLA '$tla' in CountData for line $line") unless exists($tlaData{$tla}); $tlaData{$tla} += 1; } } elsif (SummaryInfo::BRANCH_DATA == $covtype) { foreach my $line ($testcaseCounts->keylist()) { my $lineData = $fileDetails->line($line); # "SourceLine" structure my $branchEntry = $lineData->branchElem(); foreach my $blk ($branchEntry->blocks()) { my $block = $blk->elements(); foreach my $br (@$block) { $tlaData{$br->tla()} += 1; } } } } elsif (SummaryInfo::MCDC_DATA == $covtype) { foreach my $line ($testcaseCounts->keylist()) { my $lineData = $fileDetails->line($line); # "SourceLine" structure my $mcdcEntry = $lineData->mcdcElem(); my $mcdcGroups = $mcdcEntry->groups(); while (my ($groupSize, $group) = each(%$mcdcGroups)) { foreach my $expr (@$group) { foreach my $sense (0, 1) { # use 'current' count... my ($tla, $b_count, $c_count) = @{$expr->count($sense)}; $tlaData{$tla} += 1; } } } } } else { die("$covtype not supported yet") unless $covtype == SummaryInfo::FUNCTION_DATA; foreach my $key ($testcaseCounts->keylist()) { my $func = $testcaseCounts->findKey($key); my $line = $func->line(); # differential FunctionEntry my $lineData = $fileDetails->line($line); my $differential = $lineData->functionElem(); my @tlas; if ($main::merge_function_aliases) { push(@tlas, $differential->hit()->[1]); } else { foreach my $alias ($differential->aliasNames()) { push(@tlas, $differential->alias($alias)->tla()); } } foreach my $tla (@tlas) { $tlaData{$tla} += 1; } } } } my $self = [$testcaseCounts, $fileDetails, \%tlaData, $covtype]; bless $self, $class; return $self; } sub covtype { my $self = shift; return $self->[3]; } sub count { my ($self, $tla) = @_; my $tlaCounts = $self->[2]; return exists($tlaCounts->{$tla}) ? $tlaCounts->{$tla} : 0; } package GenHtml; use constant { TOP => 0, WORKLIST => 1, PENDING => 2, JOBS => 3, FORK_MGR => 4, TMPDIR => 5, DELAY_TIMER => 6, WEIGHTS => 7, }; sub new { my ($class, $current_data) = @_; my $self = [ SummaryInfo->new("top", ''), # top has empty name [], # worklist: dependencies are complete - this can run immediately {}, # %pending; # map of name->list of as-yet incomplete dependencies # This task can be scheduled as soon as its last dependency # is complete. [], # jobs which are to be scheduled for execution undef, # the fork manager - 'compute' owns the children, the retry # counts and the count of jobs currently running File::Temp->newdir( "genhtml_XXXX", DIR => $lcovutil::tmp_dir, CLEANUP => !defined($lcovutil::preserve_intermediates) ), 0, # delay timer {}, # file key -> its record count, for the memory throttle ]; bless $self, $class; lcovutil::info(1, "Writing temporary data to " . $self->[TMPDIR] . "\n"); my $pending = $self->[PENDING]; my $worklist = $self->[WORKLIST]; my $top_level_summary = $self->[0]; # no parent data for top-level my $toplevelPerTestData = ($main::hierarchical || $main::flat) ? [{}, {}, {}, {}] : undef; $pending->{""} = [ ['top', [$top_level_summary, $toplevelPerTestData, $top_level_summary->name() ], ['root'], ['root'], [undef, undef] ], {} ]; # if the worklist is sorted alphabetically, then each segment tends to # contain files from the same directory. # However, large files (which may take a long time to process) often # cluster inside the same directory - so a random (hash) order often # performs better. # Better is to look at profile data from the prior execution of # this job - say, from last night's regressions - and to distribute # the files we think will be long-running (...because they ran a long # the last time) into different segments so they can run in parallel. # We also want to schedule those long-running files so they are executed # first. That way, we don't end up starting some long job late in the # game - and then have to wait a long time for a single task to finish # (as opposed to it running in parallel with a lot of other stuff). # A file whose predicted runtime is large enough gets its own dedicated # segment (see $lcovutil::dedicate_segment_threshold) so it is not # batched behind up to $max_tasks_per_core other files and does not # serialize the tail of the run. Such files are also scheduled first. # The prediction is EXACT when '--history-script' data is available (we # know how long the file took last time). Without history, we only # estimate - from the instrumented-line count - for files large enough # to plausibly cross the threshold; every other unknown file stays cheap # and is handled as before. my @predicted; # [$f, $filename, $predictedSeconds] my @unknown; # no useful prediction my $weights = $self->[WEIGHTS]; foreach my $f ($current_data->files()) { my $traceInfo = $current_data->data($f); # This is the name every output path below is derived from - the # directory hierarchy, the page names, the links between them - so it # is the name in this platform's spelling, whatever spelling the input # used. See 'lcovutil::native_path'; '$f' is still the name as read, # and is what the data is keyed by my $filename = lcovutil::native_path( ReadCurrentSource::resolve_path($traceInfo->filename())); # What a child which is given this file has to hold: its records, which # the memory throttle turns into bytes with a rate it measures - see # 'lcovutil::ForkManager::_estimate'. Collected here because this is # the one place which already has the file's data in hand; the # scheduler only ever sees the summary object. $weights->{$f} = $traceInfo->found() + $traceInfo->branch_found(); my $predicted; if ( defined($lcovutil::profileHistoryCallback) && defined($predicted = $lcovutil::profileHistoryCallback->history($filename)) ) { # exact - this is how long the file took the last time it ran push(@predicted, [$f, $filename, $predicted]); } elsif ($lcovutil::dedicate_segment_threshold && $lcovutil::dedicate_segment_line_estimate && $traceInfo->found() >= $lcovutil::dedicate_segment_line_estimate) { # no history, but the file is large enough that we should guess: # estimate runtime from instrumented lines so a big straggler # still gets a dedicated segment on a first (history-less) run push(@predicted, [$f, $filename, $traceInfo->found() / $lcovutil::dedicate_segment_line_estimate ]); } else { # put the ones we don't know about at the start of the list # - work on them first, trusting that we will have parallelism # capacity for the ones we know about push(@unknown, [$f, $filename]); } } lcovutil::info(scalar(@unknown) . ' (of ' . (scalar(@predicted) + scalar(@unknown)) . ") files do not appear in \'--history\' profile.\n") if (defined($lcovutil::profileHistoryCallback) && @unknown); # sort the 'predicted' ones based on expected execution time # tie break alphabetically @predicted = sort({ $b->[2] <=> $a->[2] or $a->[1] cmp $b->[1] } @predicted); # Split off the files predicted to run at least the threshold: these get # their own dedicated segment. @fileOrder entries are # [$f, $filename, $isDedicated]. my @fileOrder; my @shared; foreach my $p (@predicted) { if ($lcovutil::dedicate_segment_threshold && $p->[2] >= $lcovutil::dedicate_segment_threshold) { # already in descending-cost order - schedule first, one per # segment (dedicated flag = 1) push(@fileOrder, [$p->[0], $p->[1], 1]); } else { push(@shared, $p); } } lcovutil::info( scalar(@fileOrder) . " file(s) assigned a dedicated segment.\n") if @fileOrder; # interleave the remaining long-running files so they get distributed # between segments my $offset = 0; my $idx = 0; for (my $i = 0; $i <= $#shared; ++$i) { push(@fileOrder, [$shared[$idx]->[0], $shared[$idx]->[1], 0]); $idx += $lcovutil::maxParallelism; if ($idx > $#shared) { ++$offset; $idx = $offset; } } push(@fileOrder, [$_->[0], $_->[1], 0]) foreach @unknown; # unknown to back foreach my $d (@fileOrder) { my ($f, $filename, $dedicated) = @$d; #my $traceInfo = $current_data->data($f); #my $filename = ReadCurrentSource::resolve_path($traceInfo->filename()); my ($vol, $parentDir, $file) = File::Spec->splitpath($filename); if (!File::Spec->file_name_is_absolute($filename)) { if ($parentDir) { $parentDir = File::Spec->catfile($main::cwd, $parentDir); } else { $parentDir = $main::cwd; } $filename = File::Spec->catfile($parentDir, $file); } my $short_name = $main::no_prefix ? $filename : main::apply_prefix($filename, @main::dir_prefix); my $is_absolute = File::Spec->file_name_is_absolute($short_name); # a host which takes both separators takes a name rooted at either my $root = $lcovutil::accepts_both_separators ? '[\\\\/]' : $lcovutil::dirseparator_re; $short_name =~ s|^$root||; my @short_path = File::Spec->splitdir($short_name); my @path = File::Spec->splitdir($parentDir); my @rel_dir_stack = @short_path; # @rel_dir_stack: relative path to parent dir of $f pop(@rel_dir_stack); # remove filename from the end my $pendingParent; if ($main::hierarchical) { # excludes trailing '/' my $base = substr($filename, 0, -(length($short_name) + 1)); my $relative_path = ""; my $path = $base; $pendingParent = $pending->{""}; # this is the top-level object dependency my @sp; my @p = split(/$lcovutil::dirseparator_re/, substr($base, 0, -1)) ; # remove trailing '/' while (scalar(@rel_dir_stack)) { my $element = shift(@rel_dir_stack); $relative_path .= $element; $path .= $lcovutil::dirseparator . $element; push(@p, $element); push(@sp, $element); if (exists($pending->{$path})) { $pendingParent = $pending->{$path}; } else { my $perTestData = [{}, {}, {}, {}]; my @dirData = (SummaryInfo->new("directory", $relative_path, $is_absolute), $perTestData, $path); add_dependency($pendingParent, $path); my @spc = @sp; my @pc = @p; $pendingParent = [ ['dir', \@dirData, \@spc, \@pc, $pendingParent ], {} ]; die("unexpected pending entry") if exists($pending->{$dirData[2]}); $pending->{$dirData[2]} = $pendingParent; } $relative_path .= $lcovutil::dirseparator; } } elsif (!$main::flat) { # not hierarchical my $relative_path = File::Spec->catdir(@rel_dir_stack); if (!exists($pending->{$parentDir})) { my $perTestData = [{}, {}, {}, {}]; my @dirData = (SummaryInfo->new( "directory", $relative_path, $is_absolute ), $perTestData, $parentDir); $pending->{$parentDir} = [ ['dir', \@dirData, \@rel_dir_stack, \@path, $pending->{$top_level_summary->name()} ], {} ]; } $pendingParent = $pending->{$parentDir}; # my directory is dependency for the top-level add_dependency($pending->{$top_level_summary->name()}, $parentDir); } # current file is a dependency of the top-level for a flat (2-level) # report - or of the parent directory otherwise my $mergeInto = $main::flat ? $pending->{$top_level_summary->name()} : $pendingParent; add_dependency($mergeInto, $f); # this file is ready to be processed # 4-element tuple: [summary, [{line},{branch},{fn}], file, mcdc_hash] # index [3] (mcdc_hash) accessed at line 13484; must be present my @fileData = (SummaryInfo->new("file", $filename), [{}, {}, {}], $f, {}); # tuple index [5] (dedicated) flags a file that should occupy its own # segment - see _segment_worklist push(@$worklist, ['file', \@fileData, \@short_path, \@path, $mergeInto, $dedicated] ); # Where this file's pages will be written, keyed by the name the report # knows the file by. The cross-file hyperlinks of the call site page # need the page location of a file other than the one being processed, # and 'SummaryInfo::relativeDir' cannot answer that: it is set per task # as the task is dispatched, so in a forked child only the files of # that child's own segment have it. This is the same # computation '_segment_worklist' does, made once here in the parent so # that every child inherits the whole map across the fork if ($lcovutil::callgraph_coverage) { my $pageDir = File::Spec->catdir(@short_path[0 .. $#short_path - 1]); $main::filePageDir{$f} = $lcovutil::case_insensitive ? lc($pageDir) : $pageDir; } } $self->compute(); # remove empty directories produced when no data in the directory # was selected. We want to clean both the directory and its parents. foreach my $dir (@cleanDirectoryList) { while ($dir) { my $d = File::Spec->catfile($main::output_directory, $dir); if (-d $d) { print("removing empty $d\n"); if (!rmdir($d)) { # directory not empty - stop last; } } $d = File::Basename::dirname($dir); # look up last if $d eq $dir; $dir = $d; } } $lcovutil::profileData{mergeDelayTimer} = $self->[DELAY_TIMER]; printf("overall delay: %0.3fs\n", $self->[DELAY_TIMER]) if $main::debugScheduler; return $self; } sub top() { my $self = shift; return $self->[0]; } sub add_dependency { my ($parent, $name) = @_; lcovutil::debug(1, "add depend '$name' in -> " . $parent->[0]->[1]->[2] . "\n") unless exists($parent->[1]->{$name}); print("add depend $name -> " . $parent->[0]->[1]->[2] . "\n") if $main::debugScheduler > 1; $parent->[1]->{$name} = 1; } sub completed_dependency { # return 0 when last dependency is removed my ($self, $parent, $name) = @_; my $pending = $self->[PENDING]; die("missing pending '$parent'") unless exists($pending->{$parent}); my $pendingParent = $pending->{$parent}; die("missing pending entry for $name (in $parent") unless exists($pendingParent->[1]->{$name}); delete($pendingParent->[1]->{$name}); print("completed $name -> $parent " . scalar(%{$pendingParent->[1]}) . "\n") if $main::debugScheduler > 1; # LCOV_EXCL_START if ($main::debugScheduler > 1) { # print parent dependencies while (my ($k, $d) = each(%{$pendingParent->[1]})) { die("unexpected parent dependency $k ARRAY: " . $d->[0] . ' ' . join('/', @{$d->[2]})) if ('ARRAY' eq ref($d) || 1 != $d); print(" $k\n"); } } # LCOV_EXCL_STOP if (!%{$pendingParent->[1]}) { # no more dependencies - schedule this one my $p = $pendingParent->[0]; print("completed dependencies - schedule " . $p->[0] . ' ' . join('/', @{$p->[2]}) . "\n") if $main::debugScheduler > 1; push(@{$self->[WORKLIST]}, $p); delete($pending->{$parent}); } } sub merge_one { my ($self, $parentPerTestData, $fullname, $perTest, $summary, $parentSummary, $parentPath, $parallel) = @_; my $type = $summary->type(); if ('file' eq $type) { # the key the file table looks this up under - so the same last element # 'process_file' wrote the pages under. '$fullname' is the name as # read (it is what the dependency bookkeeping below is keyed by), so # it has to be spelled for this platform first my $base_name = File::Basename::basename(lcovutil::native_path($fullname)); for (my $i = 0; $i < scalar(@$perTest); $i++) { $parentPerTestData->[$i]->{$base_name} = $perTest->[$i]; } } elsif ('directory' eq $type) { for (my $i = 0; $i < scalar(@$perTest); $i++) { while (my ($basename, $data) = each(%{$perTest->[$i]})) { $parentPerTestData->[$i]->{$basename} = $data; } } } else { die("unexpected type $type") unless $type eq 'top'; # set the top-level to this (restored) value... $self->[0] = $summary if $parallel; } if ($type ne 'top') { my $mergeInto = $main::flat ? $self->top() : $parentSummary; $mergeInto->append($summary); my $parentName = $main::flat ? '' : $parentPath; $self->completed_dependency($parentName, $fullname); } } sub compute_one { my ($type, $name, $summary, $parentSummary, $perTestData, $rel_dir, $base_dir, $trunc_dir) = @_; my $start = Time::HiRes::gettimeofday(); if ('file' eq $type) { my ($testdata, $testfncdata, $testbrdata, $testcase_mcdc) = main::process_file($summary, $parentSummary, $trunc_dir, $rel_dir, $name); $perTestData = [$testdata, $testfncdata, $testbrdata, $testcase_mcdc]; } elsif ('dir' eq $type) { # process the directory... main::write_summary_pages($name, 1, # this is a directory, $summary, $main::show_details, $rel_dir, $base_dir, $trunc_dir, $perTestData); } else { die("unexpected task") unless 'top' eq $type; # Create sorted pages main::write_summary_pages($name, 0, # 0 == list directories $summary, # generate top-level 'details' in flat or hierarchical modes $main::show_details && ($main::flat || $main::hierarchical), ".", "", undef, $perTestData); } $summary->checkCoverageCriteria(); if ('file' eq $type && 1 < $lcovutil::maxParallelism && !($main::buildSerializableDatabase || $main::show_details)) { if ($main::no_sourceview || $main::frames || !$main::show_tla) { $summary->[SummaryInfo::FILE_DETAILS] = undef; } elsif ($main::show_tla) { # just store the location of the first coverpoint in each display # group - rather than returning the whole detail structure? $summary->[SummaryInfo::FILE_DETAILS]->simplify(); } } my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{file}{$name} = $end - $start; return $perTestData; } sub _running { # how many of our jobs are in flight. The manager owns the children, and # it exists for the whole of 'compute' - which is the only place that # forks - so anywhere else the answer is none. my $mgr = $_[0]->[FORK_MGR]; return $mgr ? $mgr->count() : 0; } sub _reschedule { my ($self, $jobID, $jobs) = @_; # clean up foreach my $task (@$jobs) { my $selfSummary = $task->[1]; $selfSummary->unsetDirs(); # reset state } push(@{$self->[JOBS]}, [$jobs, $jobID]); # put job back print(" reschedule $jobID current:", $self->_running(), " jobs: ", scalar(@{$self->[JOBS]}), "\n") if $main::debugScheduler; } sub _process_child { # I'm the child: compute my jobs and hand the results back. The manager # owns the capture, the log files and the dump - see 'ForkManager'. my ($self, $jobs, $jobId) = @_; my $childStart = Time::HiRes::gettimeofday(); # clear counters so we store count only what we saw in the child $SourceFile::annotatedFiles = 0; $SourceFile::totalFiles = 0; my $status = 0; my @rtnData; # a scope, not a loop: the manager's capture used to be here, and the work # below is what it wrapped { foreach my $t (@$jobs) { my ($task, $sSummary, $rel_dir, $full_dir_path, $base_dir, $trunc_dir) = @$t; my @taskData; push(@rtnData, \@taskData); my ($type, $data, $short_path, $path, $parentData) = @$task; my ($selfSummary, $perTestData, $name) = @$data; die("inconsistent") unless $selfSummary == $sSummary; my ($parentSummary, $parentPerTestData, $parentPath); ($parentSummary, $parentPerTestData, $parentPath) = @{$parentData->[0]->[1]} unless 'top' eq $type; eval { my $thisTestData = compute_one($type, $name, $selfSummary, $parentSummary, $perTestData, $rel_dir, $base_dir, $trunc_dir); # clear the parent pointer that we hacked into place. Don't want that # extra data returned by dumper. $selfSummary->[SummaryInfo::PARENT] = undef; $selfSummary->[SummaryInfo::SOURCES] = {} if $selfSummary->type() ne 'file'; my $objname = $selfSummary->type() eq 'top' ? "" : $selfSummary->name(); my $criteria; $criteria = $CoverageCriteria::coverageCriteria{$objname} if exists($CoverageCriteria::coverageCriteria{$objname}); push(@taskData, $thisTestData, $parentPerTestData, $selfSummary, $criteria); }; # eval if ($@) { $status = 1; print(STDERR $@); # @todo maybe this should be an ignorable error next; } } # foreach } my $childEnd = Time::HiRes::gettimeofday(); $lcovutil::profileData{child}{$jobId} = $childEnd - $childStart; return ([\@rtnData, [$SourceFile::annotatedFiles, $SourceFile::totalFiles], [$childStart, $childEnd] ], $status); } sub _segment_worklist { my ($self, $segmentID) = @_; my $worklist = $self->[WORKLIST]; my $nTasks = scalar(@$worklist); # how many cores are available? my $nCores = $lcovutil::maxParallelism - $self->_running(); # let's distribute them evenly # could be more sophisticated and look at the compute time used # by each of those tasks previously - then balance the expected load my $tasksPerCore = $lcovutil::maxParallelism > 1 ? ($nTasks + $nCores - 1) / $nCores : 1; $tasksPerCore = $lcovutil::max_tasks_per_core if ($tasksPerCore > $lcovutil::max_tasks_per_core); while (@$worklist && scalar(@{$self->[JOBS]}) <= $nCores) { # How many tasks go in this segment? Normally $tasksPerCore, but a # file flagged 'dedicated' (worklist tuple index [5]) gets a segment # to itself: if the next task is dedicated, take just it; otherwise # take a normal batch but stop before the first dedicated task so it # starts a fresh segment. my $batch = $tasksPerCore; if ($worklist->[0]->[5]) { $batch = 1; } else { for (my $i = 1; $i < $batch && $i < scalar(@$worklist); ++$i) { if ($worklist->[$i]->[5]) { $batch = $i; last; } } } my @jobs; push(@{$self->[JOBS]}, [\@jobs, $segmentID++]); foreach my $task (splice(@$worklist, 0, $batch)) { my ($type, $data, $short_path, $path, $parentData) = @$task; my ($selfSummary, $perTestData, $name) = @$data; #main::info("$type: $name\n"); # break initialization into 2 statements - see # https://stackoverflow.com/questions/26676488/why-is-the-variable-not-available my ($parentSummary, $parentPerTestData, $parentPath); ($parentSummary, $parentPerTestData, $parentPath) = @{$parentData->[0]->[1]} unless 'top' eq $type; my $rel_dir; my $full_dir_path; if ($type eq 'file') { $rel_dir = File::Spec->catdir(@{$short_path}[0 .. $#$short_path - 1]); die("file error for " . File::Spec->catdir(@$short_path)) if '' eq $rel_dir; $full_dir_path = File::Spec->catdir(@{$path}[0 .. $#$path - 1]); } elsif ($type eq 'dir') { $rel_dir = File::Spec->catdir(@$short_path); $full_dir_path = File::Spec->catdir(@$path); } else { $rel_dir = '.'; $full_dir_path = '.'; } $rel_dir = lc($rel_dir) if ($lcovutil::case_insensitive); my $p = File::Spec->catdir($main::output_directory, $rel_dir); File::Path::make_path($p) unless -d $p || $main::no_html; my $base_dir = main::get_relative_base_path($rel_dir); my $trunc_dir = ($rel_dir eq '') ? 'root' : $rel_dir; push(@jobs, [$task, $selfSummary, $rel_dir, $full_dir_path, $base_dir, $trunc_dir ]); } # foreach task assigned to this core } return $segmentID; } sub compute { my $self = shift; my $worklist = $self->[WORKLIST]; my $pending = $self->[PENDING]; my $joblist = $self->[JOBS]; my $segmentID = 0; # This scheduler cannot use 'ForkManager::run': its queue is dependency # ordered (a job becomes runnable when the last job it depends on is # merged), so it dispatches from its own job list and drives 'throttle', # 'fork_one' and 'reap_one' itself. Everything below the dispatch - the # child wrapper, the reap, the retries and the profile keys - is in the # common framework. my $mgr = lcovutil::ForkManager->new( operation => 'genhtml', phase => 'segment', tempDir => '' . $self->[TMPDIR], prefix => 'genhtml', # non-parallel execution prints stuff to stdout - so we should too showStdout => ($lcovutil::verbose >= 0), # the segment children are the big ones: a job is a set of files whose # source, annotations and per-testcase detail all have to be live at # once memoryThrottle => 1, # ..and how much of that there is scales with the records of the files in # the job. A directory or top-level job aggregates what its children # already computed and weighs nothing here, which leaves the estimate # at "a copy of us" for it - see 'ForkManager::_estimate'. unitWeight => sub { my $jobs = shift; my $weight = 0; foreach my $task (@$jobs) { next unless 'file' eq $task->[0]->[0]; $weight += $self->[WEIGHTS]->{$task->[0]->[1]->[2]} // 0; } return $weight; }, remaining => sub { # not covered: the memory throttle's wait message is the # only caller - see the note on 'remaining' and # 'childFailMessage' in lcovutil::ForkManager return scalar(@$worklist) + scalar(keys(%$pending)); }, # 'next' is the framework's dispatch loop, which this # scheduler does not use: see above. # The job list is built by '_segment_worklist' and # dispatched below. next => undef, child => sub { my ($jobs, $jobId) = @_; return $self->_process_child($jobs, $jobId); }, postFork => sub { my ($jobs, $jobId) = @_; print("forked $jobId current: ", $self->_running(), " jobs:", scalar(@$joblist), "\n") if $main::debugScheduler; }, postStore => sub { my ($jobs, $jobId, $dumpfile, $dumpStart) = @_; printf(" %d: dump %d %d jobs %0.3fs %0.3fMb %s\n", $jobId, $$, scalar(@$jobs), Time::HiRes::gettimeofday() - $dumpStart, (-f $dumpfile ? (-s $dumpfile) : 0) / (1024 * 1024), DateTime->now()) if ($main::debugScheduler); }, merge => sub { my ($jobs, $jobId, $payload, $ctx) = @_; $self->merge_child($jobs, $jobId, $payload, $ctx); }, postReap => sub { my $ctx = shift; my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{segment}{$ctx->{id}} = $end - $ctx->{forkAt}; $lcovutil::profileData{merge_segment}{$ctx->{id}} = $end - $ctx->{reapAt}; }, requeue => sub { my ($jobs, $jobId) = @_; $self->_reschedule($jobId, $jobs); }, forkFailWhen => sub { return "process segment $_[0]"; }, retryWhen => sub { my $ctx = shift; return 'compute job ' . $ctx->{id} . " (child $ctx->{child})"; }, mergeFailMessage => sub { my $ctx = shift; return "unable to deserialize $ctx->{dumpfile}: $ctx->{error}"; }, childFailMessage => sub { # also not covered: a segment child which returns # non-zero and still leaves a readable dump - see note # in lcovutil::ForkManager my $ctx = shift; my $msg = 'compute job ' . $ctx->{id}; $msg .= ": unable to deserialize $ctx->{dumpfile}" unless defined($ctx->{payload}); return $msg; },); $self->[FORK_MGR] = $mgr; WORK: while ($mgr->count() || @$joblist || @$worklist || %$pending) { if (1 < $lcovutil::maxParallelism) { # wait here for child: # - while we are oversubscribed # - too many parallel jobs or # - consuming too much memory), OR # - if the worklist is empty and there are no jobs ready to # go (these are jobs which failed and were reschedule - e.g., # due to out-of-memory). # There are two possibilities: # - there are pending jobs: # they will be put on the worklist when their dependency # finishes (at least one of those dependencies must be # running now - we could go check that...) # - there are no pending jobs: so there must be just # one remaining top-level job currently running - # when it finishes, we are done. # Also do a nonblocking wait here to collect any children that # have finished - so we have a better chance to parallelize # more/have more tasks that we can dispatch print(" currentParallel:", $mgr->count(), " pending:", scalar(%$pending), "\n") if ($main::debugScheduler > 1 || ($main::debugScheduler && 0 == scalar(@$worklist) && 0 == scalar(@$joblist))); my $reaped = 0; if (0 == scalar(@$worklist) && 0 == scalar(@$joblist)) { # nothing we could dispatch even if there was room: wait for # whatever is running to finish and unblock its dependents $reaped += $mgr->reap_one(1); } else { # about the job we would dispatch next, when there is one: the # worklist may not have been cut into jobs yet, and then there # is nothing to weigh $reaped += $mgr->throttle(@$joblist ? @{$joblist->[-1]} : ()); } # and collect anything else which has already finished, so we have # more tasks to dispatch below while ($mgr->reap_one(0)) { ++$reaped; } print("reaped $reaped current:", $mgr->count(), " jobs:", scalar(@$joblist), " work:", scalar(@$worklist), " pending:", scalar(%$pending), "\n") if ($main::debugScheduler && $reaped); next WORK if $reaped; } # LCOV_EXCL_START unless (@$worklist || @$joblist) { foreach my $e (keys %$pending) { # something went wrong - print some debug data lcovutil::info("me: $e: depends\n"); my $p = $pending->{$e}; while (my ($k, $d) = each(%{$p->[1]})) { lcovutil::info(" $k\n"); die("unexpected data for depend $k: $d") unless $d == 1; } lcovutil::info(" parent: " . $p->[0]->[0] . ' ' . join('/', @{$p->[0]->[2]}) . "\n"); } # can get here if child process hit a 'die' (as opposed to an error) # and "--keep-going" was specified. die("unexpected empty worklist??"); } # LCOV_EXCL_STOP # schedule a few items from the worklist if we can.. $segmentID = $self->_segment_worklist($segmentID); while (@$joblist) { if (1 < $lcovutil::maxParallelism) { # need to wait here if insufficient resources # (only wait until resource available - pending and # worklist are managed above) # ..and the job we are about to take is the one to ask about, so # that the memory estimate is for the work it really holds $mgr->throttle(@{$joblist->[-1]}); } my $d = pop(@$joblist); my ($jobs, $jobId) = @$d; my $start = Time::HiRes::gettimeofday(); foreach my $task (@$jobs) { # keep track of where this data is stored - need it later for # top-level 'flat' view my $selfSummary = $task->[1]; my $relDir = $task->[2]; my $fullDir = $task->[3]; $selfSummary->relativeDir($relDir); $selfSummary->fullDir($fullDir); } # distribute this job to parallel execution unless there is # nothing else running. In that case, we won't continue # until this job is finished - so might as well do it # locally and not incur parallelism overhead. if (1 < $lcovutil::maxParallelism && ($mgr->count() > 0 || 0 != scalar(@$joblist) || scalar(@$jobs) > 1) ) { $lcovutil::profileData{nJobs}{$jobId} = scalar(@$jobs); # a failed fork has already requeued this job - and slept, so # that the next attempt has a chance of succeeding next unless $mgr->fork_one($jobs, $jobId); } else { #not parallel lcovutil::info(1, "serial execution of task $jobId\n") if ($lcovutil::maxParallelism > 1); die("unexpected distribution") unless 1 == scalar(@$jobs); my ($task, $sSummary, $rel_dir, $full_dir_path, $base_dir, $trunc_dir) = @{$jobs->[0]}; my ($type, $data, $short_path, $path, $parentData) = @$task; my ($selfSummary, $perTestData, $name) = @$data; die("inconsistent") unless $selfSummary == $sSummary; my ($parentSummary, $parentPerTestData, $parentPath); ($parentSummary, $parentPerTestData, $parentPath) = @{$parentData->[0]->[1]} unless 'top' eq $type; my $thisTestData = compute_one($type, $name, $selfSummary, $parentSummary, $perTestData, $rel_dir, $base_dir, $trunc_dir); $self->merge_one($parentPerTestData, $name, $thisTestData, $selfSummary, $parentSummary, $parentPath); my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{$type}{$name} = $end - $start; } } # while (job list not empty) } # while (work remains) # the manager did the waiting, so it is the one which knows how long for $self->[DELAY_TIMER] = $mgr->delay_timer(); # The manager's callbacks close over '$self', and '$self' holds the manager - # so this reference cycle keeps the whole task graph, including the # 'File::Temp' child data directory, alive until global destruction. We # are done with the children, so break it here: that releases the temp # directory now, while 'temp_cleanup' can still take its parent away. $self->[FORK_MGR] = undef; $self->[TMPDIR] = undef; } sub merge_child { # One child's results, in the parent. The manager has already reaped it, # shown what it had to say and restored its payload; what is left is # ours. Anything which throws here is reported once, by the manager, as # 'unable to deserialize '. my ($self, $jobs, $jobId, $payload, $ctx) = @_; printf(" %d restore %d: %0.3fs %s (parallel %d)\n", $jobId, $ctx->{child}, Time::HiRes::gettimeofday() - $ctx->{reapAt}, DateTime->now(), $self->_running()) if $main::debugScheduler; my ($jobData, $countData, $otherData) = @$payload; die("error processing child") if (scalar(@$jobData) != scalar(@$jobs)); $SourceFile::annotatedFiles += $countData->[0]; $SourceFile::totalFiles += $countData->[1]; $lcovutil::profileData{startDelay}{$jobId} = $otherData->[0] - $ctx->{forkAt}; $lcovutil::profileData{mergeDelay}{$jobId} = $ctx->{reapAt} - $otherData->[1]; foreach my $t (@$jobs) { my $task = $t->[0]; my ($type, $data, $short_path, $fullname, $parentData) = @$task; my ($childSummary, $perTestData, $name) = @$data; my ($parentSummary, $parentPerTestData, $parentPath); ($parentSummary, $parentPerTestData, $parentPath) = @{$parentData->[0]->[1]} unless 'top' eq $type; my $taskData = shift(@$jobData); # there was an error while processing the child die("error processing child " . scalar(@$taskData)) if (scalar(@$taskData) != 4); my ($thisTestData, $pPerTestData, $summary, $criteria) = @$taskData; $childSummary->copyGuts($summary); $self->merge_one($parentPerTestData, $name, $thisTestData, $childSummary, $parentSummary, $parentPath, 1); # and save the deserialized criteria data if (defined($criteria)) { my $name = $childSummary->type() eq 'top' ? "" : $childSummary->name(); $CoverageCriteria::coverageCriteria{$name} = $criteria; $criteria->[1] = 0 if $criteria->[1] eq ''; my $v = $criteria->[1]; die('unexpected criteria data \'' . join(' ', @$criteria) . '\'') unless (Scalar::Util::looks_like_number($v)); # criteria failure is sticky. # The criteria of the merged jobs is independent. # We want the parent to fail if any child fails (so don't # change the parent flag unless it is to mark a fail. $CoverageCriteria::coverageCriteriaStatus = $v if $v != 0; } } # foreach job } # Where the callgraph numbers of the header of a call site page lead. There # the numbers count exactly the rows of the table below them - on the callees # page the call sites this file's functions contain, as everywhere else in the # report, and on the callers page the calls into this file's functions (see # 'withCallgraphDestinations') - so the header leads into that table rather # than into the source view: each number to the first row it counts. This is # the table's own index of those rows, built as the page is, and it answers # the three questions the header asks of a SourceFile - see # SourceFile::nextCallgraphTlaGroup, nextCallgraphInDateBin and # nextCallgraphInOwnerBin - for the rows of the page instead of for the lines # of the file. # The date and owner bins are those of the line each number is binned by: on # the callees page the line a call is on, whichever function it is in, and on # the callers page the line the function it calls starts on - which is the # date and owner the Age and Owner columns of that page show. # A number may have no row to lead to: a site written in an '#include'd file # is counted in the header of that file and listed on the page of the file # which includes it. The header then writes the number without a link - it is # not an error here, as it would be in the source view package CallgraphPageIndex; sub new { my $class = shift; return bless({tla => {}, date => {}, owner => {}}, $class); } sub add { # one row of the page, in page order: its category, the annotation of the # line it is binned by (or undef), and the anchor which leads to it my ($self, $tla, $annot, $anchor) = @_; $self->{tla}->{$tla} = $anchor unless exists($self->{tla}->{$tla}); return unless defined($annot); my $date = $annot->{bin} . "\0" . $tla; my $owner = $annot->{owner} . "\0" . $tla; $self->{date}->{$date} = $anchor unless exists($self->{date}->{$date}); $self->{owner}->{$owner} = $anchor unless exists($self->{owner}->{$owner}); } sub locationHref { my ($self, $anchor) = @_; return "#$anchor"; } sub firstTlaGroup { my ($self, $tla) = @_; return $self->{tla}->{$tla}; } sub firstInDateBin { my ($self, $bin, $tla) = @_; return $self->{date}->{$bin . "\0" . $tla}; } sub firstInOwnerBin { my ($self, $owner, $tla) = @_; return $self->{owner}->{$owner . "\0" . $tla}; } package main; # Global variables & initialization lcovutil::save_cmd_line(\@ARGV, $lcovutil::tool_dir); # TraceFile Instance containing all data from the 'current' .info file # - constructed at start of GenHtml our $current_data; # TraceFile Instance containing all data from the baseline .info file (if any) # - constructed in GenHtml (if needed) our $base_data; # does the baseline data have a statement span anywhere? See # 'changedStatementLines' our $baseline_has_spans = 0; # Instance containing all data from diff file our $diff_data = DiffMap->new(); our @opt_dir_prefix; # Array of prefixes to remove from all sub directories our @dir_prefix; # source file name -> the report directory its pages are written to, relative to # the output directory. Populated in the parent while the worklist is built # and read only by the cross-file call-site links; empty unless '--callgraph' our %filePageDir; our %test_description; # Hash containing test descriptions if available our $current_date = get_date_string(undef); our @info_filenames; # List of .info files to use as data source our $header_title; # Title at top of HTML report page (above table) our $footer; # String at bottom of HTML report page our $test_title; # Title for output as written to each page header our $output_directory; # Name of directory in which to store output our @base_filenames; # Optional: names of files containing baseline data our $age_basefile; # how old is the baseline data file? our $baseline_title; # Optional name of baseline - written to page headers our $baseline_date; # Optional date that baseline was created our $diff_filename; # Optional name of file containing baseline data our $strip; # If set, strip leading directories when applying diff our $desc_filename; # Name of file containing test descriptions our $css_filename; # Optional name of external stylesheet file to use our $show_details; # If set, generate detailed directory view our $no_prefix; # If set, do not remove filename prefix our $show_tla; # categorize coverage data (or not) our $show_functionProportions = 0 ; # show proportion of lines/branches contained in the function which are hit our $show_hitTotalCol; # include the 'hit' or 'missed' total count in tables # - this is part of the 'legacy' view # - also enabled when full differential categories are used # (i.e., but not when there is no baseline data - so no # categories apart from 'GNC' and 'UNC' our $use_legacyLabels; our $show_dateBins; # show last modified and last author info our $show_ownerBins; # show list of people who have edited code # (in this file/this directory/etc) our $show_nonCodeOwners; # show last modified and last author info for # non-code lines (e.g., comments) our $show_zeroTlaColumns; # unless set, improve readability by removing # summary table columns which contain # only zero (blank) entries our $show_simplifiedColors; our $treatNewFileAsBaseline; our $elide_path_mismatch = 0; # handle case that file in 'baseline' and 'current' .info # data matches some name in the 'diff' file such that # the basename is the same but the pathname is different # - then pretend that the names DID match our $synthesizeMissingFile = 0; # create file content if not found for annotation our $hierarchical = 0; # if true: show directory hierarchy # default: legacy two-level report our $flat = 0; # if true: single table of all files in top level our $sort_tables = 1; # If set, provide directory listings with sorted entries our $no_sort; # Disable sort our $frames; # If set, use frames for source code view our $keep_descriptions; # If set, do not remove unused test case descriptions our $suppress_function_aliases; # if set, don't show list of collapsed # function aliases # if set, the call site pages show a function whose aliases are alias groups # by its summary block alone - see _showAliasGroups our $suppress_alias_groups = 0; # if set, the call site pages show a 'summary block' for functions which # contain multiple alias groups (and only the alias group for functions # which have just one). # The callgraph coverpoint counts follow this setting: count only unique # coverpoints found across alias groups if enabled, count coverpoints found # in all the alias groups if disabled. our $show_callgraph_summary = 1; # the order of the metric column groups of the coverage tables - see # 'genhtml_metric_column_order' in man lcovrc(5). A table sorted by metric # shows that metric's columns first - followed by the rest, in this order. our $metric_column_order = 'line,mcdc,branch,function,callgraph'; our @metricColumnOrder; # the coverage types, in that order # ..and the names of the metrics in that list our %metricColumnName = ('line' => SummaryInfo::LINE_DATA, 'mcdc' => SummaryInfo::MCDC_DATA, 'branch' => SummaryInfo::BRANCH_DATA, 'function' => SummaryInfo::FUNCTION_DATA, 'callgraph' => SummaryInfo::CALLGRAPH_DATA,); our @defaultMetricColumnOrder = map({ $metricColumnName{$_} } qw(line mcdc branch function callgraph)); our $merge_function_aliases; our $no_sourceview; # If set, do not create a source code view for each file our $no_html; # don't generate HTML if set our $legend; # If set, include legend in output our $tab_size = $lcovutil::default_tab_size; # Number of spaces per tab our $html_prolog_file; # Custom HTML prolog file (up to and including ) our $html_epilog_file; # Custom HTML epilog file (from onwards) our $html_prolog; # Actual HTML prolog our $html_epilog; # Actual HTML epilog our $html_ext = $lcovutil::default_html_extension; # Extension for HTML files our $html_gzip = 0; # Compress with gzip our $opt_missed; # List/sort lines by missed counts our $dark_mode; # Use dark mode palette or normal our $charset = "UTF-8"; # Default charset for HTML pages our @fileview_sortlist; our @fileview_sortname = ("", "-sort-l", "-sort-f", "-sort-b", '-sort-m', '-sort-c'); our @fileview_prefixes = (""); our @funcview_sortlist; # ..and the orders a call site page is written in: by name, which is how a # reader looks a function up, and by how much of that function's own side of # the callgraph was never exercised, which is what the reader who is not # looking for a particular function came for our @callgraphview_sortlist; our @rate_name = ("Lo", "Med", "Hi"); our @rate_png = ("ruby.png", "amber.png", "emerald.png"); our $rc_desc_html = 0; # lcovrc: genhtml_desc_html # simplify/shorten names in 'function detail table' our @simplifyFunctionScript; # the arg list our $simplifyFunctionCallback; # the actual callback our $cwd = cwd(); # Current working directory # the call site numbering the two sides of the callgraph page share, and how many # sites the entries in it cover - see '_callSitePositions', which is where both # are used. Declared here because the code entry point is below, and an # initializer beside the function would not have run by the time it is called our %callSitePositions; our $callSitePositionSites = 0; # for debugging our $verboseScopeRegexp; # dump categorization processing if match # # Code entry point # $SIG{__WARN__} = \&lcovutil::warn_handler; $SIG{__DIE__} = \&lcovutil::die_handler; STDERR->autoflush; STDOUT->autoflush; my @datebins; my (@rc_date_bins, @rc_annotate_script, @rc_select_script, @rc_date_labels, @rc_simplifyFunctionScript); my %genhtml_rc_opts = ( "genhtml_css_file" => \$css_filename, "genhtml_header" => \$header_title, "genhtml_footer" => \$footer, "genhtml_line_field_width" => \$line_field_width, "genhtml_overview_width" => \$overview_width, "genhtml_nav_resolution" => \$nav_resolution, "genhtml_nav_offset" => \$nav_offset, "genhtml_keep_descriptions" => \$keep_descriptions, "genhtml_no_prefix" => \$no_prefix, "genhtml_no_source" => \$no_sourceview, "genhtml_num_spaces" => \$tab_size, "genhtml_frames" => \$frames, "genhtml_legend" => \$legend, "genhtml_html_prolog" => \$html_prolog_file, "genhtml_html_epilog" => \$html_epilog_file, "genhtml_html_extension" => \$html_ext, "genhtml_html_gzip" => \$html_gzip, "genhtml_precision" => \$lcovutil::default_precision, "genhtml_function_coverage" => \$lcovutil::func_coverage, "genhtml_branch_coverage" => \$lcovutil::br_coverage, "genhtml_hi_limit" => \$hi_limit, "genhtml_med_limit" => \$med_limit, "genhtml_line_hi_limit" => \$ln_hi_limit, "genhtml_line_med_limit" => \$ln_med_limit, "genhtml_function_hi_limit" => \$fn_hi_limit, "genhtml_function_med_limit" => \$fn_med_limit, "genhtml_branch_hi_limit" => \$br_hi_limit, "genhtml_branch_med_limit" => \$br_med_limit, "genhtml_mcdc_hi_limit" => \$mcdc_hi_limit, "genhtml_mcdc_med_limit" => \$mcdc_med_limit, "genhtml_callgraph_hi_limit" => \$callgraph_hi_limit, "genhtml_callgraph_med_limit" => \$callgraph_med_limit, "genhtml_branch_field_width" => \$br_field_width, "genhtml_mcdc_field_width" => \$mcdc_field_width, "genhtml_owner_field_width" => \$owner_field_width, "genhtml_age_field_width" => \$age_field_width, "genhtml_sort" => \$sort_tables, "genhtml_charset" => \$charset, "genhtml_desc_html" => \$rc_desc_html, 'merge_function_aliases' => \$merge_function_aliases, 'suppress_function_aliases' => \$suppress_function_aliases, 'suppress_alias_groups' => \$suppress_alias_groups, 'show_callgraph_summary' => \$show_callgraph_summary, 'genhtml_metric_column_order' => \$metric_column_order, "genhtml_missed" => \$opt_missed, "genhtml_dark_mode" => \$dark_mode, "genhtml_hierarchical" => \$hierarchical, "genhtml_flat_view" => \$flat, "genhtml_show_navigation" => \$show_tla, "genhtml_show_noncode_owners" => \$show_nonCodeOwners, "genhtml_show_function_proportion" => \$show_functionProportions, 'genhtml_show_owner_table' => \$show_ownerBins, 'genhtml_annotate_script' => \@rc_annotate_script, 'genhtml_annotate_tooltip' => \$SourceFile::annotateTooltip, "select_script" => \@rc_select_script, "simplify_function" => \@rc_simplifyFunctionScript, 'num_context_lines' => \$InInterestingRegion::num_context_lines, 'genhtml_date_bins' => \@rc_date_bins, 'genhtml_date_labels' => \@rc_date_labels, 'truncate_owner_table' => \@truncateOwnerTableLevels, 'owner_table_entries' => \$ownerTableElements, 'compact_summary_tables' => \$compactSummaryTables, 'genhtml_synthesize_missing' => \$synthesizeMissingFile, 'genhtml_alias_list_length' => \$alias_list_length, 'scope_regexp' => \$verboseScopeRegexp,); my $save; my $serialize; my $validateHTML = exists($ENV{LCOV_VALIDATE}); my %genhtml_options = ( "output-directory|o=s" => \$output_directory, "header-title=s" => \$header_title, "footer=s" => \$footer, "title|t=s" => \$test_title, "description-file|d=s" => \$desc_filename, "keep-descriptions|k" => \$keep_descriptions, "css-file|c=s" => \$css_filename, "baseline-file|b=s" => \@base_filenames, "baseline-title=s" => \$baseline_title, "baseline-date=s" => \$baseline_date, "current-date=s" => \$current_date, "diff-file=s" => \$diff_filename, "annotate-script=s" => \@SourceFile::annotateScript, "select-script=s" => \@SummaryInfo::selectCallbackScript, "simplify-script=s" => \@simplifyFunctionScript, "new-file-as-baseline" => \$treatNewFileAsBaseline, 'elide-path-mismatch' => \$elide_path_mismatch, 'synthesize-missing' => \$synthesizeMissingFile, # if 'show-owners' is set: generate the owner table # if it is passed a value: show all the owners, # regardless of whether they have uncovered code or not 'show-owners:s' => \$show_ownerBins, 'show-noncode' => \$show_nonCodeOwners, 'show-zero-columns' => \$show_zeroTlaColumns, 'simplified-colors' => \$show_simplifiedColors, "date-bins=s" => \@datebins, 'date-labels=s' => \@SummaryInfo::ageGroupHeader, "prefix|p=s" => \@opt_dir_prefix, "num-spaces=i" => \$tab_size, "no-prefix" => \$no_prefix, "no-sourceview" => \$no_sourceview, 'no-html' => \$no_html, "show-details|s" => \$show_details, "frames|f" => \$frames, "legend" => \$legend, 'save' => \$save, 'serialize=s' => \$serialize, 'scheduler+' => \$debugScheduler, "html-prolog=s" => \$html_prolog_file, "html-epilog=s" => \$html_epilog_file, "html-extension=s" => \$html_ext, "html-gzip" => \$html_gzip, "hierarchical" => \$hierarchical, "flat" => \$flat, "sort-tables" => \$sort_tables, "no-sort" => \$no_sort, "precision=i" => \$lcovutil::default_precision, "missed" => \$opt_missed, "dark-mode" => \$dark_mode, "show-navigation" => \$show_tla, "show-proportion" => \$show_functionProportions, "merge-aliases" => \$merge_function_aliases, "suppress-aliases" => \$suppress_function_aliases, 'validate' => \$validateHTML,); Getopt::Long::Configure('default'); # Parse command line options if (!lcovutil::parseOptions(\%genhtml_rc_opts, \%genhtml_options, \$output_directory) ) { print(STDERR "Use $tool_name --help to get usage information\n"); exit(1); } # Translate windows paths, if necessary lcovutil::posix_paths(\$desc_filename, \$css_filename, \$diff_filename, \$serialize, \$html_prolog_file, \$html_epilog_file); $merge_function_aliases = 1 if ($suppress_function_aliases || defined($lcovutil::cov_filter[$lcovutil::FILTER_FUNCTION_ALIAS])); $buildSerializableDatabase = 1 if $serialize; $no_sourceview = 1 if $no_html; # Copy related values if not specified $ln_hi_limit = $hi_limit if (!defined($ln_hi_limit)); $ln_med_limit = $med_limit if (!defined($ln_med_limit)); $fn_hi_limit = $hi_limit if (!defined($fn_hi_limit)); $fn_med_limit = $med_limit if (!defined($fn_med_limit)); $br_hi_limit = $hi_limit if (!defined($br_hi_limit)); $br_med_limit = $med_limit if (!defined($br_med_limit)); $mcdc_hi_limit = $hi_limit if (!defined($mcdc_hi_limit)); $mcdc_med_limit = $med_limit if (!defined($mcdc_med_limit)); $callgraph_hi_limit = $hi_limit if (!defined($callgraph_hi_limit)); $callgraph_med_limit = $med_limit if (!defined($callgraph_med_limit)); $frames = undef unless (defined($frames) && $frames); # ..and the same for the other flag which lcovrc can set to zero: most of the # places which consult it ask whether it is true, but a few ask whether it is # defined - so a zero from the config file would turn the navigation columns # half on $show_tla = undef unless (defined($show_tla) && $show_tla); if (defined($show_ownerBins)) { # the command line spelling is an optional-value string, so it can only be # undef, '' or the value the user typed - but the config file spells the # same option as a boolean. Every test of whether the tables were asked # for at all asks whether this is defined, so a '0' from the config file # would turn them on: normalize the boolean forms to the two the rest of # the code understands, and reject anything else here rather than letting # it reach a string compare which quietly reads it as 'not all' if ($show_ownerBins eq '0') { $show_ownerBins = undef; } else { # lcovrc(5) documents '1' as equivalent to the flag with no value $show_ownerBins = '' if $show_ownerBins eq '1'; unless ($show_ownerBins eq '' || $show_ownerBins eq 'all') { lcovutil::ignorable_error($lcovutil::ERROR_USAGE, "Unsupported '--show-owners' value '$show_ownerBins': expected 'all', or no value ('0' or '1' from a configuration file)." ); # message ignored: treat it as the flag with no value $show_ownerBins = ''; } } } foreach my $rc ([\@datebins, \@rc_date_bins], [\@SummaryInfo::ageGroupHeader, \@rc_date_labels], [\@SourceFile::annotateScript, \@rc_annotate_script], [\@SummaryInfo::selectCallbackScript, \@rc_select_script], [\@simplifyFunctionScript, \@rc_simplifyFunctionScript], ) { @{$rc->[0]} = @{$rc->[1]} unless (@{$rc->[0]}); } foreach my $cb ([\$SourceFile::annotateCallback, \@SourceFile::annotateScript], [\$SummaryInfo::selectCallback, \@SummaryInfo::selectCallbackScript ], [\$simplifyFunctionCallback, \@simplifyFunctionScript], ) { lcovutil::configure_callback($cb->[0], @{$cb->[1]}) if scalar(@{$cb->[1]}); } # we won't apply simplifications if we don't generate the table # (we still check that the callback is valid - even though we don't use it) $simplifyFunctionCallback = undef if $no_sourceview; if (defined($lcovutil::stop_on_error) && !$lcovutil::stop_on_error) { # in the spirit of "don't stop" - don't worry about missing files. $synthesizeMissingFile = 1; } # Merge sort options $sort_tables = 0 if ($no_sort); die( "unsupported use of mutually exclusive '--flat' and '--hierarchical' options") if ($flat && $hierarchical); $show_tla = 1 if (@base_filenames || defined($diff_filename)); if ($show_tla && (0 == scalar(@base_filenames) && !defined($diff_filename)) ) { # no baseline - so not a differential report. # modify some settings to generate corresponding RTL code. $use_legacyLabels = 1; SummaryInfo::noBaseline(); } else { $show_hitTotalCol = 1; } if (@SourceFile::annotateScript) { $show_dateBins = 1; if (0 == scalar(@datebins)) { # default: 7, 30, 180 days @datebins = @SummaryInfo::defaultCutpoints; } else { my %uniqify = map { $_, 1 } split($lcovutil::split_pattern, join($lcovutil::split_char, @datebins)); @datebins = sort(keys %uniqify); } SummaryInfo::setAgeGroups(@datebins); if (defined($show_ownerBins)) { @truncateOwnerTableLevels = split($lcovutil::split_pattern, join($lcovutil::split_char, @truncateOwnerTableLevels)); foreach my $l (@truncateOwnerTableLevels) { lcovutil::ignorable_error($lcovutil::ERROR_USAGE, "Unknown 'truncate_owner_table' level '$l': should be 'top', 'directory', or 'file'." ) unless grep(/^$l$/, ('top', 'directory', 'file')); } lcovutil::ignorable_error($lcovutil::ERROR_USAGE, "Unsupported value 'owner_table_entries = $ownerTableElements': expected positive integer." ) unless (!defined($ownerTableElements) || (Scalar::Util::looks_like_number($ownerTableElements) && 0 < $ownerTableElements)); } } else { $treatNewFileAsBaseline = undef; die("\"--show-owners\" option requires \"--annotate-script\" for revision control integration" ) if defined($show_ownerBins); die("\"--date-bins\" option requires \"--annotate-script\" for revision control integration" ) if (0 != scalar(@datebins)); } unless (Scalar::Util::looks_like_number($alias_list_length) && $alias_list_length == int($alias_list_length) && 0 <= $alias_list_length) { lcovutil::ignorable_error($lcovutil::ERROR_USAGE, "Unsupported value 'genhtml_alias_list_length = $alias_list_length': expected non-negative integer." ); # ..ignored: the default $alias_list_length = 10; } @metricColumnOrder = parseMetricColumnOrder($metric_column_order); # Inconsistent options: no way to both NOT show the alias group summary and # not show alias groups. if ($suppress_alias_groups && !$show_callgraph_summary) { lcovutil::ignorable_warning($lcovutil::ERROR_USAGE, "'suppress_alias_groups = 1' conflicts with 'show_callgraph_summary = 0': alias groups are shown." ); # error ignored - suppress the 'summary blocks' and show the alias groups $suppress_alias_groups = 0; } if (0 != (defined($diff_filename) ^ (0 != scalar(@base_filenames)))) { if (@base_filenames) { lcovutil::ignorable_warning($lcovutil::ERROR_USAGE, "Specified --baseline-file without --diff-file: assuming no source differences. Hope that is OK.\n" ); } else { lcovutil::ignorable_warning($lcovutil::ERROR_USAGE, "Specified --diff-file without --baseline-file: assuming baseline code coverage was empty (nothing covered). Hope that is OK\n" ); # OK..just assume that the baseline is empty... } $show_tla = 1; } if (defined($header_title)) { $title = $header_title; } else { # use the default title bar. $title =~ s/ differential// # not a differential report, if no baseline... unless defined($diff_filename) || 0 != scalar(@base_filenames); } push(@fileview_prefixes, "-date") if ($show_dateBins); push(@fileview_prefixes, "-owner") if (defined($show_ownerBins)); # use LCOV original colors if no baseline file # (so no differential coverage) if ($use_legacyLabels || !$show_tla || (defined($show_simplifiedColors) && $show_simplifiedColors) ) { lcovutil::use_vanilla_color(); } if ($dark_mode) { # if 'dark_mode' is set, then update the color maps # For the moment - just reverse the foreground and background my %reverse; while (my ($key, $value) = each(%lcovutil::normal_palette)) { $reverse{lc($value)} = $key; } # every TLA whose color can reach the stylesheet, not just the ones in the # current presentation order: 'noBaseline' has already run at this point, # so @tlaPriorityOrder is down to two entries in legacy-label mode foreach my $tla (@SummaryInfo::allTla) { # swap my $bg = $lcovutil::tlaColor{$tla}; my $key = substr($bg, 1); # if this color is in normal_palette, then swap to dark_palette # version. Otherwise, just swap the 'tlaColor' and 'tlaTextColor' # (foreground and background) if (exists($reverse{lc($key)})) { my $k = $reverse{lc($key)}; $lcovutil::tlaColor{$tla} = '#' . uc($lcovutil::dark_palette{$k}); } else { $lcovutil::tlaColor{$tla} = $lcovutil::tlaTextColor{$tla}; } $lcovutil::tlaTextColor{$tla} = $bg; } } @info_filenames = AggregateTraces::find_from_glob(@ARGV); # Split the list of prefixes if needed parse_dir_prefix(@opt_dir_prefix); # Check for info filename if (!@info_filenames) { die("No filename specified\n" . "Use $tool_name --help to get usage information\n"); } # Generate a title if none is specified $test_title = compute_title(\@ARGV, \@info_filenames) unless $test_title; if (@base_filenames) { my @base_patterns; @base_patterns = @base_filenames unless $baseline_title; @base_filenames = AggregateTraces::find_from_glob(@base_filenames); $baseline_title = compute_title(\@base_patterns, \@base_filenames) unless $baseline_title; my $baseline_create; if ($baseline_date) { eval { my $epoch = Date::Parse::str2time($baseline_date); $baseline_create = DateTime->from_epoch(epoch => $epoch); }; if ($@) { #did not parse lcovutil::info( "failed to parse date '$baseline_date' - falling back to file creation time\n" ); } } if (!defined($baseline_create)) { # if not specified, use 'last modified' of first baseline trace file my $create = (stat($base_filenames[0]))[9]; $baseline_create = DateTime->from_epoch(epoch => $create); $baseline_date = get_date_string($create) unless defined($baseline_date); } $age_basefile = $baseline_create->delta_days(DateTime->now())->in_units('days'); } # Make sure css_filename is an absolute path (in case we're changing # directories) if ($css_filename) { if (!File::Spec->file_name_is_absolute($css_filename)) { $css_filename = File::Spec->catfile($cwd, $css_filename); } } # Get HTML prolog and epilog $html_prolog = get_html_prolog($html_prolog_file); $html_epilog = get_html_epilog($html_epilog_file); # Issue a warning if --no-sourceview is enabled together with --frames if (defined($frames)) { if ($no_sourceview) { lcovutil::ignorable_warning($lcovutil::ERROR_USAGE, "option --frames disabled because --no-sourceview was specified."); $frames = undef; } elsif ($show_tla) { lcovutil::info( "Note: file table to source location navigation hyperlinks are disabled when --frames is enabled\n" ); } } # Issue a warning if --no-prefix is enabled together with --prefix if ($no_prefix && @dir_prefix) { lcovutil::ignorable_warning($lcovutil::ERROR_USAGE, "option --prefix disabled because --no-prefix was ignored"); @dir_prefix = (); } @fileview_sortlist = ($SORT_FILE); @funcview_sortlist = ($SORT_FILE); @callgraphview_sortlist = ($SORT_FILE); if ($sort_tables) { # the call site pages are in the order of each column which describes a # function rather than one of its calls - a file's page is written in only # those of them whose column for which it contains data - i.e., # not in the 'callee' column if we don't know who its callees are. # see 'callgraphPageSorts' push(@callgraphview_sortlist, $SORT_MISSING_CALLGRAPH, $SORT_XREF, $SORT_OWNER, $SORT_AGE) if ($lcovutil::callgraph_coverage); push(@fileview_sortlist, $SORT_LINE); push(@fileview_sortlist, $SORT_FUNC) if ($lcovutil::func_coverage); push(@fileview_sortlist, $SORT_BRANCH) if ($lcovutil::br_coverage); push(@fileview_sortlist, $SORT_MCDC) if ($lcovutil::mcdc_coverage); push(@fileview_sortlist, $SORT_CALLGRAPH) if ($lcovutil::callgraph_coverage); push(@funcview_sortlist, $SORT_LINE); if ($show_functionProportions) { push(@funcview_sortlist, $SORT_MISSING_LINE); push(@funcview_sortlist, $SORT_MISSING_BRANCH) if ($lcovutil::br_coverage); push(@funcview_sortlist, $SORT_MISSING_MCDC) if ($lcovutil::mcdc_coverage); push(@funcview_sortlist, $SORT_MISSING_CALLERS, $SORT_MISSING_CALLGRAPH) if ($lcovutil::callgraph_coverage); } } if ($frames) { # Include genpng code needed for overview image generation do(File::Spec->catfile($tool_dir, 'genpng')); } # Make sure precision is within valid range check_precision(); # ..and the same for the source view field widths: none of the five has a # command line form, so nothing has type checked them, and they are used as # arithmetic - a zero divides by zero when the data has to be distributed # over several lines, and a value perl does not read as a number formats to # nothing at all, one warning per source line foreach my $width (['genhtml_line_field_width', \$line_field_width], ['genhtml_branch_field_width', \$br_field_width], ['genhtml_mcdc_field_width', \$mcdc_field_width], ['genhtml_owner_field_width', \$owner_field_width], ['genhtml_age_field_width', \$age_field_width] ) { my $value = ${$width->[1]}; # string match, so the message can quote what was actually written die("'$width->[0] = $value' is not a positive integer\n") unless $value =~ /^\d+$/ && $value > 0; } # Make sure output_directory exists, create it if necessary $output_directory = '.' if !defined($output_directory); if ($output_directory && !-d $output_directory) { info(1, "make_path $output_directory\n"); File::Path::make_path($output_directory); } if ($save && $output_directory) { # save copy of .info and diff files: useful for debugging user cases later # the layout (directory + naming) is defined once in SavedReport so that # html2lcov reads back exactly what we write here. SavedReport::save_inputs($output_directory, \@info_filenames, \@base_filenames, $diff_filename); } # save command line in output directory - useful for later debugging: my $f = File::Spec->catfile($output_directory, 'cmd_line'); open(CMD, '>', $f) or die("unable to open $f: $!"); print(CMD $lcovutil::profileData{config}{cmdLine} . "\n"); close(CMD) or die("unable to close $f: $!\n"); my $exit_status = 0; # Do something my $now = Time::HiRes::gettimeofday(); my $top; eval { $top = gen_html(); }; if ($@) { $exit_status = 1; print(STDERR $@); } my $then = Time::HiRes::gettimeofday(); $lcovutil::profileData{total} = $then - $now; if (0 == $exit_status) { # warn about unused include/exclude directives lcovutil::warn_file_patterns(); ReadCurrentSource::warn_sourcedir_patterns(); } # now check the coverage criteria (if any) if (0 == $exit_status && ($CoverageCriteria::coverageCriteriaStatus || @CoverageCriteria::coverageCriteriaScript) ) { CoverageCriteria::summarize(); # fail for signal or status $exit_status = (($CoverageCriteria::coverageCriteriaStatus & 0xFF) | ($CoverageCriteria::coverageCriteriaStatus >> 8)); } if ($serialize) { #my $f = File::Spec->catfile($output_directory, 'coverage.dat'); my $data; eval { $data = Storable::store($top, $serialize); }; if ($@ || !defined($data)) { print("unable to serialize coverage data", $@ ? ": $@" : ''); $exit_status = 1; } } lcovutil::cleanup_callbacks(); lcovutil::save_profile(File::Spec->catfile($output_directory, 'genhtml'), File::Spec->catfile($output_directory, 'profile.html')); if (0 == $exit_status && $validateHTML) { ValidateHTML->new($output_directory, '.' . $html_ext); } # exit with non-zero status if --keep-going and some errors detected $exit_status = 1 if (0 == $exit_status && lcovutil::saw_error()); # the 'GenHtml' object - and its 'File::Temp' child data directory - # went out of scope when 'gen_html' returned, so the parent is empty by now lcovutil::temp_cleanup(); exit($exit_status); # # print_usage(handle) # # Print usage information. # sub print_usage(*) { local *HANDLE = $_[0]; print(HANDLE < 0 will replace them with NUM spaces NUM < 0 will only change their appearance --legend Include color legend in HTML output --html-prolog FILE Use FILE as HTML prolog for generated pages --html-epilog FILE Use FILE as HTML epilog for generated pages --html-extension EXT Use EXT as filename extension for pages --html-gzip Use gzip to compress HTML --(no-)sort Enable (disable) sorted coverage views --demangle-cpp [OPT] Demangle C++ function names --precision NUM Set precision of coverage rate --missed Show miss counts as negative numbers --dark-mode Use the dark-mode CSS --simplified-colors Use reduced color scheme for categories --hierarchical Generate multilevel HTML report, matching source code directory structure --flat Generate flat HTML report, with all files listed on top-level page -s, --show-details Generate detailed directory view --show-owners [all] Show owner summary table. If optional value provided, show all the owners, regardless of whether they have uncovered code or not --show-noncode Show author in summary table even if none of their lines are recognized as code --show-zero-columns Keep summary columns for categories with no entries (default: remove) --show-navigation Include 'goto first hit/not hit' and 'goto next hit/not hit' hyperlinks in non-differential source code detail page --show-proportion Show function coverage rates --suppress-aliases Merge data for function aliases For more information see the genhtml man page. END_OF_USAGE } # # print_overall_rate(trace, ln_do, fn_do, callgraph_do, br_do, mcdc_do, summary) # # Print overall coverage rates for the specified coverage types. # sub print_overall_rate($$$$$$$) { my ($currentTrace, $ln_do, $fn_do, $callgraph_do, $br_do, $mcdc_do, $summary) = @_; # use verbosity level -1: so print unless user says "-q -q"...really quiet info(-1, "Overall coverage rate:\n"); my @types; push(@types, SummaryInfo::LINE_DATA) if $ln_do; push(@types, SummaryInfo::FUNCTION_DATA) if $fn_do; push(@types, SummaryInfo::CALLGRAPH_DATA) if $callgraph_do; push(@types, SummaryInfo::BRANCH_DATA) if $br_do; push(@types, SummaryInfo::MCDC_DATA) if $mcdc_do; # use source file count from current - we don't care about files # that were deleted and are in baseline info(-1, " source files: %d\n", scalar($currentTrace->files())); my $indent = ' '; for my $type (@types) { my $found = $summary->get("found", $type); # user was confused about why there was no coverage data my $explain = ($type == SummaryInfo::LINE_DATA && 0 == $found && $SummaryInfo::selectCallback) ? " (your '--select-script' criteria may not match any coverpoints)" : ''; info(-1, "%s%s\n", lcovutil::summaryRow(SummaryInfo::type2str($type), $found, $summary->get("hit", $type)), $explain); if ($main::show_tla) { for my $tla (@SummaryInfo::tlaOrderNoExclude) { my $v = $summary->get($tla, $type); next if $v == 0; my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacySrcLabel{$tla} : $tla; my $f = '.' x ($lcovutil::summaryLabelWidth - (length($indent) + length($label))); info(-1, " $indent$label$f: $v\n"); } } } summarize_cov_filters(); summarize_messages(); } # # gen_html() # # Generate a set of HTML pages from contents of .info file INFO_FILENAME. # Files will be written to the current directory. If provided, test case # descriptions will be read from .tests file TEST_FILENAME and included # in output. # # Die on error. # sub gen_html() { # Read in all specified .info files my $now = Time::HiRes::gettimeofday(); my $readSourceFile = ReadCurrentSource->new(); ($current_data) = AggregateTraces::merge($readSourceFile, @info_filenames); my $then = Time::HiRes::gettimeofday(); $lcovutil::profileData{parse_current} = $then - $now; info("Found %d entries.\n", scalar($current_data->files())); # Read and apply diff data if specified - need this before we # try to read and process the baseline.. if ($diff_filename) { $now = Time::HiRes::gettimeofday(); info("Reading diff file $diff_filename\n"); $diff_data->load($diff_filename, $current_data, \@lcovutil::build_directory); $then = Time::HiRes::gettimeofday(); $lcovutil::profileData{parse_diff} = $then - $now; } # Read and apply baseline data if specified if (@base_filenames) { $now = Time::HiRes::gettimeofday(); my $readBaseSource = ReadBaselineSource->new($diff_data); $then = Time::HiRes::gettimeofday(); $lcovutil::profileData{parse_source} = $then - $now; # Read baseline file $now = Time::HiRes::gettimeofday(); ($base_data) = AggregateTraces::merge($readBaseSource, @base_filenames); info("Found %d baseline entries.\n", scalar($base_data->files())); foreach my $f ($base_data->files()) { next unless $base_data->data($f)->span()->entries(); $baseline_has_spans = 1; last; } $then = Time::HiRes::gettimeofday(); $lcovutil::profileData{parse_baseline} = $then - $now; } elsif (defined($diff_filename)) { # no data.. $base_data = TraceFile->new(); } if ($diff_filename) { # check for files which appear in the udiff but which don't appear # in either the current or baseline trace data. Those may be # mapping issues - different pathname in .info file vs udiff if (!$diff_data->check_path_consistency($base_data, $current_data)) { lcovutil::ignorable_error($lcovutil::ERROR_PATH, "possible path inconsistency in baseline/current/udiff data"); } } if ($no_prefix) { # User requested that we leave filenames alone info("User asked not to remove filename prefix\n"); } elsif (!@dir_prefix) { # Get prefix common to most directories in list my $prefix = get_prefix(1, $current_data->files()); if ($prefix) { info("Found common filename prefix \"$prefix\"\n"); $dir_prefix[0] = $prefix; } else { info("No common filename prefix found!\n"); $no_prefix = 1; } } else { my $msg = "Using user-specified filename prefix "; my $dirs = $current_data->directories(); my $i = 0; # somewhat of a hack: the layout code doesn't react well when # the 'prefix' directory contains source files (as opposed to # containing a directory which contains source files). # Rather than trying to handle that special case, just munge the # prefix to be something we like better. while ($i <= $#dir_prefix) { my $p = $dir_prefix[$i]; # remove redundant /'s $p =~ s/$lcovutil::dirseparator_re+$//; $p = substr($p, 0, -1) if $lcovutil::dirseparator eq substr($p, -1); while (exists($dirs->{$p}) && $p) { $p = File::Basename::dirname($p); } unless ($p) { lcovutil::info("skipping prefix $dir_prefix[$i]\n"); splice(@dir_prefix, $i, 1); next; } lcovutil::info( "using prefix '$p' (rather than '$dir_prefix[$i]')\n") if ($p ne $dir_prefix[$i]); $dir_prefix[$i] = $p; $msg .= ", " unless 0 == $i; $msg .= "\"" . $p . "\""; ++$i; } info($msg . "\n"); } # Read in test description file if specified if ($desc_filename) { info("Reading test description file $desc_filename\n"); %test_description = %{read_testfile($desc_filename)}; # Remove test descriptions which are not referenced # from %current_data if user didn't tell us otherwise if (!$keep_descriptions) { remove_unused_descriptions(); } } # add quotes to args - so string concat works if there are embedded spaces, etc foreach my $s (\@SourceFile::annotateScript, \@CoverageCriteria::coverageCriteriaScript, \@lcovutil::extractVersionScript) { my $count = scalar(@$s); next unless $count > 1; foreach my $e (@$s) { $e = "'$e'" if ($e =~ /\s/); } } unless ($no_html) { info(1, "Writing .css and .png files.\n"); write_css_file(); write_png_files(); } if ($html_gzip) { info(1, "Writing .htaccess file.\n"); write_htaccess_file(); } # Everything is read, merged and filtered, and this is the parent process: # the one point at which the callee-side view of the call data can be # built - see TraceFile::resolveCallSites - and it has to be before any # summary reads it. The baseline is resolved too, since the differential # categorization of a site compares the two. if ($lcovutil::callgraph_coverage) { # ...which may turn the metric off, if the user chose to ignore the error $current_data->checkCallgraphData(); if ($lcovutil::callgraph_coverage) { $current_data->resolveCallSites(); $base_data->resolveCallSites() if defined($base_data); # ..and the baseline's names for the aliases the two versions merged # differently are the current ones: see # TraceFile::adoptAliasNames. $base_data->resolveCallSites() if defined($base_data) && $base_data->adoptAliasNames($current_data); # ..and index where each symbol is defined, which is the key that # the callee cell of a call-site row links to. # Built here, in the parent, because it is a whole-report # question and a forked child holds only its own segment # ..after the symbols this report calls, which indicates # whether the callgraph data of a file whose own do not have # any callsite actually exists - see # FunctionMap::hasCallgraphData. Another whole-report question - # used by the index $callgraphCalledSymbols = $current_data->calledSymbols(); build_callee_location_index($current_data); # ..and categorize every site now, rather than when its own file is # processed: a site's category is displayed by the file of its # callee as well as by the file it is in. See the sub # Whether or not there is a baseline: a report with none still gives # every site a category ('GNC' or 'UNC'), and the 'callers of this # function' table displays it, so leaving the pass out made that # table depend on the file order and on '--parallel' categorize_all_call_sites($current_data, $base_data, $diff_data); # ..and, last, the sites each file's text holds which belong to a # function of another file: after the categorization, so that the # groups carry the category the site was given build_callgraph_foreign_index($current_data); } } # ..and without a callgraph, where nothing is merged but the inputs may have # been, the same - which, after the above, finds nothing more to do $base_data->adoptAliasNames($current_data) if defined($base_data); info("Generating output.\n"); my $genhtml = GenHtml->new($current_data); if (@SourceFile::annotateScript && 0 == $SourceFile::annotatedFiles) { lcovutil::ignorable_error( $lcovutil::ERROR_ANNOTATE_SCRIPT, "\"--annotate-script '" . join(' ', @SourceFile::annotateScript) . "'\" did not find any revision-controlled files in your sandbox" ); } # Check if there are any test case descriptions to write out if (%test_description) { info("Writing test case description file.\n"); write_description_file(\%test_description, $genhtml->top()); } print_overall_rate($current_data, 1, $lcovutil::func_coverage, $lcovutil::callgraph_coverage, $lcovutil::br_coverage, $lcovutil::mcdc_coverage, $genhtml->top()); return $genhtml->top(); } # # html_create(handle, filename) # sub html_create($$) { my $handle = $_[0]; # only the report-relative part is lower-cased: 'case_insensitive' is about # matching the names which came out of the coverage data, not about # renaming the directory the user asked for - and the sites which create # those directories, and 'write_css_file'/'write_htaccess_file', do not # lower-case it either, so lower-casing the joined path here names a file # in a directory nothing ever creates my $rel = $lcovutil::case_insensitive ? lc($_[1]) : $_[1]; my $filename = File::Spec->catfile($output_directory, $rel); if ($html_gzip) { # quote the name: may be spaces in the user's '--output-directory' open($handle, "|-", "gzip -c > " . lcovutil::shell_quote($filename)) or die("cannot open $filename for writing (gzip): $!\n"); } else { open($handle, ">", $filename) or die("cannot open $filename for writing: $!\n"); } } # $ctrls = [$view_type, $sort_type, $bin_prefix] # $perTestcaseResult = [\%line, \%func, \%branch] #sub write_dir_page($$$$$$$;$) sub write_dir_page { my ($callback_type, $ctrls, $page_suffix, $title, $rel_dir, $base_dir, $trunc_dir, $summary, $perTestcaseResult) = @_; my $bin_prefix = $ctrls->[3]; # Generate directory overview page including details html_create(*HTML_HANDLE, File::Spec->catfile($rel_dir, "index$bin_prefix$page_suffix.$html_ext")); if (!defined($trunc_dir)) { $trunc_dir = ""; } $title .= " - " if ($trunc_dir ne ""); write_html_prolog(*HTML_HANDLE, $base_dir, "LCOV - $title$trunc_dir"); my $activeTlaColsForType = write_header(*HTML_HANDLE, $callback_type, $ctrls, $trunc_dir, $rel_dir, $summary, undef, undef); if (0 != $summary->sources()) { write_file_table(*HTML_HANDLE, $callback_type, $base_dir, $perTestcaseResult, $summary, $ctrls, $activeTlaColsForType); } else { my $msg = "Coverage data table is empty - no coverpoints exist in the selected subset."; write_html(*HTML_HANDLE, <
$msg

END_OF_HTML } write_html_epilog(*HTML_HANDLE, $base_dir); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); } sub unsortParentLinks { # The sorted pages of the directories below '$rel_dir' link to its view in # the same order - see 'write_header' - but it writes those views only if # its table has more than one entry - which we know only in the parent, # after the child pages were written. Thus: if the directory table has # only one entry, we regexp the child page link so they link to the # parent page (sorted by name) my $rel_dir = shift; return if $main::no_html; my $top = File::Spec->catdir($main::output_directory, $rel_dir); my @sorts = grep({ $_ ne '' } @main::fileview_sortname); my $sorts = join('|', map({ quotemeta($_) } @sorts)); my @pages; my $wanted = sub { push(@pages, $File::Find::name) if $File::Find::dir ne $top && /^index.*(?:$sorts)\.\Q$html_ext\E$/; }; File::Find::find($wanted, $top); foreach my $page (@pages) { my $rel = File::Spec->abs2rel(File::Basename::dirname($page), $top); my $up = '../' x scalar(File::Spec->splitdir($rel)); my $quoted = lcovutil::shell_quote($page); my ($in, $out, $html); if ($main::html_gzip) { open($in, '-|', "gzip -dc $quoted") or die("unable to read $page: $!\n"); } else { open($in, '<', $page) or die("unable to read $page: $!\n"); } { local $/; $html = <$in>; } close($in) or die("unable to close $page: $!\n"); my $fixed = $html; $fixed =~ s{href="\Q$up\E(index[^"/]*?)(?:$sorts)\.\Q$html_ext\E"} {href="$up$1.$html_ext"}g; next if $fixed eq $html; if ($main::html_gzip) { open($out, '|-', "gzip -c > $quoted") or die("unable to write $page: $!\n"); } else { open($out, '>', $page) or die("unable to write $page: $!\n"); } print($out $fixed); close($out) or die("unable to close $page: $!\n"); } } sub write_summary_pages($$$$$$$$) { my ($name, $summaryType, $summary, $show_details, $rel_dir, $base_dir, $trunc_dir, $testhashes) = @_; my $callback_type = $summaryType == 1 ? 'directory' : 'top'; foreach my $c ($summary->sources()) { my $child = $summary->get_source($c); # filter this one out if no data if ($child->is_empty()) { $summary->remove_source($c); } } return if $main::no_html; if (0 == $summary->sources() && $summaryType == 1) { # remove empty directory push(@cleanDirectoryList, $rel_dir); return; } if ($main::show_tla) { info('Processing ' . ($summaryType == 1 ? ('directory: ' . $summary->name()) : 'top level:') . "\n"); my $sep = $summaryType == 1 ? $summary->name() : ''; foreach my $t ('line', $lcovutil::br_coverage ? 'branch' : undef, $lcovutil::mcdc_coverage ? 'mcdc' : undef, $lcovutil::func_coverage ? 'function' : undef, $lcovutil::callgraph_coverage ? 'callgraph' : undef ) { my $s; $s = $summary->tlaSummary($t) if defined($t); next unless $s; info(" $t: $s\n"); } } my $start = Time::HiRes::gettimeofday(); my @summaryBins; if (defined($main::show_ownerBins)) { if ( 0 != scalar($summary->owners($main::show_ownerBins, SummaryInfo::LINE_DATA )) || ( $lcovutil::br_coverage && 0 != scalar($summary->owners($main::show_ownerBins, SummaryInfo::BRANCH_DATA ))) || ( $lcovutil::mcdc_coverage && 0 != scalar($summary->owners($main::show_ownerBins, SummaryInfo::MCDC_DATA ))) ) { # at least one owner will appear - so table will be referenced push(@summaryBins, 'owner'); } else { lcovutil::info(1, $summary->name() . " has no visible owners..eliding page\n"); } } push(@summaryBins, 'date') if defined($main::show_dateBins); my $singleSource = 1 == scalar($summary->sources()); # the table has a column - and so a link to the table sorted that data # - but only if this directory actually contains data of that covtype # See 'write_file_table_prolog' my %hasColumn = ($SORT_FUNC => $summary->function_found(), $SORT_BRANCH => $summary->branch_found(), $SORT_MCDC => $summary->mcdc_found(), $SORT_CALLGRAPH => $summary->callgraph_found()); my @dirPageCalls; foreach my $sort_type (@main::fileview_sortlist) { next if exists($hasColumn{$sort_type}) && !$hasColumn{$sort_type}; my @ctrls = ($summaryType, # 1 == 'list files' "name", # primary key $sort_type, "", $singleSource); my $sort_str = $main::fileview_sortname[$sort_type]; # 'fileview prefixes' is ('', '-date', '-owner) foreach my $bin_prefix (@main::fileview_prefixes) { # Generate directory overview page (without details) # no per-testcase data in this page... $ctrls[3] = $bin_prefix; # need copy because we are calling multiple child processes my @copy = @ctrls; push(@dirPageCalls, [\@copy, $sort_str, $test_title, $rel_dir, $base_dir, $trunc_dir, $summary ]); if ($show_details) { # Generate directory overview page including details push(@dirPageCalls, [\@copy, "-detail" . $sort_str, $test_title, $rel_dir, $base_dir, $trunc_dir, $summary, $testhashes ]); } } $ctrls[3] = ""; # no bin... foreach my $primary_key (@summaryBins) { # we don't associate function owner - so we elide 'function' columns in the # 'owner detail' pages - and thus won't create a sort-by-function link. # Don't generate the unreferenced page next if $sort_type == $SORT_FUNC && $primary_key eq 'owner'; # we elide the 'line' sort links for date/owner page if there # is only one file next if $sort_type == $SORT_LINE && $primary_key ne 'name' && scalar($summary->sources()) < 2; $ctrls[1] = $primary_key; my @copy = @ctrls; push(@dirPageCalls, [\@copy, '-bin_' . $primary_key . $sort_str, $test_title, $rel_dir, $base_dir, $trunc_dir, $summary ]); } last if $singleSource; } foreach my $params (@dirPageCalls) { write_dir_page($callback_type, @$params); last # only write 'index.html' - not the sorted versions if ($summary->is_empty()); } unsortParentLinks($rel_dir) if $singleSource || $summary->is_empty(); my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{html}{$name} = $end - $start; } sub write_function_page($$$$$$$$$$$$$$$) { # Generate function table for this file my ($fileCovInfo, $base_dir, $rel_dir, $trunc_dir, $base_name, $title, $sumcount, $funcdata, $testfncdata, $sumbrcount, $testbrdata, $mcdc, $testcase_mcdc, $sort_type, $summary) = @_; # $fileCovInfo is array [function hash, line hash, branch hash, mcdc hash, # callee hash, call site detail] - see 'callgraphDetailEntries' for the # last my $filename; if ($sort_type == $main::SORT_FILE) { $filename = File::Spec->catfile($rel_dir, "$base_name.func.$html_ext"); } elsif ($sort_type == $main::SORT_LINE) { # by declaration line number $filename = File::Spec->catfile($rel_dir, "$base_name.func-c.$html_ext"); } elsif ($sort_type == $main::SORT_MISSING_LINE) { # by proportion of exercised lines $filename = File::Spec->catfile($rel_dir, "$base_name.func-l.$html_ext"); } elsif ($sort_type == $main::SORT_MISSING_MCDC) { # by proportion of exercised MC/DC expressions return unless %{$fileCovInfo->[3]}; $filename = File::Spec->catfile($rel_dir, "$base_name.func-m.$html_ext"); } elsif ($sort_type == $main::SORT_MISSING_CALLGRAPH) { # by proportion of exercised call sites return unless %{$fileCovInfo->[4]}; $filename = File::Spec->catfile($rel_dir, "$base_name.func-x.$html_ext"); } elsif ($sort_type == $main::SORT_MISSING_CALLERS) { # by proportion of exercised calls into the function - whose column is # there where the file has a callers page return unless defined($fileCovInfo->[5]) && $fileCovInfo->[5]->[CALLGRAPH_VIEW_CALLERS]; $filename = File::Spec->catfile($rel_dir, "$base_name.func-r.$html_ext"); } else { die("Unexpected sort $sort_type") unless ($sort_type == $main::SORT_MISSING_BRANCH); # don't emit page if there are no branches in the file return unless %{$fileCovInfo->[2]}; # by proportion of exercised branches $filename = File::Spec->catfile($rel_dir, "$base_name.func-b.$html_ext"); } html_create(*HTML_HANDLE, $filename); my $pagetitle = "LCOV - $title - " . File::Spec->catfile($trunc_dir, $base_name) . " - functions"; write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle); write_header(*HTML_HANDLE, 'file', # function table always written from 'file' level [$HDR_FUNC, 'name', $sort_type,], File::Spec->catfile($trunc_dir, $base_name), File::Spec->catfile($rel_dir, $base_name), $summary, undef, $fileCovInfo->[0], $fileCovInfo->[5]); write_function_table(*HTML_HANDLE, $fileCovInfo, "$base_name.gcov.$html_ext", $sumcount, $funcdata, $testfncdata, $sumbrcount, $testbrdata, $mcdc, $testcase_mcdc, $base_name, $base_dir, $sort_type); write_html_epilog(*HTML_HANDLE, $base_dir, 1); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); } sub write_callgraph_page($$$$$$$$$$$) { # One direction of the callgraph of one file: the calls made to each of its # functions, or the calls each of them makes. A page per direction rather # than one table carrying both, for two reasons # - a reader follows one direction at a time, and the two sides of a # function are neither the same length nor the same kind of row, so a # row of a combined table holds two unrelated facts, and # - the two directions are sorted by different numbers: the callers of a # function and the calls it makes are two proportions, and one table can # only be in one order. # ..and a page of its own rather than rows of the function table, because # the anchors the source view and the other direction lead to are then in # one place rather than in whichever sorted page happened to be written. # '$entries' is the functions this page shows, already selected: a function # is on the page of a direction only when it has something to say in that # direction - see 'callgraphViewEntries' - and every page of the file is # written from the one list. '$sort_type' puts them in order. # See '--callgraph-coverage' in genhtml(1) my ($view, $sort_type, $entries, $pages, $differentialMap, $base_dir, $rel_dir, $trunc_dir, $base_name, $title, $summary) = @_; my $page = $callgraphView[$view]; html_create(*HTML_HANDLE, File::Spec->catfile($rel_dir, callgraphPageName( $base_name, $view, $sort_type ))); my $pagetitle = "LCOV - $title - " . File::Spec->catfile($trunc_dir, $base_name) . ' - ' . $page->{title}; write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle); # the categories this page's rows have, if any - and whether they are a column # of their own. In a report whose only two categories are 'hit' and # 'missed' they are not: that column would say in a word what the count # cell beside it already says in a colour, and what is not a duplicate there # - the word itself, and the tour of one category - moves onto the count. # See '_callgraphCountCell' my $hasTlas = $main::show_tla && 0 != scalar(keys %$differentialMap); my $foldTlas = $hasTlas && $main::use_legacyLabels; my $showTlas = $hasTlas && !$foldTlas; my $source = pageName($base_name, 'gcov'); # the columns this page's own side has - a name, a location, a category where # the report has categories, and a count - which is what a row saying # something about the side rather than carrying a call has to span my $span = $showTlas ? 4 : 3; # ..and the two columns which are not about calls at all: who last changed # the function and when. Written only where the annotation callback # answered for this file, since a pair of empty columns is not a fact my $showAnnot = grep({ defined($_->{annot}) } @$entries) ? 1 : 0; # ..and the column which leads to the other direction of the callgraph, which # is there only where the file has that page: a column no function of this # file could have an entry in is not a fact either my $other = $callgraphView[1 - $view]; my $showXref = (defined($pages) && $pages->[1 - $view]) ? 1 : 0; my $columns = ''; # ..which is left of the name, not right of it: the name is what the reader # came to this page to find, so nothing of this function's own should be # pushed away from the columns which describe its calls foreach my $head (($showAnnot ? map({ callgraphview_get_label($base_name, $base_dir, $_, $view, $sort_type) } ('age', 'owner')) : ()), ($showXref ? callgraphview_get_label($base_name, $base_dir, 'xref', $view, $sort_type) : ()), callgraphview_get_label( $base_name, $base_dir, undef, $view, $sort_type ), callgraphview_get_label( $base_name, $base_dir, 'hit', $view, $sort_type ), @{$page->{heading}} ) { $columns .= _callgraphCellLine("$head"); } $columns .= _callgraphCellLine(_callgraphTlaHead($showTlas)) . _callgraphCellLine('Count'); # Every row of the page, in the order the page is in, before any of it is # written: the category cell of a row leads to the start of the next group # of that same category, and which row that is cannot be known until all of # them are. The date and owner cells lead the same way, by the group of # one function rather than of one row - which is what they describe. # Built before the header, too: the header leads into this table (see # CallgraphPageIndex), and on the callers page its callgraph numbers are # counted from it. There they are the calls into this file rather than # the calls its functions make, which is what this page lists: the # callees page lists the calls the numbers of every other page count my (@blocks, @owns, @blockAnchors, @heights, @dateKeys, @ownerKeys, @colours); my (@tlaKeys, @countKeys, @anchors); my $siteIndex = CallgraphPageIndex->new(); $summary = $summary->withCallgraphDestinations() if $view == CALLGRAPH_VIEW_CALLERS; foreach my $entry (@$entries) { my @rows = $view == CALLGRAPH_VIEW_CALLERS ? _callgraphCallerCells($entry, $base_dir, $span, $sort_type) : _callSiteCells($entry, $source, $base_dir, $span, $sort_type); # ..and, where its aliases are shown as groups, the summary rows of the # function as a whole come first, as in the function table. # A function is on a page only if it has a caller, a call site or a # call out of the report there, each of which is a row in the # summary block. None if there is no summary block - either # because the option is disabled or because this function has only # one group - see 'show_callgraph_summary' in lcovrc(5) my $own = defined($entry->{aliasGroups}) ? grep({ $_->{summary} } @rows) : scalar(@rows); push(@blocks, \@rows); push(@owns, $own); push(@heights, scalar(@rows)); my $anchor = _calleeFuncAnchor($entry->{name}); push(@blockAnchors, $anchor); my $annot = $entry->{annot}; my $colour = _callgraphUnhitTla(\@rows); push(@colours, $colour); # a group is the same author or the same date bin *and* the same colour: # contiguous functions which share both are one cell, and the cell # leads to the next one which shares both again. The colour is what # the reader following one author is scanning for, so a change of it # is a new group even where the name is the same my $shade = defined($colour) ? $colour : ''; push(@dateKeys, defined($annot) ? $annot->{bin} . "\0" . $shade : undef); push(@ownerKeys, defined($annot) ? $annot->{owner} . "\0" . $shade : undef); my $landing = $anchor; for (my $r = 0; $r < scalar(@rows); ++$r) { my $row = $rows[$r]; push(@tlaKeys, $row->{tla}); push(@countKeys, _callgraphHitKey($row)); # ..the row itself where it has an id and its block where it has # none - the alias group it is in, or else its function: a link # has to land somewhere, and the block is the most likely # place the user would look next $landing = _aliasGroupAnchor($row->{group}->[0]) if defined($row->{group}); my $at = defined($row->{id}) ? $row->{id} : $landing; push(@anchors, $at); # ..and the header counts each unique call site once: the rows of # a group repeat those of the summary block above them. The # 'next' hyperlink chains above still tour every row. Where # there is no summary block, use the (single) alias group # of the function - see _countedCallSites next unless defined($row->{tla}) && ($r < $own || 0 == $own); if ($view == CALLGRAPH_VIEW_CALLERS) { $summary->countCallgraphDestination($row->{tla}, $entry->{destAnnot}); $siteIndex->add($row->{tla}, $entry->{destAnnot}, $at); } else { $siteIndex->add($row->{tla}, $row->{site}, $at); } } } my @dateRuns = _callgraphMergedRuns(\@dateKeys, \@heights); my @ownerRuns = _callgraphMergedRuns(\@ownerKeys, \@heights); my $tlaSpans = _callgraphRowRuns(\@tlaKeys); write_header(*HTML_HANDLE, 'file', # as the function table is, and for the same reason [$page->{hdr}, 'name', $sort_type], File::Spec->catfile($trunc_dir, $base_name), File::Spec->catfile($rel_dir, $base_name), $summary, $siteIndex, $differentialMap, $pages); # Tell the user that alias groups exist for this page but have been # suppressed and how to get them back my $suppressed = ''; if ($main::suppress_alias_groups && grep({ defined($_->{aliasGroups}) } @$entries)) { my $width = () = $columns =~ /" . "$ALIAS_GROUPS_SUPPRESSED_NOTE\n"; } write_html(*HTML_HANDLE, < $suppressed $columns END_OF_HTML my $alternate = 0; my $index = 0; # of the whole page, which is what the category chain is for (my $block = 0; $block < scalar(@blocks); ++$block) { my $entry = $entries->[$block]; my $rows = $blocks[$block]; my $metric = $entry->{metric}->[$view]; my $height = scalar(@$rows); my $lead = ''; $lead = _callgraphAnnotCells($entries, $block, $colours[$block], [\@dateRuns, \@ownerRuns], \@blockAnchors) if $showAnnot; # ..and the way to the other direction, where the function is on that page my $crossPage = $entry->{has}->[1 - $view] ? callgraphPageName($base_name, 1 - $view, callgraphLinkSort($pages, 1 - $view, $sort_type)) : undef; # Where its aliases are shown as groups, the function's summary block # describes what the function as a whole did (same as the function # detail table): its cells span that block alone. # If there is no summary block, there are none: the # cells of each group describe that group my $own = $owns[$block]; if ($own) { $lead .= _calleeXrefCell(defined($crossPage) ? "$crossPage#" . _calleeFuncAnchor($entry->{name}) : undef, $other, $entry->{metric}->[1 - $view], $own, $alternate) if $showXref; $lead .= _calleeFuncCell($entry, $source, $view, $own, $alternate); $lead .= _callgraphHitCell($metric, $own); } my $firstGroup = 1; for (my $r = 0; $r < scalar(@$rows); ++$r) { my $row = $rows->[$r]; my $html = ' {id}\"" if defined($row->{id}); # ..and the rule which separates one function from the next, the # other half of the alternating colour of the name cell # ..and the rows of a group are set off from those of the summary # block above them $html .= ' class="callgraphFirstRow"' if 0 == $r; $html .= ' class="callgraphGroupRow"' if $own && $r >= $own; $html .= ">\n"; $html .= $lead if 0 == $r; # every row below the summary block is in a group if (defined($row->{group})) { $html .= _aliasGroupLeadCells( $entry, $row->{group}, $view, $showXref ? [$other, $crossPage] : undef, $source, 0 == $own ? $alternate : undef, $firstGroup); $firstGroup = 0; } if (exists($row->{note})) { $html .= $row->{note}; } else { my $tla = $row->{tla}; my $tlaHref = _callgraphGroupHref(\@tlaKeys, $index, \@anchors); my $hitHref = _callgraphGroupHref(\@countKeys, $index, \@anchors); # one cell for a run of rows of one category, which is written # by the first of them and spans the rest my $tlaCell = _callgraphTlaCell($showTlas, $tla, $tlaHref, $tlaSpans->[$index]); $html .= $row->{head} . _callgraphCellLine($tlaCell); # ..and the count, which says what the category column would have # said where there is no category column. A row with no count # of its own carries its cell ready made: there is no category # on such a row either, so there is nothing to fold into it. # The count leads past the run of calls its own call is in to # the next call which went the same way - see _callgraphHitKey $html .= exists($row->{count}) ? _callgraphCellLine(_callgraphCountCell( $row->{count}, _callgraphRowStyle($row->{count}, $tla), $foldTlas ? $tla : undef, $hitHref )) : (exists($row->{external}) ? _callgraphCellLine( _callgraphExternalCountCell($row->{external}, $hasTlas) ) : $row->{tail}); } ++$index; write_html(*HTML_HANDLE, $html . " \n"); } $alternate = !$alternate; } write_html(*HTML_HANDLE, <
END_OF_HTML write_html_epilog(*HTML_HANDLE, $base_dir, 1); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); } # # process_file(file_summary, parent_dir_summary, trunc_dir, rel_dir, filename) # sub process_file($$$$$) { my ($fileSummary, $parent_dir_summary, $trunc_dir, $rel_dir, $filename) = @_; my $trunc_name = apply_prefix($filename, @dir_prefix); info("Processing file $trunc_name" . (($main::diff_filename && $diff_data->containsFile($filename)) ? ' (source code changed)' : '')); # the name of every page written for this file starts with this, so it is # the last element of the name in this platform's spelling - not of the # name as read, which may hold the other platform's separator and would # then be one long element. See 'lcovutil::native_path' my $base_name = basename(lcovutil::native_path($filename)); my $base_dir = get_relative_base_path($rel_dir); my @source; my $pagetitle; local *HTML_HANDLE; my $fileData = $current_data->data($filename); # TraceInfo struct my ($testdata, $sumcount, $funcdata, $checkdata, $testfncdata, $testbrdata, $sumbrcount, $mcdc_summary, $testcase_mcdc) = $fileData->get_info(); my ($lines_found, $lines_hit, $br_found, $br_hit, $mcdc_found, $mcdc_hit, $fn_found, $fn_hit) = ($fileData->found(), $fileData->hit(), $fileData->branch_found(), $fileData->branch_hit(), $fileData->mcdc_found(), $fileData->mcdc_hit(), $fileData->function_found(), $fileData->function_hit()); # the caller-side view - the sites contained in this file's functions. Not # asked for unless the metric is on: it walks every function's sites my ($callgraph_found, $callgraph_hit) = (0, 0); ($callgraph_found, $callgraph_hit) = ($fileData->callgraph_found(), $fileData->callgraph_hit()) if $lcovutil::callgraph_coverage; $fileSummary->lines_found($lines_found); $fileSummary->lines_hit($lines_hit); $fileSummary->function_found($fn_found); $fileSummary->function_hit($fn_hit); $fileSummary->branch_found($br_found); $fileSummary->branch_hit($br_hit); $fileSummary->mcdc_found($mcdc_found); $fileSummary->mcdc_hit($mcdc_hit); $fileSummary->callgraph_found($callgraph_found); $fileSummary->callgraph_hit($callgraph_hit); # handle case that file was moved between baseline and current my $baseline_filename = $diff_data->baseline_file_name($filename); # when looking up the baseline file, handle the case that the # pathname does not match exactly - see comment in TraceFile::data my $fileBase; $fileBase = $base_data->data($baseline_filename, 1) if defined($base_data); my $fileCurrent = $current_data->data($filename); # build coverage differential categories my $now = Time::HiRes::gettimeofday(); my $fileCovInfo = FileCoverageInfo->new($filename, $fileBase, $fileCurrent, $diff_data, defined($main::verboseScopeRegexp) && $filename =~ m/$main::verboseScopeRegexp/); my $then = Time::HiRes::gettimeofday(); my $elapsed = $then - $now; $lcovutil::profileData{categorize}{$filename} = $elapsed; info(sprintf(" (%0.3fs)\n", $elapsed)); my $fileHasNoBaselineInfo = $main::treatNewFileAsBaseline && (!defined($fileBase) || ($fileBase->is_empty() && @main::base_filenames)); # if this file is older than the baseline and there is no associated # baseline data - then it appears to have been added to the build # recently # We want to treat the code as "CBC" or "UBC" (not "GIC" and "UIC") # because we only just turned section "on" - and we don't want the # coverage ratchet to fail the build if UIC is nonzero # NOTE: SourceFile constructor modifies some input data: # - $fileSummary struct is also modified: update total counts in # each bucket, counts in each date range # - $fileCovInfo: change GIC->CBC, UIC->UBC if $fileHasNoBaselineInfo # and source code is older than baseline file my $srcfile = SourceFile->new($filename, $fileSummary, $fileCovInfo, $fileHasNoBaselineInfo); my $endSrc = Time::HiRes::gettimeofday(); $lcovutil::profileData{source}{$filename} = $endSrc - $then; my $differentialFunctionMap; if ($lcovutil::func_coverage) { $differentialFunctionMap = $fileCovInfo->functionMap(); if ($SummaryInfo::selectCallback) { # prune hash to remove functions which are outside the # selected region my $lineCovMap = $fileCovInfo->lineMap(); my %tempMap; while (my ($f, $fn) = each(%$differentialFunctionMap)) { my $line = $fn->line(); $tempMap{$f} = $fn if exists($lineCovMap->{$line}); } $differentialFunctionMap = \%tempMap; } } # ..and which of its functions hold callgraph data - both what the call # site pages hold and which of them there are to hyperlink # to: a file whose functions all make calls and are all called from # outside it has both pages, one whose functions make no calls at all - # a leaf module - has only callee data my @callgraphDetail; my $callgraphPages; # ..and which of them, function by function - which is what the two # callgraph columns of the function table link to, and leave empty where # the function is not on that page. Each entry is that function's # proportion of the direction, or undef where it is not on that page: # the columns are coloured by it, exactly as they are on the pages they # lead to. The element after the directions is the same of each alias # group, where they are shown - see _showAliasGroups my $callgraphXref; if ($lcovutil::callgraph_coverage && defined($differentialFunctionMap) && !$srcfile->is_empty()) { @callgraphDetail = callgraphDetailEntries($funcdata, $differentialFunctionMap, $srcfile); $callgraphPages = [ map({ callgraphPageSorts(\@callgraphDetail, $_) } 0 .. CALLGRAPH_VIEW_COUNT - 1) ]; $callgraphXref = { map({ my $e = $_; ($e->{name} => [ map({ $e->{has}->[$_] ? $e->{metric}->[$_] : undef } 0 .. CALLGRAPH_VIEW_COUNT - 1), $e->{groupMetric} ]); } @callgraphDetail) }; } # ..and the callgraph numbers of the file are computed from the rows shown # on the page - see SummaryInfo::recountCallgraphSites - whether or not # the pages are written (so that the numbers do not depend on it) $fileSummary->recountCallgraphSites( [map({ _countedCallSites($_, $srcfile) } @callgraphDetail)]) if $lcovutil::callgraph_coverage && defined($differentialFunctionMap) && !$srcfile->is_empty(); ($callgraph_found, $callgraph_hit) = ($fileSummary->callgraph_found(), $fileSummary->callgraph_hit()); my $r = " lines=$lines_found hit=$lines_hit"; $r .= " functions=$fn_found hit=$fn_hit" if $lcovutil::func_coverage && $fn_found != 0; $r .= " branches=$br_found hit=$br_hit" if $lcovutil::br_coverage && $br_found != 0; $r .= " MC/DC=$mcdc_found hit=$mcdc_hit" if $lcovutil::mcdc_coverage && $mcdc_found != 0; $r .= " callgraph=$callgraph_found hit=$callgraph_hit" if $lcovutil::callgraph_coverage && $callgraph_found != 0; info($r . "\n"); if (@main::base_filenames) { # summarize the bin data if we had baseline data for comparison foreach my $t (['line', $lines_found], ['branch', $lcovutil::br_coverage ? $br_found : 0], ['mcdc', $lcovutil::mcdc_coverage ? $mcdc_found : 0], ['function', $lcovutil::func_coverage ? $fn_found : 0], ['callgraph', $lcovutil::callgraph_coverage ? $callgraph_found : 0 ] ) { next if 0 == $t->[1]; info(' ' . $t->[0] . ': ' . $fileSummary->tlaSummary($t->[0]) . "\n"); } } if ($srcfile->is_empty()) { return; } # somewhat of a hack: we are ultimately going to merge $fileSummary # (the data for this particular file) into $parent_dir (the data # for the parent directory) - but we need to do that in the caller # (because we are building $fileSummary in a child process that we are # going to pass back. But we also use the parent and its name # in HTML generation... # we clear this setting before we return the generated summary $fileSummary->setParent($parent_dir_summary); my $from = Time::HiRes::gettimeofday(); # Return after this point in case user asked us not to generate # source code view if (!$no_sourceview) { # Generate source code view for this file # ..and, where the filter leaves part of the file out, which part: what # a link into the pages of this file can lead to. # Lines are added as the source view writes them $main::pageShown = $SummaryInfo::selectCallback ? callgraphShownAnchors($srcfile->path(), \@callgraphDetail) : undef; html_create(*HTML_HANDLE, File::Spec->catfile($rel_dir, "$base_name.gcov.$html_ext")); $pagetitle = "LCOV - $test_title - " . File::Spec->catfile($trunc_dir, $base_name); write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle); write_header(*HTML_HANDLE, 'file', [$HDR_SOURCE, 'name', 0], File::Spec->catfile($trunc_dir, $base_name), File::Spec->catfile($rel_dir, $base_name), $fileSummary, $srcfile, $differentialFunctionMap, $callgraphPages); @source = write_source(*HTML_HANDLE, $srcfile, $sumcount, $checkdata, $fileCovInfo, $funcdata, $sumbrcount, $mcdc_summary, $base_dir); write_html_epilog(*HTML_HANDLE, $base_dir, 1); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); if ($lcovutil::func_coverage) { # Create function tables my $lineCovMap = $fileCovInfo->lineMap(); # simply map between function leader name and differential data # compute number of hit/missed coverpoints in each function my %lineCov; my %branchCov; my %mcdcCov; my %callgraphCov; if ($main::show_functionProportions) { while (my ($name, $funcEntry) = each(%$differentialFunctionMap)) { # ..and where the first coverpoint of each kind is, which is # where the proportion in the function table leads: a # reader who sees that something in there was never # exercised wants the code, and the top of the function is # not it when the function is longer than the screen my $lineData = $funcEntry->findMyLines($sumcount); if (defined($lineData)) { my $found = 0; my $hit = 0; foreach my $d (@$lineData) { ++$found; ++$hit if (0 != $d->[1]); } $lineCov{$name} = [ $found, $hit, scalar(@$lineData) ? $lineData->[0]->[0] : undef ]; } if ($lcovutil::br_coverage) { my $branchData = $funcEntry->findMyBranches($sumbrcount); if (defined($branchData) && 0 != scalar(@$branchData)) { # there are branches here.. my $found = 0; my $hit = 0; foreach my $branch (@$branchData) { my ($f, $h) = $branch->totals(); $found += $f; $hit += $h; } $branchCov{$name} = [$found, $hit, $branchData->[0]->line()]; } } if ($lcovutil::mcdc_coverage) { my $mcdcData = $funcEntry->findMyMcdc($mcdc_summary); if (defined($mcdcData) && 0 != scalar(@$mcdcData)) { # there are MC/DC expressions here.. my $found = 0; my $hit = 0; foreach my $mcdc (@$mcdcData) { my ($f, $h) = $mcdc->totals(); $found += $f; $hit += $h; } $mcdcCov{$name} = [$found, $hit, $mcdcData->[0]->line()]; } } if ($lcovutil::callgraph_coverage) { # the sites this function contains - the 'per file' view # of the metric, and the one which is a proportion of # something local. Computed from its callees page # - see SummaryInfo::recountCallgraphSites my $xref = $callgraphXref->{$name}; my $metric = defined($xref) ? $xref->[CALLGRAPH_VIEW_CALLEES] : undef; $callgraphCov{$name} = $metric if defined($metric) && 0 != $metric->[0]; } } } if (%$differentialFunctionMap) { my $funcStart = Time::HiRes::gettimeofday(); foreach my $sort_type (@funcview_sortlist) { write_function_page([$differentialFunctionMap, \%lineCov, \%branchCov, \%mcdcCov, \%callgraphCov, $callgraphPages, $callgraphXref, ], $base_dir, $rel_dir, $trunc_dir, $base_name, $test_title, $sumcount, $funcdata, $testfncdata, $sumbrcount, $testbrdata, $mcdc_summary, $testcase_mcdc, $sort_type, $fileSummary); } my $funcEnd = Time::HiRes::gettimeofday(); my $elapsed = $funcEnd - $funcStart; $lcovutil::profileData{function_html}{$filename} = $elapsed; #info(sprintf(" function HTML %0.3fs\n", $elapsed)); # ..and the call site detail of those functions: a page per # direction, each of them in every sort order the report offers - # the two directions are two different proportions, so neither # page can be an ordering of the other if (defined($callgraphPages) && grep({ $_ } @$callgraphPages)) { my $callgraphStart = Time::HiRes::gettimeofday(); for (my $view = 0; $view < CALLGRAPH_VIEW_COUNT; ++$view) { next unless $callgraphPages->[$view]; foreach my $sort_type (@{$callgraphPages->[$view]}) { my @entries = callgraphViewEntries(\@callgraphDetail, $view, $sort_type); write_callgraph_page( $view, $sort_type, \@entries, $callgraphPages, $differentialFunctionMap, $base_dir, $rel_dir, $trunc_dir, $base_name, $test_title, $fileSummary); } } $lcovutil::profileData{callgraph_html}{$filename} = Time::HiRes::gettimeofday() - $callgraphStart; } } } # Additional files are needed in case of frame output if ($frames) { # The frameset, the overview page and the overview image refer to # each other and to the source page by name, and 'html_create' # lower-cases the report-relative name it writes under # 'case_insensitive' - so fold the base name once and spell # all four of them from the result. my $page_base = $lcovutil::case_insensitive ? lc($base_name) : $base_name; # Create overview png file my $simplified = defined($main::show_simplifiedColors) && $main::show_simplifiedColors; my $png = File::Spec->catfile($rel_dir, "$page_base.gcov.png"); gen_png(File::Spec->catfile($output_directory, $png), $main::show_tla && !$simplified, $main::dark_mode, $overview_width, $tab_size, @source); # Create frameset page html_create(*HTML_HANDLE, File::Spec->catfile( $rel_dir, "$page_base.gcov.frameset.$html_ext" )); write_frameset(*HTML_HANDLE, $base_dir, $page_base, $pagetitle); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); # Write overview frame html_create(*HTML_HANDLE, File::Spec->catfile( $rel_dir, "$page_base.gcov.overview.$html_ext" )); write_overview(*HTML_HANDLE, $base_dir, $page_base, $pagetitle, scalar(@source)); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); } } my $to = Time::HiRes::gettimeofday(); $elapsed = $to - $from; $lcovutil::profileData{html}{$filename} = $elapsed; #info(sprintf(" HTML %0.3fs\n", $elapsed)); return ($testdata, $testfncdata, $testbrdata, $testcase_mcdc); } sub compute_title($$) { my ($patterns, $info_files) = @_; my $title; if (1 == scalar(@$info_files)) { # just one coverage DB file $title = basename($info_files->[0]); } elsif (1 == scalar(@$patterns)) { # just one pattern... $title = $patterns->[0]; $title = substr($title, length($main::cwd) + 1) if (File::Spec->file_name_is_absolute($title) && length($main::cwd) < length($title) && $main::cwd eq substr($title, 0, length($main::cwd))); } else { $title = scalar(@$info_files) . ' coverage DB files'; } return $title; } # # get_prefix(min_dir, filename_list) # # Search FILENAME_LIST for a directory prefix which is common to as many # list entries as possible, so that removing this prefix will minimize the # sum of the lengths of all resulting shortened filenames while observing # that no filename has less than MIN_DIR parent directories. # sub get_prefix($@) { my ($min_dir, @filename_list) = @_; my %prefix; # mapping: prefix -> sum of lengths my $current; # Temporary iteration variable # Find list of prefixes my @munged; foreach (@filename_list) { # Need explicit assignment to get a copy of $_ so that # shortening the contained prefix does not affect the list # the prefix is a directory to remove from an output path, so this is # the same name 'MergeManager::new' derives the layout from my $current = lcovutil::native_path(ReadCurrentSource::resolve_path($_)); my ($vol, $parentDir, $file) = File::Spec->splitpath($current); if (!File::Spec->file_name_is_absolute($current)) { if ($parentDir) { $parentDir = File::Spec->catfile($main::cwd, $parentDir); } else { $parentDir = $main::cwd; } $current = File::Spec->catfile($parentDir, $file); } push(@munged, $current); while ($current = shorten_prefix($current)) { # Skip rest if the remaining prefix has already been # added to hash if (exists($prefix{$current})) { last; } # Initialize with 0 $prefix{$current} = "0"; } } # Remove all prefixes that would cause filenames to have less than # the minimum number of parent directories foreach my $filename (@munged) { my $dir = dirname($filename); for (my $i = 0; $i < $min_dir; $i++) { delete($prefix{$dir}); $dir = shorten_prefix($dir); } } # Check if any prefix remains return undef if (!%prefix); # Calculate sum of lengths for all prefixes foreach $current (keys(%prefix)) { foreach (@munged) { # Add original length $prefix{$current} += length($_); # Check whether prefix matches if (substr($_, 0, length($current)) eq $current) { # Subtract prefix length for this filename $prefix{$current} -= length($current); } } } # Find and return prefix with minimal sum $current = (keys(%prefix))[0]; foreach (keys(%prefix)) { if ($prefix{$_} < $prefix{$current}) { $current = $_; } } return ($current); } # # shorten_prefix(prefix) # # Return PREFIX shortened by last directory component. # sub shorten_prefix($) { my ($vol, $dir, $name) = File::Spec->splitpath($_[0]); return File::Spec->catdir($vol, $dir); } # # get_relative_base_path(subdirectory) # # Return a relative path string which references the base path when applied # in SUBDIRECTORY. # # Example: get_relative_base_path("fs/mm") -> "../../" # sub get_relative_base_path($) { # Make an empty directory path a special case. A length test, not a truth # test: a directory named '0' is a false string, and returning '' for it # leaves every relative reference on the pages below it - the stylesheet, # the icons, the top level index - pointing into the wrong directory return '' unless defined($_[0]) && length($_[0]); # Count number of /s in path my $index = ($_[0] =~ s/$lcovutil::dirseparator_re/$lcovutil::dirseparator/g); # Add a ../ to $result for each / in the directory path + 1 my $result = ""; for (; $index >= 0; $index--) { $result .= "..$lcovutil::dirseparator"; } return $result; } # # read_testfile(test_filename) # # Read in file TEST_FILENAME which contains test descriptions in the format: # # TN: # TD: # # for each test case. Return a reference to a hash containing a mapping # # test name -> test description. # # Die on error. # sub read_testfile($) { my $file = shift; my %result; my $test_name; my $changed_testname; local *TEST_HANDLE; open(TEST_HANDLE, "<", $file) or die("cannot open description file '$file': $!\n"); while () { chomp($_); s/\r//g; # Match lines beginning with TN: next if /^#/; # skip comment if (/^TN:\s*(.*?)\s*$/) { # Store name for later use $test_name = $1; if ($test_name =~ s/\W/_/g) { $changed_testname = 1; } } # Match lines beginning with TD: if (/^TD:\s*(.*?)\s*$/) { if (!defined($test_name)) { lcovutil::ignorable_error($lcovutil::ERROR_FORMAT, "\"$file\":$.: Found test description without prior test name." ); next; } # Check for empty line - by length, not by truth: 'TD: 0' is a # description whose text happens to be a false string, and reading # it as blank both loses the text and splits the description it is # in the middle of into two paragraphs if (length($1)) { # Add description to hash $result{$test_name} .= " $1"; } else { # Add empty line $result{$test_name} .= "\n\n"; } } } close(TEST_HANDLE) or die("unable to close HTML file: $!\n"); if (!%result) { lcovutil::ignorable_error($lcovutil::ERROR_EMPTY, "no test descriptions found in '$file'."); } if ($changed_testname) { lcovutil::ignorable_error($lcovutil::ERROR_FORMAT, "invalid characters removed from testname in descriptions file '$file'." ); } return \%result; } # # escape_html(STRING) # # Return a copy of STRING in which all occurrences of HTML special characters # are escaped. # # The HTML escape map and the tab-expansion algorithm live in lcovutil so that # html2lcov (which reverses this rendering) shares one definition and cannot # drift. escape_html() composes them and adds the newline ->
step. sub escape_html($) { my $string = $_[0]; # ..and the same here: callers do pass undef, which is why the guard exists, # but the string '0' is a source line, a path component or an owner name # which has to survive - it is false, not empty return '' unless defined($string) && length($string); # Escape all four HTML special characters in a single pass. Using a # character-class match with a lookup table both avoids re-scanning the # string four times and side-steps the double-escaping ordering hazard # (the old code relied on '&' being substituted first). $string =~ s/([&<>"])/$lcovutil::html_escape_map{$1}/g; $string = lcovutil::expand_tabs($string, $tab_size); $string =~ s/\n/
/g; # \n ->
return $string; } # # get_date_string() # # Return the current date in the form: yyyy-mm-dd # sub get_date_string($) { my $time = $_[0]; my @timeresult; if (!defined($time)) { if (defined $ENV{'SOURCE_DATE_EPOCH'}) { @timeresult = gmtime($ENV{'SOURCE_DATE_EPOCH'}); } else { @timeresult = localtime(); } } else { @timeresult = localtime($time); } my ($year, $month, $day, $hour, $min, $sec) = @timeresult[5, 4, 3, 2, 1, 0]; return sprintf("%d-%02d-%02d %02d:%02d:%02d", $year + 1900, $month + 1, $day, $hour, $min, $sec); } # # write_description_file(descriptions, overall_found, overall_hit, # total_fn_found, total_fn_hit, total_br_found, # total_br_hit) # # Write HTML file containing all test case descriptions. DESCRIPTIONS is a # reference to a hash containing a mapping # # test case name -> test case description # # Die on error. # sub write_description_file($$) { my %description = %{$_[0]}; my $summary = $_[1]; my $test_name; local *HTML_HANDLE; html_create(*HTML_HANDLE, "descriptions.$html_ext"); write_html_prolog(*HTML_HANDLE, "", "LCOV - test case descriptions"); write_header(*HTML_HANDLE, 'top', [3, 'name', 0], "", "", $summary, undef, undef); write_test_table_prolog(*HTML_HANDLE, "Test case descriptions - alphabetical list"); foreach $test_name (sort(keys(%description))) { my $desc = $description{$test_name}; $desc = escape_html($desc) if (!$rc_desc_html); write_test_table_entry(*HTML_HANDLE, $test_name, $desc); } write_test_table_epilog(*HTML_HANDLE); write_html_epilog(*HTML_HANDLE, ""); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); } # # write_png_files() # # Create all necessary .png files for the HTML-output in the current # directory. .png-files are used as bar graphs. # # Die on error. # sub write_png_files() { my %data; local *PNG_HANDLE; $data{"ruby.png"} = $dark_mode ? [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x80, 0x1b, 0x18, 0x00, 0x00, 0x00, 0x39, 0x4a, 0x74, 0xf4, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ] : [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x18, 0x10, 0x5d, 0x57, 0x34, 0x6e, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0x35, 0x2f, 0x00, 0x00, 0x00, 0xd0, 0x33, 0x9a, 0x9d, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ]; $data{"amber.png"} = $dark_mode ? [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x99, 0x86, 0x30, 0x00, 0x00, 0x00, 0x51, 0x83, 0x43, 0xd7, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ] : [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x28, 0x04, 0x98, 0xcb, 0xd6, 0xe0, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xe0, 0x50, 0x00, 0x00, 0x00, 0xa2, 0x7a, 0xda, 0x7e, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ]; $data{"emerald.png"} = $dark_mode ? [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x00, 0x66, 0x00, 0x0a, 0x0a, 0x0a, 0xa4, 0xb8, 0xbf, 0x60, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ] : [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x22, 0x2b, 0xc9, 0xf5, 0x03, 0x33, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x1b, 0xea, 0x59, 0x0a, 0x0a, 0x0a, 0x0f, 0xba, 0x50, 0x83, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ]; $data{"snow.png"} = $dark_mode ? [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xdd, 0xdd, 0xdd, 0x00, 0x00, 0x00, 0xae, 0x9c, 0x6c, 0x92, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ] : [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x1e, 0x1d, 0x75, 0xbc, 0xef, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x55, 0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ]; $data{"glass.png"} = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x55, 0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4e, 0x53, 0x00, 0x40, 0xe6, 0xd8, 0x66, 0x00, 0x00, 0x00, 0x01, 0x62, 0x4b, 0x47, 0x44, 0x00, 0x88, 0x05, 0x1d, 0x48, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xd2, 0x07, 0x13, 0x0f, 0x08, 0x19, 0xc4, 0x40, 0x56, 0x10, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x48, 0xaf, 0xa4, 0x71, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ]; if ($sort_tables) { $data{"updown.png"} = $dark_mode ? [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0e, 0x08, 0x06, 0x00, 0x00, 0x00, 0x16, 0xa3, 0x8d, 0xab, 0x00, 0x00, 0x00, 0x43, 0x49, 0x44, 0x41, 0x54, 0x28, 0xcf, 0x63, 0x60, 0x40, 0x03, 0x77, 0xef, 0xde, 0xfd, 0x7f, 0xf7, 0xee, 0xdd, 0xff, 0xe8, 0xe2, 0x8c, 0xe8, 0x8a, 0x90, 0xf9, 0xca, 0xca, 0xca, 0x8c, 0x18, 0x0a, 0xb1, 0x99, 0x82, 0xac, 0x98, 0x11, 0x9f, 0x22, 0x64, 0xc5, 0x8c, 0x84, 0x14, 0xc1, 0x00, 0x13, 0xc3, 0x80, 0x01, 0xea, 0xbb, 0x91, 0xf8, 0xe0, 0x21, 0x29, 0xc0, 0x89, 0x89, 0x42, 0x06, 0x62, 0x13, 0x05, 0x00, 0xe1, 0xd3, 0x2d, 0x91, 0x93, 0x15, 0xa4, 0xb2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ] : [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x0e, 0x08, 0x06, 0x00, 0x00, 0x00, 0x16, 0xa3, 0x8d, 0xab, 0x00, 0x00, 0x00, 0x3c, 0x49, 0x44, 0x41, 0x54, 0x28, 0xcf, 0x63, 0x60, 0x40, 0x03, 0xff, 0xa1, 0x00, 0x5d, 0x9c, 0x11, 0x5d, 0x11, 0x8a, 0x24, 0x23, 0x23, 0x23, 0x86, 0x42, 0x6c, 0xa6, 0x20, 0x2b, 0x66, 0xc4, 0xa7, 0x08, 0x59, 0x31, 0x23, 0x21, 0x45, 0x30, 0xc0, 0xc4, 0x30, 0x60, 0x80, 0xfa, 0x6e, 0x24, 0x3e, 0x78, 0x48, 0x0a, 0x70, 0x62, 0xa2, 0x90, 0x81, 0xd8, 0x44, 0x01, 0x00, 0xe9, 0x5c, 0x2f, 0xf5, 0xe2, 0x9d, 0x0f, 0xf9, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 ]; } foreach (keys(%data)) { my $f = File::Spec->catfile($main::output_directory, $_); open(PNG_HANDLE, ">", $f) or die("cannot create $f: $!\n"); binmode(PNG_HANDLE); print(PNG_HANDLE map(chr, @{$data{$_}})); close(PNG_HANDLE) or die("unable to close PNG handle: $!\n"); } } # # write_htaccess_file() # sub write_htaccess_file() { local *HTACCESS_HANDLE; my $htaccess_data; my $f = File::Spec->catdir($main::output_directory, '.htaccess'); open(*HTACCESS_HANDLE, ">", $f) or die("cannot open $f for writing: $!\n"); $htaccess_data = (<<"END_OF_HTACCESS") AddEncoding x-gzip .html END_OF_HTACCESS ; print(HTACCESS_HANDLE $htaccess_data); close(*HTACCESS_HANDLE) or die("unable to close .htaccess: $!\n"); } # # write_css_file() # # Write the cascading style sheet file gcov.css to the current directory. # This file defines basic layout attributes of all generated HTML pages. # sub write_css_file() { local *CSS_HANDLE; my $f = File::Spec->catdir($main::output_directory, 'gcov.css'); # Check for a specified external style sheet file if ($css_filename) { # Simply copy that file # 'File::Copy::copy' rather than 'system("cp", ...)': # 'copy' sets '$!', and this also takes a 'cp' out of the # Windows/Cygwin path copy($css_filename, $f) or die("cannot copy file $css_filename to $f: $!\n"); return; } open(CSS_HANDLE, ">", $f) or die("cannot open $f for writing: $!\n"); # ************************************************************* # ************************************************************* my $ownerBackground = "#COLOR_17"; # very light pale grey/blue my $ownerCovHi = "#COLOR_18"; # light green my $ownerCovMed = "#COLOR_19"; # light yellow my $ownerCovLo = "#COLOR_20"; # lighter red # use same background color as file entry unless in hierarchical report my $directoryBackground = $main::hierarchical ? '#COLOR_18' : '#COLOR_06'; my $css_data = ($_ = <<"END_OF_CSS") /* All views: initial background and text color */ body { color: #COLOR_00; background-color: #COLOR_14; } /* All views: standard link format*/ a:link { color: #COLOR_15; text-decoration: underline; } /* All views: standard link - visited format */ a:visited { color: #COLOR_01; text-decoration: underline; } /* All views: standard link - activated format */ a:active { color: #COLOR_11; text-decoration: underline; } /* All views: main title format */ td.title { text-align: center; padding-bottom: 10px; font-family: sans-serif; font-size: 20pt; font-style: italic; font-weight: bold; } /* table footnote */ td.footnote { text-align: left; padding-left: 100px; padding-right: 10px; background-color: #COLOR_08; /* light blue table background color */ /* dark blue table header color background-color: #COLOR_03; */ white-space: nowrap; font-family: sans-serif; font-style: italic; font-size:70%; } /* "Line coverage date bins" leader */ td.subTableHeader { text-align: center; padding-bottom: 6px; font-family: sans-serif; font-weight: bold; vertical-align: center; } /* All views: header item format */ td.headerItem { text-align: right; padding-right: 6px; font-family: sans-serif; font-weight: bold; vertical-align: top; white-space: nowrap; } /* All views: header item value format */ td.headerValue { text-align: left; color: #COLOR_15; font-family: sans-serif; font-weight: bold; white-space: nowrap; } /* All views: header item coverage table heading */ td.headerCovTableHead { text-align: center; padding-right: 6px; padding-left: 6px; padding-bottom: 0px; font-family: sans-serif; white-space: nowrap; } /* All views: header item coverage table entry */ td.headerCovTableEntry { text-align: right; color: #COLOR_15; font-family: sans-serif; font-weight: bold; white-space: nowrap; padding-left: 12px; padding-right: 4px; background-color: #COLOR_08; } /* All views: header item coverage table entry for high coverage rate */ td.headerCovTableEntryHi { text-align: right; color: #COLOR_00; font-family: sans-serif; font-weight: bold; white-space: nowrap; padding-left: 12px; padding-right: 4px; background-color: #COLOR_04; } /* All views: header item coverage table entry for medium coverage rate */ td.headerCovTableEntryMed { text-align: right; color: #COLOR_00; font-family: sans-serif; font-weight: bold; white-space: nowrap; padding-left: 12px; padding-right: 4px; background-color: #COLOR_13; } /* All views: header item coverage table entry for low coverage rate */ td.headerCovTableEntryLo { text-align: right; color: #COLOR_00; font-family: sans-serif; font-weight: bold; white-space: nowrap; padding-left: 12px; padding-right: 4px; background-color: #COLOR_10; } /* All views: header legend value for legend entry */ td.headerValueLeg { text-align: left; color: #COLOR_00; font-family: sans-serif; font-size: 80%; white-space: nowrap; padding-top: 4px; } /* All views: color of horizontal ruler */ td.ruler { background-color: #COLOR_03; } /* All views: version string format */ td.versionInfo { text-align: center; padding-top: 2px; font-family: sans-serif; font-style: italic; } /* Directory view/File view (all)/Test case descriptions: table headline format */ td.tableHead { text-align: center; color: #COLOR_14; background-color: #COLOR_03; font-family: sans-serif; font-size: 120%; font-weight: bold; white-space: nowrap; padding-left: 4px; padding-right: 4px; } span.tableHeadSort { padding-right: 4px; } /* Directory view/File view (all): filename entry format */ td.coverFile { text-align: left; padding-left: 10px; padding-right: 20px; color: #COLOR_15; background-color: #COLOR_08; font-family: monospace; } /* Directory view/File view (all): directory name entry format */ td.coverDirectory { text-align: left; padding-left: 10px; padding-right: 20px; color: #COLOR_15; background-color: $directoryBackground; font-family: monospace; } /* Directory view/File view (all): filename entry format */ td.overallOwner { text-align: center; font-weight: bold; font-family: sans-serif; background-color: #COLOR_08; padding-right: 10px; padding-left: 10px; } /* Directory view/File view (all): filename entry format */ td.ownerName { text-align: right; font-style: italic; font-family: sans-serif; background-color: $ownerBackground; padding-right: 10px; padding-left: 20px; } /* Directory view/File view (all): bar-graph entry format*/ td.coverBar { padding-left: 10px; padding-right: 10px; background-color: #COLOR_08; } /* Directory view/File view (all): bar-graph entry format*/ td.owner_coverBar { padding-left: 10px; padding-right: 10px; background-color: $ownerBackground; } /* Directory view/File view (all): bar-graph outline color */ td.coverBarOutline { background-color: #COLOR_00; } /* Directory view/File view (all): percentage entry for files with high coverage rate */ td.coverPerHi { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_04; font-weight: bold; font-family: sans-serif; } /* 'owner' entry: slightly lighter color than 'coverPerHi' */ td.owner_coverPerHi { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerCovHi; font-weight: bold; font-family: sans-serif; } /* Directory view/File view (all): line count entry */ td.coverNumDflt { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_08; white-space: nowrap; font-family: sans-serif; } /* td background color and font for the 'owner' section of the table */ td.ownerTla { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerBackground; white-space: nowrap; font-family: sans-serif; font-style: italic; } /* Directory view/File view (all): line count entry for files with high coverage rate */ td.coverNumHi { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_04; white-space: nowrap; font-family: sans-serif; } td.owner_coverNumHi { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerCovHi; white-space: nowrap; font-family: sans-serif; } /* Directory view/File view (all): percentage entry for files with medium coverage rate */ td.coverPerMed { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_13; font-weight: bold; font-family: sans-serif; } td.owner_coverPerMed { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerCovMed; font-weight: bold; font-family: sans-serif; } /* Directory view/File view (all): line count entry for files with medium coverage rate */ td.coverNumMed { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_13; white-space: nowrap; font-family: sans-serif; } td.owner_coverNumMed { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerCovMed; white-space: nowrap; font-family: sans-serif; } /* Directory view/File view (all): percentage entry for files with low coverage rate */ td.coverPerLo { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_10; font-weight: bold; font-family: sans-serif; } td.owner_coverPerLo { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerCovLo; font-weight: bold; font-family: sans-serif; } /* Directory view/File view (all): line count entry for files with low coverage rate */ td.coverNumLo { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_10; white-space: nowrap; font-family: sans-serif; } td.owner_coverNumLo { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerCovLo; white-space: nowrap; font-family: sans-serif; } /* File view (all): "show/hide details" link format */ a.detail:link { color: #COLOR_06; font-size:80%; } /* File view (all): "show/hide details" link - visited format */ a.detail:visited { color: #COLOR_06; font-size:80%; } /* File view (all): "show/hide details" link - activated format */ a.detail:active { color: #COLOR_14; font-size:80%; } /* File view (detail): test name entry */ td.testName { text-align: right; padding-right: 10px; background-color: #COLOR_08; font-family: sans-serif; } /* File view (detail): test percentage entry */ td.testPer { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_08; font-family: sans-serif; } /* File view (detail): test lines count entry */ td.testNum { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_08; font-family: sans-serif; } /* Test case descriptions: test name format*/ dt { font-family: sans-serif; font-weight: bold; } /* Test case descriptions: description table body */ td.testDescription { padding-top: 10px; padding-left: 30px; padding-bottom: 10px; padding-right: 30px; background-color: #COLOR_08; } /* Source code view: function entry */ td.coverFn { text-align: left; padding-left: 10px; padding-right: 20px; color: #COLOR_15; background-color: #COLOR_08; font-family: monospace; } /* Source code view: function entry zero count*/ td.coverFnLo { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_10; font-weight: bold; font-family: sans-serif; } /* Source code view: function entry nonzero count*/ td.coverFnHi { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_08; font-weight: bold; font-family: sans-serif; } td.coverFnAlias { text-align: right; padding-left: 10px; padding-right: 20px; color: #COLOR_15; /* make this a slightly different color than the leader - otherwise, otherwise the alias is hard to distinguish in the table */ background-color: #COLOR_17; /* very light pale grey/blue */ font-family: monospace; } /* Source code view: function entry zero count*/ td.coverFnAliasLo { text-align: right; padding-left: 10px; padding-right: 10px; background-color: $ownerCovLo; /* lighter red */ font-family: sans-serif; } /* Source code view: function entry nonzero count*/ td.coverFnAliasHi { text-align: right; padding-left: 10px; padding-right: 10px; background-color: #COLOR_08; font-weight: bold; font-family: sans-serif; } /* Call site page: the name cell of one function, which spans every row that function has. Two colours used in turn, so that the extent of one function is readable where its rows run on */ td.calleeFn { text-align: left; vertical-align: top; padding-left: 10px; padding-right: 20px; color: #COLOR_15; background-color: #COLOR_08; font-family: monospace; } td.calleeFnAlt { text-align: left; vertical-align: top; padding-left: 10px; padding-right: 20px; color: #COLOR_15; background-color: #COLOR_07; font-family: monospace; } /* Call site page: the date and owner cells of a run of functions, which span their rows as the name cell spans one function's. A statement about the functions rather than about a call, so they are not read as a call is - and centred at the top of the run, where the first of the functions it describes starts */ td.calleeAnnot { text-align: center; vertical-align: top; padding-left: 10px; padding-right: 10px; color: #COLOR_15; background-color: #COLOR_08; font-family: sans-serif; white-space: nowrap; } td.calleeAnnotAlt { text-align: center; vertical-align: top; padding-left: 10px; padding-right: 10px; color: #COLOR_15; background-color: #COLOR_07; font-family: sans-serif; white-space: nowrap; } /* Call site page: the category cell of a run of rows of one category, which is written at the top of the run it spans */ td.callgraphSpan { vertical-align: top; } /* Call site page: the other direction of the callgraph for one function, in a column of its own beside its name, and spanning its rows as the name cell does. These two are the cell where it is empty - where that function says nothing in the other direction, which is a fact the reader reads without a click; where it does lead somewhere it is a proportion, coloured as the 'Hit' column is */ td.calleeXref { text-align: left; vertical-align: top; padding-left: 10px; padding-right: 10px; color: #COLOR_15; background-color: #COLOR_08; font-family: sans-serif; white-space: nowrap; } td.calleeXrefAlt { text-align: left; vertical-align: top; padding-left: 10px; padding-right: 10px; color: #COLOR_15; background-color: #COLOR_07; font-family: sans-serif; white-space: nowrap; } /* Call site page: which side of the boundary of the report a function's callers are on - a statement about the data, so read as the captions are rather than as a name */ span.calleeFnNote { font-family: sans-serif; font-style: italic; font-size: smaller; } /* A name which other names were merged into: how many names it represents. A number rather than the asterisks the table notes use, and with the names themselves in the tooltip */ sup.mergedNames { font-family: sans-serif; font-style: italic; cursor: help; position: relative; } /* ..and its tooltip: the names. A table, so that the counts line up and the names start in one column and so that function name background colors can alternate (otherwise: very hard to read a long list of function names */ span.mergedNamesList { display: none; position: absolute; left: 0; top: 100%; z-index: 10; border: 1px solid #COLOR_15; font-family: monospace; font-style: normal; font-size: small; white-space: nowrap; text-align: left; color: #COLOR_15; background-color: #COLOR_08; } sup.mergedNames:hover > span.mergedNamesList { display: table; } span.mergedNamesHead { display: table-caption; padding: 2px 4px; font-family: sans-serif; font-style: italic; white-space: nowrap; color: #COLOR_15; background-color: #COLOR_08; border: 1px solid #COLOR_15; border-bottom: none; } span.mergedNamesRow { display: table-row; background-color: #COLOR_08; } span.mergedNamesRowAlt { display: table-row; background-color: #COLOR_07; } span.mergedNamesCount { display: table-cell; text-align: right; padding: 1px 4px; } span.mergedNamesName { display: table-cell; text-align: left; padding: 1px 8px 1px 0px; } /* Call site page: the first row of one function, which this rule separates from the function above it */ tr.callgraphFirstRow td { border-top: 3px solid #COLOR_03; } /* Call site page: a row which says something about a function rather than carrying a call - which alias' callers follow. Spans the columns which describe a call */ td.callgraphNote { text-align: left; background-color: #COLOR_14; } /* Call site page: a call-site cell - location, callee or caller name */ td.coverCallgraph { text-align: left; padding-left: 10px; padding-right: 20px; color: #COLOR_15; background-color: #COLOR_17; /* very light pale grey/blue */ font-family: monospace; } /* Call site page: a call site which was never taken */ td.coverCallgraphLo { text-align: left; padding-left: 10px; padding-right: 10px; background-color: #COLOR_10; font-weight: bold; font-family: monospace; } /* Call site page: a call site which was taken */ td.coverCallgraphHi { text-align: left; padding-left: 10px; padding-right: 10px; background-color: #COLOR_08; font-weight: bold; font-family: monospace; } /* Call site page: a call site whose count the capture did not have - see FunctionEntry::SITE_COUNT. Neither of the two colours above, because either of them would answer a question the data does not */ td.coverCallgraphUnknown { text-align: left; padding-left: 10px; padding-right: 10px; background-color: #COLOR_17; font-weight: bold; font-family: monospace; } /* Call site page: the count of an external caller row, on a page whose other counts are coloured by category - see _callgraphExternalCountCell. The colour stays; the rest is the look of the category cells */ td.callgraphTlaCount { text-align: right; font-weight: normal; font-family: inherit; padding-left: 1px; padding-right: 1px; } /* Call site page: which alias' callers follow, for a function which has more than one */ span.callgraphCaption { margin-left: 10px; font-family: sans-serif; font-style: italic; } /* Call site pages: the name cell of one alias group of a function, which spans the rows of the group - written as its row in the function table is, indented under the function's name */ td.calleeFnAlias { text-align: left; vertical-align: top; padding-left: 30px; padding-right: 20px; color: #COLOR_15; background-color: $ownerBackground; font-family: monospace; } /* Call site pages: the call cells of a row of an alias group, set off from those of the function's summary block above them - in a paler colour than theirs */ tr.callgraphGroupRow td.coverCallgraph { padding-left: 30px; background-color: #COLOR_21; } /* Call site pages and function table: the cells of a row of an alias group, in the subdued colours of the alias rows of the function table - see 'td.coverFnAlias'. The function table marks those rows 'fnGroupRow' */ tr.callgraphGroupRow td.coverCallgraphLo { background-color: $ownerCovLo; } tr.callgraphGroupRow td.coverCallgraphHi, tr.callgraphGroupRow td.calleeXref, tr.callgraphGroupRow td.calleeXrefAlt, tr.fnGroupRow td.coverFn { background-color: $ownerBackground; } tr.callgraphGroupRow td.coverPerHi, tr.fnGroupRow td.coverPerHi { background-color: $ownerCovHi; } tr.callgraphGroupRow td.coverPerMed, tr.fnGroupRow td.coverPerMed { background-color: $ownerCovMed; } tr.callgraphGroupRow td.coverPerLo, tr.fnGroupRow td.coverPerLo { background-color: $ownerCovLo; } /* Source code view: a call site whose callee was reached */ a.callSite { color: #COLOR_15; text-decoration: underline dotted; } /* Source code view: a call site which was never taken */ a.callSiteLo { color: #COLOR_15; background-color: #COLOR_10; text-decoration: underline dotted; } /* Source code view: a call site whose count the capture did not have. A dashed underline rather than the 'not taken' background: the site is not known to have been missed, only not measured */ a.callSiteUnknown { color: #COLOR_15; text-decoration: underline dashed; } /* Source code view: where a call site goes and how often - one 'count: name' line per destination, drawn on hover from the attribute for the same reasons as 'calleeMulti'. The Lines alternate color: a stripe the height of one line, repeated */ a[data-calls] { position: relative; } a[data-calls]:hover::after { content: attr(data-calls); position: absolute; left: 0; top: 100%; z-index: 10; white-space: pre; font-family: monospace; font-style: normal; font-weight: normal; font-size: smaller; line-height: 1.5em; padding: 0px 8px 0px 4px; text-align: left; color: #COLOR_15; border: 1px solid #COLOR_15; background: repeating-linear-gradient(#COLOR_08 0em, #COLOR_08 1.5em, #COLOR_07 1.5em, #COLOR_07 3em); } /* Source code view: how many places a call site can go to. The digit is generated from the attribute rather than written into the page, so that 'html2lcov' - which strips tags but keeps their text - does not recover it as part of the source line */ span.calleeMulti::after { content: attr(data-n); vertical-align: super; font-size: smaller; } /* Source code view: the sites on a line which the source itself cannot point at - the implicit destructor call at a closing brace, or a nested call whose column the compiler folded onto the enclosing one. They are named in a comment appended to the line, generated - text and all - from the attributes, for the reason 'calleeMulti' is */ span.callgraphAnnot::before { content: " // calls: "; font-style: italic; } a.callgraphAnnot::after { content: attr(data-callee); font-style: italic; } a.callgraphAnnot + a.callgraphAnnot::before { content: ", "; font-style: italic; } /* Source code view: source code format */ pre.source { font-family: monospace; white-space: pre; margin-top: 2px; tab-size: #TAB_SIZE; } /* elided/removed code */ span.elidedSource { font-family: sans-serif; /*font-size: 8pt; */ font-style: italic; background-color: lightgrey; } /* Source code view: line number format */ span.lineNum { background-color: #COLOR_09; } /* Source code view: line number format when there are deleted lines in the corresponding location */ span.lineNumWithDelete { foreground-color: #COLOR_09; background-color: lightgrey; } /* Source code view: format for Cov legend */ span.coverLegendCov { padding-left: 10px; padding-right: 10px; padding-bottom: 2px; background-color: #COLOR_07; } /* Source code view: format for NoCov legend */ span.coverLegendNoCov { padding-left: 10px; padding-right: 10px; padding-bottom: 2px; background-color: #COLOR_12; } /* Source code view: format for the source code heading line */ pre.sourceHeading { white-space: pre; font-family: monospace; font-weight: bold; margin: 0px; } /* All views: header legend value for low rate */ td.headerValueLegL { font-family: sans-serif; text-align: center; white-space: nowrap; padding-left: 4px; padding-right: 2px; background-color: #COLOR_10; font-size: 80%; } /* All views: header legend value for med rate */ td.headerValueLegM { font-family: sans-serif; text-align: center; white-space: nowrap; padding-left: 2px; padding-right: 2px; background-color: #COLOR_13; font-size: 80%; } /* All views: header legend value for hi rate */ td.headerValueLegH { font-family: sans-serif; text-align: center; white-space: nowrap; padding-left: 2px; padding-right: 4px; background-color: #COLOR_04; font-size: 80%; } /* All views except source code view: legend format for low coverage */ span.coverLegendCovLo { padding-left: 10px; padding-right: 10px; padding-top: 2px; background-color: #COLOR_10; } /* All views except source code view: legend format for med coverage */ span.coverLegendCovMed { padding-left: 10px; padding-right: 10px; padding-top: 2px; background-color: #COLOR_13; } /* All views except source code view: legend format for hi coverage */ span.coverLegendCovHi { padding-left: 10px; padding-right: 10px; padding-top: 2px; background-color: #COLOR_04; } a.branchTla:link { color: #COLOR_00; } a.branchTla:visited { color: #COLOR_00; } a.mcdcTla:link { color: #COLOR_00; } a.mcdcTla:visited { color: #COLOR_00; } END_OF_CSS ; # the full list, not the presentation order: a source page emits # 'class="tlaECC"'/'class="tlaEUC"' for an excluded coverpoint even in # legacy-label mode, and a class with no rule renders as ordinary text in a # column whose whole purpose is the color # ..and the paler colour of each, for the rows of an alias group: halfway # to the page background. A named colour is left as it is my $page = ($dark_mode ? \%lcovutil::dark_palette : \%lcovutil::normal_palette) ->{COLOR_14}; foreach my $tla (@SummaryInfo::allTla) { my $title = $SummaryInfo::tlaToTitle{$tla}; my $color = $lcovutil::tlaColor{$tla}; my $paler = $color; $paler = sprintf('#%02X%02X%02X', map({ ( hex(substr($color, 1 + 2 * $_, 2)) + hex(substr($page, 2 * $_, 2))) / 2 } 0 .. 2)) if $color =~ /^#[0-9a-f]{6}$/i; foreach my $elem ("td", "span") { my $align = $elem eq 'td' ? "right" : "left"; $css_data .= ($_ = <<"END_OF_SPAN") /* Source code view/table entry background: format for lines classified as "$title" */ $elem.tla$tla { text-align: $align; background-color: $color; } $elem.tlaBg$tla { background-color: $color; } END_OF_SPAN ; } # the href anchor tag background $css_data .= ($_ = <<"END_OF_SPAN"); a.tlaBg$tla { background-color: $color; color: #COLOR_00; } td.headerCovTableHead$tla { text-align: center; padding-right: 6px; padding-left: 6px; padding-bottom: 0px; font-family: sans-serif; white-space: nowrap; background-color: $color; } /* Call site pages and function table: the category of a row of an alias group - see 'tr.callgraphGroupRow' and 'tr.fnGroupRow' */ tr.callgraphGroupRow td.tla$tla, tr.callgraphGroupRow a.tlaBg$tla, tr.fnGroupRow td.tla$tla { background-color: $paler; } END_OF_SPAN } # 'span' tags for date bins... # probably should have one for each bin... $css_data .= ($_ = <<"END_OF_DATE_SPAN") /* Source code view: format for date/owner bin that is not hit */ span.missBins { background-color: #COLOR_10 /* red */ } END_OF_DATE_SPAN ; # ************************************************************* # Remove leading tab from all lines $css_data =~ s/^\t//gm; $css_data =~ s/^ //gm; # and 8 spaces... # Apply palette my $palette = $dark_mode ? \%lcovutil::dark_palette : \%lcovutil::normal_palette; while (my ($key, $color) = each(%$palette)) { $css_data =~ s/$key/$color/gm; } if ($tab_size < 0) { my $abs_tab_size = abs($tab_size); $css_data =~ s/#TAB_SIZE/$abs_tab_size/gm; } else { $css_data =~ s/^.*#TAB_SIZE.*\n//gm; } print(CSS_HANDLE $css_data); close(CSS_HANDLE) or die("unable to close CSS handle: $!\n"); } # # get_bar_graph_code(base_dir, cover_found, cover_hit) # # Return a string containing HTML code which implements a bar graph display # for a coverage rate of cover_hit * 100 / cover_found. # sub get_bar_graph_code($$$) { my ($base_dir, $found, $hit) = @_; my $graph_code; # Check number of instrumented lines if ($found == 0) { return ""; } my $alt = rate($hit, $found, "%"); my $width = rate($hit, $found, undef, 0); my $remainder = 100 - $width; # Decide which .png file to use my $png_name = $rate_png[classify_rate($found, $hit, $ln_med_limit, $ln_hi_limit)]; if ($width == 0) { # Zero coverage $graph_code = (<

$alt
END_OF_HTML } elsif ($width == 100) { # Full coverage $graph_code = (<$alt END_OF_HTML } else { # Positive coverage $graph_code = (<$alt$alt END_OF_HTML } # Remove leading tabs from all lines $graph_code =~ s/^\t+//gm; chomp($graph_code); return ($graph_code); } # # sub classify_rate(found, hit, med_limit, high_limit) # # Return 0 for low rate, 1 for medium rate and 2 for hi rate. # sub classify_rate($$$$) { my ($found, $hit, $med, $hi) = @_; if ($found == 0) { return 2; } my $rate = rate($hit, $found); if ($rate < $med) { return 0; } elsif ($rate < $hi) { return 1; } return 2; } # # sub covLimits(covType) # # Return the (medium, high) rate limits for one of the SummaryInfo coverage # types. Written once rather than once per caller: the same chain would # otherwise appear verbatim in both 'buildDateSummaryTable' and # 'buildOwnerSummaryTable', so every new metric would have to be added to each # of them. # sub covLimits($) { my $covType = shift; return ($ln_med_limit, $ln_hi_limit) if $covType == SummaryInfo::LINE_DATA; return ($fn_med_limit, $fn_hi_limit) if $covType == SummaryInfo::FUNCTION_DATA; return ($callgraph_med_limit, $callgraph_hi_limit) if $covType == SummaryInfo::CALLGRAPH_DATA; return ($br_med_limit, $br_hi_limit) if $covType == SummaryInfo::BRANCH_DATA; die("unexpected cover type $covType") unless $covType == SummaryInfo::MCDC_DATA; return ($mcdc_med_limit, $mcdc_hi_limit); } # # parseMetricColumnOrder(spec) # # The coverage types of the comma-separated 'genhtml_metric_column_order' # list, in its order. Metrics omitted from the user's list go at the # end, in the default order. # sub parseMetricColumnOrder($) { my $spec = shift; my (@order, %seen); foreach my $name (split($lcovutil::split_pattern, $spec)) { $name =~ s/^\s+|\s+$//g; my $type = $metricColumnName{lc($name)}; if (!defined($type) || exists($seen{$type})) { lcovutil::ignorable_error( $lcovutil::ERROR_USAGE, "Unsupported value 'genhtml_metric_column_order = $spec': " . (defined($type) ? "'$name' appears more than once" : "unknown metric '$name'") . ". Expected a list of 'line', 'mcdc', 'branch', 'function' and 'callgraph'." ); # ..ignored: the default return @defaultMetricColumnOrder; } $seen{$type} = 1; push(@order, $type); } push(@order, grep({ !exists($seen{$_}) } @defaultMetricColumnOrder)); return @order; } # # metricColumnRank(sort_type) # # The position of each metric's column group in a coverage table sorted by # 'sort_type': {covType => rank}. The 'sorted by' metric is leftmost - # the user is focused on it and the table is wide, so make it visible. # Other metrics follow it in the configured order. # sub metricColumnRank($) { my $sort_type = shift; my %sorted = ($SORT_LINE => SummaryInfo::LINE_DATA, $SORT_MCDC => SummaryInfo::MCDC_DATA, $SORT_BRANCH => SummaryInfo::BRANCH_DATA, $SORT_FUNC => SummaryInfo::FUNCTION_DATA, $SORT_CALLGRAPH => SummaryInfo::CALLGRAPH_DATA,); my $first = $sorted{$sort_type}; my @order = @metricColumnOrder; @order = ($first, grep({ $_ != $first } @order)) if defined($first); my %rank; @rank{@order} = (0 .. $#order); return \%rank; } # # functionTableColumns(sort_type) # # The order of the proportion columns of the function table sorted by # 'sort_type': the 'sorted by' column, then the rest in configured order. # The table has 2 callgraph columns (caller and callees) and no function # column. sub functionTableColumns($) { my $sort_type = shift; my %columns = (SummaryInfo::LINE_DATA() => ['line'], SummaryInfo::MCDC_DATA() => ['mcdc'], SummaryInfo::BRANCH_DATA() => ['branch'], SummaryInfo::CALLGRAPH_DATA() => ['callers', 'callgraph'],); my %sorted = ($SORT_MISSING_LINE => 'line', $SORT_MISSING_MCDC => 'mcdc', $SORT_MISSING_BRANCH => 'branch', $SORT_MISSING_CALLERS => 'callers', $SORT_MISSING_CALLGRAPH => 'callgraph',); my @order = map({ exists($columns{$_}) ? @{$columns{$_}} : () } @metricColumnOrder); my $first = $sorted{$sort_type}; @order = ($first, grep({ $_ ne $first } @order)) if defined($first); return @order; } # # write_html(filehandle, html_code) # # Write out HTML_CODE to FILEHANDLE while removing a leading tabulator mark # in each line of HTML_CODE. # sub write_html(*$) { local *HTML_HANDLE = $_[0]; my $html_code = $_[1]; # Remove leading tab from all lines. This matters for the heredoc template # callers whose HTML is indented in the source. The per-source-line hot # path builds strings whose tabs have already been expanded to spaces by # escape_html, so they contain no tab at all -- skip the (multiline) regex # entirely in that common case. $html_code =~ s/^\t//gm if (index($html_code, "\t") >= 0); # The links are built out of path names, but a URL separates its elements # with '/' whatever the host does $html_code =~ s{\b((?:href|src)=")([^"]*\\[^"]*)"}{$1 . ($2 =~ tr|\\|/|r) . '"'}ge if ('\\' eq $lcovutil::dirseparator && index($html_code, '\\') >= 0); print(HTML_HANDLE $html_code) or die("cannot write HTML data ($!)\n"); } # # write_html_prolog(filehandle, base_dir, pagetitle) # # Write an HTML prolog common to all HTML files to FILEHANDLE. PAGETITLE will # be used as HTML page title. BASE_DIR contains a relative path which points # to the base directory. # sub write_html_prolog(*$$) { my $basedir = $_[1]; my $pagetitle = $_[2]; my $prolog; $prolog = $html_prolog; $prolog =~ s/\@pagetitle\@/$pagetitle/g; $prolog =~ s/\@basedir\@/$basedir/g; write_html($_[0], $prolog); } # # write_header_prolog(filehandle, base_dir) # # Write beginning of page header HTML code. # sub write_header_prolog(*$) { # ************************************************************* write_html($_[0], < $title END_OF_HTML # ************************************************************* } # # write_header_line(handle, content) # # Write a header line with the specified table contents. # sub write_header_line(*@) { my ($handle, @content) = @_; write_html($handle, " \n"); # label order has to match data that is passed my @labels = qw/width class colspan title/; foreach my $entry (@content) { my @d = @$entry; my $text; $text = splice(@d, 2, 1) if (scalar(@d) > 1); # entry may not contain some data - e.g., does not have colspan or title die("unexpected entry format") unless scalar(@labels) >= scalar(@d); my $str = " whatever' write_html($handle, $str); } write_html($handle, " \n"); # then end the row } # # write_header_epilog(filehandle, base_dir) # # Write end of page header HTML code. # sub write_header_epilog(*$) { # ************************************************************* write_html($_[0], <
END_OF_HTML # ************************************************************* } # # write_file_table_prolog(handle, file_heading, binHeading, primary_key, ([heading, num_cols], ...)) # # Write heading for file table. # sub write_file_table_prolog(*$$$@) { my ($handle, $file_heading, $bin_heading, $primary_key, @columns) = @_; my $num_columns = 0; my $file_width = 40; my $col; my $width; foreach $col (@columns) { my ($heading, $cols, $titles) = @{$col}; $num_columns += $cols; } $num_columns++ if (defined($bin_heading)); $width = int((100 - $file_width) / $num_columns); # Table definition write_html($handle, < END_OF_HTML if (defined($bin_heading)) { # owner or date column write_html($handle, < END_OF_HTML } # Empty first row foreach $col (@columns) { my ($heading, $cols) = @{$col}; while ($cols-- > 0) { write_html($handle, < END_OF_HTML } } # Next row if ($primary_key eq "name") { my $spanType = defined($bin_heading) ? "colspan" : "rowspan"; ++$num_columns; write_html($handle, < END_OF_HTML } else { my $t = ucfirst($primary_key); # a bit of a hack...just substitute the primary key and related # strings into the 'file heading' link - so we display the # 'sort' widget if ($primary_key eq 'owner' && $file_heading =~ /^([^ ]+) END_OF_HTML } # Heading row foreach $col (@columns) { my ($heading, $cols, $titles) = @{$col}; my $colspan = ""; my $rowspan = ""; $colspan = " colspan=$cols" if ($cols > 1); $rowspan = " rowspan=2" if (!defined($titles)); write_html($handle, <$heading END_OF_HTML } write_html($handle, < END_OF_HTML # title row if (defined($bin_heading)) { # Next row my $str = ucfirst($bin_heading); write_html($handle, <Name END_OF_HTML } foreach $col (@columns) { my ($heading, $cols, $titles) = @{$col}; my $colspan = ""; my $rowspan = ""; if (defined($titles)) { foreach my $t (@$titles) { my $span = ""; my $popup = ''; if ("ARRAY" eq ref($t)) { my ($tla, $num, $help) = @$t; $span = " colspan=" . $num if $num > 1; $popup = " title=\"$help\"" if (defined $help); $t = $tla; } write_html($handle, < $t END_OF_HTML } } } write_html($handle, < END_OF_HTML return $num_columns; } sub escape_id($) { my ($name) = @_; # Name/ID attribute requirements according to HTML 4.01 Transitional $name =~ s/[^A-Za-z0-9-_:.]/_/g; return $name; } # write_file_table_entry(handle, base_dir, # [ name, [filename, fileDetails, fileHref, cbdata], # activeTlaCols, # rowspan, primary_key, is_secondary, fileview, # page_type, page_link, dirSummary, showDetailCol, # asterisk ], # ([ found, hit, med_limit, hi_limit, graph ], ..) # # Write an entry of the file table. # $fileview: 0 == 'table is listing directories', 1 == 'list files' # sub write_file_table_entry(*$$@) { my ($handle, $base_dir, $data, @entries) = @_; my ($name, $callbackData, $activeTlaCols, $rowspan, $primary_key, $is_secondary, $fileview, $page_type, $page_link, $dirSummary, $showBinDetailColumn, $asterisk) = @$data; my ($filename, $fileSummary, $fileDetails, $file_link, $cbData) = @$callbackData; die("unexpected callback arg types: write_file_table_entry(" . ref($fileSummary) . ', ' . ref($fileDetails) . ", $file_link, " . (defined($cbData) ? $cbData : 'undef') . ')') unless (!defined($fileDetails) || 'SourceFile' eq ref($fileDetails)) && 'SummaryInfo' eq ref($fileSummary); my $esc_name = escape_html($name); if ($main::flat && !$is_secondary) { my $relDir = $fileSummary->relativeDir(); die("relative directory not set") unless $relDir; $esc_name = escape_html(File::Spec->catfile($relDir, $name)) if '.' ne $relDir; } my $obj_name = $esc_name . ($fileSummary->type() eq 'directory' && $primary_key eq 'name' ? escape_html($lcovutil::dirseparator) : ''); my $namecode = $obj_name; my $owner; my $full_path = 'directory' eq $fileSummary->type() ? $fileSummary->fullDir() : $fileSummary->name(); $full_path = escape_html($full_path); # Add link to source if provided my $source_link; my $suppressFileHref = $main::no_sourceview && $fileSummary->type() eq 'file'; if ($is_secondary == 2) { if (defined($page_link) && $page_link ne "") { # could use $fileSummary->name() in the popup $source_link = $suppressFileHref ? $obj_name : ("type() . " $full_path\">$obj_name"); } elsif (defined($file_link) && $file_link ne "") { my $target = File::Basename::basename($fileSummary->name()); if ($main::flat) { $target = File::Spec->catfile($fileSummary->relativeDir(), $target); } if ($fileSummary->type() eq 'directory') { $target = File::Basename::dirname($target); $target = File::Basename::basename($target) if ($main::hierarchical); } $target = escape_html($target); $source_link = $suppressFileHref ? $target : ( "$target" ); } else { $source_link = $fileSummary->name(); $source_link = File::Basename::basename($source_link) if ($main::hierarchical); $source_link = escape_html($source_link); } } my $anchor = 'NAME'; if (!$is_secondary && defined($page_link) && $page_link ne "") { # could use $fileSummary->name() in the popup $namecode = $suppressFileHref ? $obj_name : ("type() . " $full_path\">$obj_name"); $owner = ""; } elsif ($is_secondary && $primary_key ne 'name' && defined($file_link) && $file_link ne "") { if ($main::flat) { $file_link = File::Spec->catfile($fileSummary->relativeDir(), $file_link); } elsif ($main::hierarchical && 'directory' eq $fileSummary->type()) { die("no parent for " . $fileSummary->name()) unless defined($fileSummary->parent()); my $pname = $fileSummary->parent()->name(); die("unexpected parent '$pname' in $file_link") unless (length($pname) < length($file_link) && $pname eq substr($file_link, 0, length($pname))); $file_link = substr($file_link, length($pname) + 1); #lcovutil::info("$filename: path is $file_link, base is '$base_dir' full path $full_path\n"); } $namecode = $suppressFileHref ? $obj_name : ( "$obj_name" ); } elsif ($is_secondary && $primary_key ne 'name' && $name eq $filename) { # get here when we suppressed the sourceview - so the file link # is not defined $namecode = $obj_name; } elsif (defined($primary_key)) { $namecode = $obj_name; if (!$suppressFileHref) { # we want the HREF anchor on the column 1 entry - # the column 0 entry may span many rows - so navigation to that # entry (e.g., to find all the files in the "(7..30] days" bin) # may be rendered such that the first element in the bin is not # visible (you have to scroll up to see it). # the fix is to put the anchor in the next column if ($primary_key eq 'owner') { $anchor = 'NAME'; } elsif ($primary_key eq 'date') { die("unexpected bin") unless (!$is_secondary && exists($SummaryInfo::ageHeaderToBin{$name})) || (defined($cbData) && $cbData <= $#SummaryInfo::ageGroupHeader); my $bin = $is_secondary ? $cbData : $SummaryInfo::ageHeaderToBin{$name}; $anchor = "NAME"; } } } my $tableHref; if (defined($file_link) && !$main::no_sourceview && $main::show_tla && defined($fileDetails)) { $tableHref = "href=\"$file_link#L__LINE__\""; # href to anchor in frame doesn't seem to work in either firefox # or chrome. However, this seems like the right syntax. $tableHref = undef # $tableHref . ' target="source"' if $main::frames; } # First column: name my $nameClass = $fileSummary->type() eq 'directory' ? 'coverDirectory' : 'coverFile'; my $prefix; if ($is_secondary) { $prefix = "owner_"; } else { $prefix = ""; $nameClass = 'ownerName' if $primary_key ne 'name'; } if ($is_secondary && ( $primary_key eq 'name' || ($primary_key ne 'name' && $fileview == 0)) ) { if ($fileview == 0 && $primary_key ne 'name' && $main::flat) { # link to (flat) file detail my $relDir = $fileSummary->relativeDir(); die("relative directory not set") unless $relDir; $namecode = $suppressFileHref ? $obj_name : ("catfile($relDir, "$filename.gcov.$html_ext") . "\" title=\"Click to go to $esc_name source detail\">$obj_name" ); } else { # link to the entry in date/owner 'summary' table (keyed other way up) $namecode = "$obj_name"; } } write_html($handle, " \n"); my $elide_note = ''; if ($is_secondary == 2) { die("unexpected cbData for $page_type, $primary_key, $name, " . (defined($cbData) ? $cbData : 'undef')) unless ($page_type eq 'owner' || ($page_type eq 'date' && ((defined($cbData) && $cbData <= $#SummaryInfo::ageGroupHeader) || (!defined($cbData) && exists($SummaryInfo::ageHeaderToBin{$name}))))); my $entry; if ($primary_key eq 'name') { die("source_link undefined: " . ( defined($fileSummary) ? ( $fileSummary->type() . " $name:" . $fileSummary->name()) : "")) unless defined($source_link); $entry = $source_link; } else { # select the appropriate bin... my $id = defined($cbData) ? ($page_type eq 'owner' ? $cbData : $SummaryInfo::ageGroupHeader[$cbData]) : $name; # need an anchor here - this is the destination of an HREF from # the header 'owner' or 'date' summary table $entry = '" . escape_html($id) . ""; } my $class = $primary_key eq 'name' ? ($fileSummary->type() eq 'directory' ? 'coverDirectory' : 'coverFile') : 'ownerName'; write_html($handle, " \n"); $elide_note = '∗∗'; } my $span = (1 == $rowspan) ? "" : " rowspan=$rowspan"; write_html($handle, " \n"); # no 'owner' column if the entire directory is not part of the project # (i.e., no files in this directory are in the repo) if ((defined($showBinDetailColumn) && $dirSummary->hasOwnerInfo()) || (defined($primary_key) && $primary_key ne 'name' && !$is_secondary) ) { $anchor =~ s/NAME/Total/; $anchor .= "$asterisk" if defined($asterisk); write_html($handle, <$anchor END_OF_HTML } foreach my $entry (@entries) { my ($found, $hit, $med, $hi, $graph, $summary, $covType) = @{$entry}; my $bar_graph; my $class; my $rate; # Generate bar graph if requested if ($graph) { if (!$is_secondary) { $class = $prefix . 'coverBar'; $bar_graph = get_bar_graph_code($base_dir, $found, $hit); } else { # graph is distracting for the second-level elements - skip them $bar_graph = ""; $class = 'coverFile'; } write_html($handle, < $bar_graph END_OF_HTML } # Get rate color and text if ($found == 0) { $rate = "-"; $class = "Hi"; } else { $rate = rate($hit, $found, " %"); $class = $rate_name[classify_rate($found, $hit, $med, $hi)]; } # Show negative number of items without coverage $hit -= $found # negative number if ($main::opt_missed); write_html($handle, <$rate END_OF_HTML if ($summary) { my @keys = ("found"); if ($main::show_hitTotalCol) { push(@keys, $opt_missed ? "missed" : "hit"); } if ($main::show_tla) { push(@keys, @{$activeTlaCols->{$covType}}); } foreach my $key (@keys) { next if exists($SummaryInfo::excludedCoverpointTla{$key}); my $count = $summary->get($key); #print("$name: $key " . $summary->get($key)); $class = $page_type ne "owner" ? "coverNumDflt" : "ownerTla"; my $v = ""; if (defined($count) && 0 != $count) { $count = -$count if $key eq 'missed'; $v = $count; # want to colorize the UNC, LBC, UIC rows if not zero $class = "tla$key" if (!$main::use_legacyLabels && exists($SummaryInfo::concerningTla{$key})); my $column = ''; # look in file details to build link to first line # '$tableHref is undef unless the data we need is available if (defined($tableHref) && !exists($SummaryInfo::deletedTla{$key}) && exists($SummaryInfo::displayedTla{$key})) { my $line; my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacySrcLabel{$key} : $key; # need to keep $fileDetails object so we know the # 'first location in ...' for hyperlink from the # file table to the line in the file my $title = "\"Go to first $label " . SummaryInfo::type2coverpoint($covType) . ' '; if ('fileOrDir' eq $page_type) { # go to first line of the indicated type in the file $line = $fileDetails->nextTlaGroup($key) if $covType == SummaryInfo::LINE_DATA; $line = $fileDetails->nextBranchTlaGroup($key) if $covType == SummaryInfo::BRANCH_DATA; $line = $fileDetails->nextMcdcTlaGroup($key) if $covType == SummaryInfo::MCDC_DATA; $line = $fileDetails->nextCallgraphTlaGroup($key) if $covType == SummaryInfo::CALLGRAPH_DATA; } elsif ('owner' eq $page_type) { my $owner = $summary->owner(); $column = $owner; $title .= "in '$owner' bin "; $line = $fileDetails->nextInOwnerBin($owner, $key) if $covType == SummaryInfo::LINE_DATA; $line = $fileDetails->nextBranchInOwnerBin($owner, $key) if $covType == SummaryInfo::BRANCH_DATA; $line = $fileDetails->nextMcdcInOwnerBin($owner, $key) if $covType == SummaryInfo::MCDC_DATA; $line = $fileDetails->nextCallgraphInOwnerBin($owner, $key) if $covType == SummaryInfo::CALLGRAPH_DATA; } elsif ('date' eq $page_type) { my $agebin = $summary->bin(); $column = $agebin; $title .= "in '$SummaryInfo::ageGroupHeader[$agebin]' bin "; $line = $fileDetails->nextInDateBin($agebin, $key) if $covType == SummaryInfo::LINE_DATA; $line = $fileDetails->nextBranchInDateBin($agebin, $key) if $covType == SummaryInfo::BRANCH_DATA; $line = $fileDetails->nextMcdcInDateBin($agebin, $key) if $covType == SummaryInfo::MCDC_DATA; $line = $fileDetails->nextCallgraphInDateBin($agebin, $key) if $covType == SummaryInfo::CALLGRAPH_DATA; } else { die("unexpected page detail type '$page_type'"); } $title .= "in $filename\""; if ($class eq "tla$key") { # add another CSS style to set the TLA-specific background # color $class .= " tlaBg$key"; } if (defined($line)) { my $href = $tableHref; $href =~ s/__LINE__/$line/; $v = "$v END_OF_HTML } # foreach key } else { write_html($handle, <$hit / $found END_OF_HTML } } # End of row write_html($handle, < END_OF_HTML } # # write_file_table_detail_entry(filehandle, base_dir, test_name, bin_type, activeTlaCols, ([found, hit], ...)) # # Write entry for detail section in file table. # sub write_file_table_detail_entry(*$$$$@) { my ($handle, $base_dir, $test, $showBinDetail, $activeTlaCols, @entries) = @_; if ($test eq "") { $test = "<unnamed>"; } elsif ($test =~ /^(.*),diff$/) { $test = $1 . " (converted)"; } # Testname - which spans the bar graph column of the line coverage, where # that is the next column my $barGraphCell = defined($showBinDetail) || SummaryInfo::LINE_DATA != $entries[0]->[2]; my $span = $barGraphCell ? 1 + defined($showBinDetail) : 2; write_html($handle, < END_OF_HTML # Test data foreach my $entry (@entries) { my ($found, $hit, $covtype, $callback) = @{$entry}; if (!defined($found)) { # no data for this type: an empty column group my $n = 2 + ($main::show_hitTotalCol ? 1 : 0) + ($main::show_tla ? scalar(@{$activeTlaCols->{$covtype}}) : 0); write_html($handle, " \n") while $n--; next; } my $rate = rate($hit, $found, " %"); if (SummaryInfo::LINE_DATA == $covtype && $barGraphCell) { write_html($handle, " \n"); } write_html($handle, " \n"); write_html($handle, " \n"); if ($main::show_hitTotalCol) { write_html($handle, " \n"); } if ($main::show_tla) { foreach my $tla (@{$activeTlaCols->{$covtype}}) { my $count = $callback->count($tla); $count = "" if 0 == $count; write_html($handle, " \n"); } } } write_html($handle, " \n"); } # # write_file_table_epilog(filehandle) # # Write end of file table HTML code. # # # _inColumnOrder(rank, key, columnGroups) # # The column groups of one table row, in the order of the table's columns: # element 'key' of each group is its coverage type - see metricColumnRank. # sub _inColumnOrder($$@) { my ($rank, $key, @groups) = @_; return sort({ $rank->{$a->[$key]} <=> $rank->{$b->[$key]} } @groups); } sub write_file_table_epilog(*) { # ************************************************************* write_html($_[0], <
END_OF_HTML } # # write_test_table_prolog(filehandle, table_heading) # # Write heading for test case description table. # sub write_test_table_prolog(*$) { # ************************************************************* write_html($_[0], <

$file_heading
$t $file_heading
$str
$entry$namecode$elide_note$test$rate$found$hit$count
$_[1]
END_OF_HTML # ************************************************************* } # # write_test_table_entry(filehandle, test_name, test_description) # # Write entry for the test table. # sub write_test_table_entry(*$$) { # ************************************************************* my $name = escape_id($_[1]); write_html($_[0], <$_[1] 
$_[2]

END_OF_HTML # ************************************************************* } # # write_test_table_epilog(filehandle) # # Write end of test description table HTML code. # sub write_test_table_epilog(*) { # ************************************************************* write_html($_[0], <

END_OF_HTML # ************************************************************* } sub fmt_centered($$) { my ($width, $text) = @_; my $w0 = length($text); my $w1 = $width > $w0 ? int(($width - $w0) / 2) : 0; my $w2 = $width > $w0 ? $width - $w0 - $w1 : 0; return (" " x $w1) . $text . (" " x $w2); } # # write_source_prolog(filehandle) # # Write start of source code table. # sub write_source_prolog(*$$$) { my ($handle, $fileHasProjectData, $showBranches, $showMcdc) = @_; my $lineno_heading = " " x 9; my $branch_heading = ""; my $mcdc_heading = ""; my $tlaWidth = 4; my $age_heading = ""; my $owner_heading = ""; my $tla_heading = ""; if (defined($main::show_dateBins) && $fileHasProjectData) { $age_heading = fmt_centered(5, "Age"); $owner_heading = fmt_centered(20, "Owner"); } if (defined($main::show_tla)) { $tla_heading = fmt_centered($tlaWidth, "TLA"); } my $line_heading = fmt_centered($line_field_width, "Line data"); my $source_heading = " Source code"; if ($showBranches) { $branch_heading = fmt_centered($br_field_width, "Branch data") . " "; } if ($showMcdc) { $mcdc_heading = fmt_centered($mcdc_field_width, "MC/DC data") . " "; } # ************************************************************* write_html($handle, <
${age_heading} ${owner_heading} ${lineno_heading}${branch_heading}${mcdc_heading}${tla_heading}${line_heading} ${source_heading}
END_OF_HTML

    # *************************************************************
}

#
# get_block_len(block)
#
# Calculate total text length of all branches in a block of branches.
#

sub get_block_len($)
{
    my ($block) = @_;
    my $len = 0;

    foreach my $branch (@{$block}) {
        $len += $branch->[$BR_LEN];
    }
    return $len;
}

#
# get_block_list(brdata | mcdc_data, offset)
#
# common method for branch and MC/DC records
# Group elements that belong to the same basic record
#
# Returns: [block1, block2, ...] <- in order of increasing block number/group size
# block:   [branch1, branch2, ...] <- in order of increasing branch/expression index
# branch:  [block_num, branch_num, taken_count, text_length, open, close]
#

sub get_block_list($$)
{
    # only the blocks or groups at location 'offset', if it is defined - see
    #   PrintCallback::coverpointsAt
    my ($data, $offset) = @_;
    return () unless defined($data);

    my @blocks;

    if ('BranchLocation' eq ref($data)) {
        my $block = [];
        my $last_blkId;

        # Group branches
        foreach my $blk ($data->blocks(1)) {
            next if defined($offset) && $blk->locationOffset() != $offset;
            my $blockData = $blk->elements();
            my $blkId     = $blk->idx();
            my $branch    = 0;
            foreach my $br_elem (@$blockData) {
                if (defined($last_blkId) && $blkId ne $last_blkId) {
                    push(@blocks, $block);
                    $block = [];
                }
                my $br = [$blkId, $branch, $br_elem, 3, 0, 0];
                push(@{$block}, $br);
                $last_blkId = $blkId;
                ++$branch;
            }
        }
        push(@blocks, $block) if (scalar(@{$block}) > 0);
    } else {
        die('unexpected ' . ref($data)) unless 'MCDC_Block' eq ref($data);
        # Group MC/DC groups
        # one groups() call, not one per group:  it materializes a fresh hash
        my $groups = $data->groups();
        foreach my $groupKey (sort keys %$groups) {
            next
                if defined($offset) &&
                (MCDC_Block::parseGroupKey($groupKey))[1] != $offset;
            my $exprs = $groups->{$groupKey};
            my $block = [];
            push(@blocks, $block);
            foreach my $expr (@$exprs) {
                # display 'true' then 'false' sense - to match what we do for branches
                # and to match behaviour of ?: operator
                foreach my $sense (1, 0) {
                    my $cond =
                        [$groupKey, $expr->index(), $sense, $expr, 3, 0, 0];
                    push(@{$block}, $cond);
                }
            }
        }
    }
    # Add braces to first and last branch in group
    foreach my $block (@blocks) {
        $block->[0]->[$BR_OPEN] = 1;
        $block->[0]->[$BR_LEN]++;
        $block->[$#$block]->[$BR_CLOSE] = 1;
        $block->[$#$block]->[$BR_LEN]++;
    }
    return @blocks;
}

# distribute blocks into lines - trying to keep groups on a single line,
# if possible
sub distribute_blocks($$)
{
    my ($blocks, $field_width) = @_;

    my $line_len = 0;
    my $line     = [];    # [branch2|" ", branch|" ", ...]
    my @lines;            # [line1, line2, ...]
    my @result;

    # Distribute blocks to lines
    foreach my $block (@$blocks) {
        my $block_len = get_block_len($block);

        # Does this block fit into the current line?
        if ($line_len + $block_len <= $field_width) {
            # Add it
            $line_len += $block_len;
            push(@{$line}, @{$block});
            next;
        } elsif ($block_len <= $field_width) {
            # It would fit if the line was empty - add it to new line
            push(@lines, $line);
            $line_len = $block_len;
            $line     = [@{$block}];
            next;
        }
        my $split = ($block_len / $field_width) > 2;
        # this block is going to take up a bunch of lines -
        # add it to a new line

        # Split the block into several lines
        foreach my $branch (@{$block}) {
            if ($line_len + $branch->[$BR_LEN] >= $field_width ||
                $split) {
                # Start a new line
                if (($line_len + 1 <= $field_width) &&
                    scalar(@{$line}) > 0 &&
                    !$line->[scalar(@$line) - 1]->[$BR_CLOSE]) {
                    # Try to align branch symbols to be in
                    # one # row
                    push(@{$line}, " ");
                }
                push(@lines, $line);
                $line_len = 0;
                $line     = [];
                $split    = 0;
            }
            push(@{$line}, $branch);
            $line_len += $branch->[$BR_LEN];
        }
    }
    push(@lines, $line);

    return @lines;
}

#
# tla_class_and_prefix(tla)
#
# The CSS class and the tooltip prefix which a differential coverpoint of this
# TLA is drawn with.  Shared by the branch and the MC/DC renderers below - see
# the '@todo' on 'get_mcdc_html'.
#
# An excluded coverpoint has no legacy label ('noBaseline' keeps 'GNC' and 'UNC'
# only) and needs none:  the '$excluded' arm of both renderers replaces the class
# and writes its own title.  Asking for one is what left the MC/DC tooltip
# opening with a bare ': ', and skipping the whole element to avoid asking is
# what dropped a branch, its closing bracket and its share of the column width.
#
sub tla_class_and_prefix($)
{
    my $tla = shift;
    return ("tla$tla", '')
        if exists($SummaryInfo::excludedCoverpointTla{$tla});
    return ("tla$tla", "$tla: ") unless $main::use_legacyLabels;
    return ("tla$tla", $SummaryInfo::tlaToLegacySrcLabel{$tla} . ': ');
}

#
# get_branch_html(brdata, printCallbackStruct)
#
# Return a list of HTML lines which represent the specified branch coverage
# data in source code view.
#

sub get_branch_html($$)
{
    my ($brdata, $cbdata) = @_;
    my $differentialBranch;
    my $fileDetail = $cbdata->sourceDetail();
    my $lineNo     = $cbdata->lineNo();
    # build the 'blocks' array from differential data if we have it - from
    #   the blocks written at this line, which may not be always the line which
    #   holds them, in the case of a middle line of a multiline statement
    my @blocks;
    foreach my $at ($cbdata->coverpointsAt(0, $lineNo, $brdata)) {
        my ($data, $offset, $isDiff) = @$at;
        $differentialBranch = 1 if $isDiff;
        push(@blocks, get_block_list($data, $offset));
    }

    my @lines = distribute_blocks(\@blocks, $br_field_width);

    # Convert to HTML
    #  branch and MC/DC code is similar - but merging only makes things
    #  more complicated as the details of the text, popups, etc are different
    # maybe revisit later
    my @result;
    my %tlaLinks;

    foreach my $line (@lines) {
        my $current     = "";
        my $current_len = 0;

        foreach my $branch (@$line) {
            # Skip alignment space
            if ($branch eq " ") {
                $current .= " ";
                $current_len++;
                next;
            }
            my ($block_num, $br_num, $br, $len, $open, $close) = @{$branch};
            my $class;
            my $prefix;
            # one call for everything this element needs:  under XS that is a
            # single Perl<->C++ crossing rather than eight on a path executed
            # once per rendered branch.  $tla/$base_count come back undef for
            # non-differential data.
            my ($taken, $count, $excluded, $type_name, $expr, $tla, $base_count)
                = $br->render_data();
            unless (defined($differentialBranch)) {
                # vanilla case - no differential coverage info:  ignore any
                # differential fields the element happens to carry
                $tla = $base_count = undef;
                if ($taken eq '-' || $taken == 0) {
                    $class = "tlaUNC";
                } else {
                    $class = 'tlaGBC';
                }
                $prefix = '';
            } else {
                ($class, $prefix) = tla_class_and_prefix($tla);
            }
            my ($char, $title);

            my $br_name = $type_name;
            $br_name .= ' ' if $br_name;
            $br_name .= 'branch ';
            $br_name .= defined($expr) ? '"' . $expr . '"' : $br_num;
            if ($excluded) {
                $class = 'tla' . ($count ? 'ECC' : 'EUC');
                $char  = 'x';
                $title = "$br_name marked unreachable";
            } elsif ($taken eq '-') {
                $char  = "#";
                $title = "${prefix}$br_name was not executed";
                $title .=
                    " (previously taken $base_count time" .
                    (1 == $base_count ? '' : 's') . ')'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));
            } elsif ($taken == 0) {
                $char  = "-";
                $title = "${prefix}$br_name was not taken";
                $title .=
                    " (previously taken $base_count time" .
                    (1 == $base_count ? '' : 's') . ')'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));
            } else {
                $char  = "+";
                $title = "${prefix}$br_name was taken $taken time" .
                    (($taken > 1) ? "s" : "");
            }
            $title = escape_html($title) if defined($expr);
            $current .= "[" if ($open);

            if (!$main::no_sourceview &&
                !$excluded &&
                defined($differentialBranch)) {
                my $href;
                if (exists($tlaLinks{$tla})) {
                    $href = $tlaLinks{$tla};
                } else {
                    my $next = $fileDetail->nextBranchTlaGroup($tla, $lineNo);
                    $href =
                        "CHAR";
                    $tlaLinks{$tla} = $href;
                }
                # the cache is per TLA, but the marker character is per branch:
                #   '#' (never executed) and '-' (reached, not taken) are both
                #   uncovered, so two branches on one line can share a TLA and
                #   still need different glyphs.  Same placeholder treatment the
                #   MC/DC loop below gives its sense character
                $href =~ s/CHAR/$char/;
                $href =~ s#TITLE#$title#;
                $current .= " $href ";
            } else {
                $current .=
                    " $char ";
            }
            $current .= "]" if ($close);
            $current_len += $len;
        }

        # Right-align result text
        if ($current_len < $br_field_width) {
            $current = (" " x ($br_field_width - $current_len)) . $current;
        }
        push(@result, $current);
    }

    return @result;
}

sub get_mcdc_html($$)
{
    # @todo can be shared with branch, MC/DC
    my ($mcdc_data, $cbdata) = @_;
    my $differentialMcdc;
    my $fileDetail = $cbdata->sourceDetail();
    my $lineNo     = $cbdata->lineNo();
    # build the 'blocks' array from differential data if we have it - see
    #   get_branch_html
    my @blocks;
    foreach my $at ($cbdata->coverpointsAt(1, $lineNo, $mcdc_data)) {
        my ($data, $offset, $isDiff) = @$at;
        $differentialMcdc = 1 if $isDiff;
        push(@blocks, get_block_list($data, $offset));
    }

    my @lines = distribute_blocks(\@blocks, $mcdc_field_width);

    # Convert to HTML
    #  branch and MC/DC code is similar - but merging only makes things
    #  more complicated as the details of the text, popups, etc are different
    # maybe revisit later
    my @result;
    my %tlaLinks;

    foreach my $line (@lines) {
        my $current     = "";
        my $current_len = 0;

        foreach my $cond (@$line) {
            # Skip alignment space
            unless ('ARRAY' eq ref($cond)) {
                die("unexpected 'cond' entry '$cond'") unless ($cond eq " ");
                $current .= " ";
                $current_len++;
                next;
            }

            my ($groupSize, $exprIdx, $sense, $expr, $len, $open, $close) =
                @$cond;
            die("unexpected expr type") unless ref($expr) eq 'MCDC_Expression';

            # one call for everything this element needs - see the comment on
            # the corresponding branch loop above
            my ($count, $expr_name, $excluded, $multiGroup) =
                $expr->render_data($sense);
            my $taken;
            my $class;
            my $prefix;
            my $tla;
            my $base_count;
            if ('ARRAY' ne ref($count)) {
                # vanilla case - no differential coverage info
                die("differential MC/DC coverage but no TLA")
                    if defined($differentialMcdc);
                $taken = $count;
                if ($count == 0) {
                    $class = "tlaUNC";
                } else {
                    $class = 'tlaGBC';
                }
                $prefix = '';
            } else {
                die("differential MC/DC coverage but no TLA")
                    unless defined($differentialMcdc);
                ($tla, $base_count, $taken) = @$count;

                ($class, $prefix) = tla_class_and_prefix($tla);
            }
            my ($char, $title);

            $title =
                ${prefix} .
                ($sense ? 'True' : 'False') .
                ' sense of expression "' .
                $expr_name . '" ';
            if ($multiGroup) {
                $title .= 'in group "' . $expr->groupSize() . '" ';
            }

            if ($excluded) {
                $char  = 'x';
                $class = 'tla' . ($taken ? 'ECC' : 'EUC');
                $title .= "marked unreachable";
            } elsif (!defined($taken)) {
                $char = '-';
                $title .= 'was dropped';
                $title .=
                    ' (previously ' .
                    (1 == $base_count ? '' : 'not ') .
                    'sensitized)'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));

            } elsif ($taken == 0) {
                $char = $sense ? 't' : 'f';
                $title .= 'was not sensitized';
                $title .=
                    ' (previously ' .
                    (1 == $base_count ? '' : 'not ') .
                    'sensitized)'
                    if (defined($tla) &&
                        ($tla eq 'LBC' || $tla eq 'ECB'));
            } else {
                $char = $sense ? 'T' : 'F';
                $title .= 'was sensitized';
            }
            $title = escape_html($title);
            $current .= "[" if ($open);

            if (!$main::no_sourceview &&
                !$excluded &&
                defined($differentialMcdc)) {
                my $href;
                if (exists($tlaLinks{$tla})) {
                    $href = $tlaLinks{$tla};
                } else {
                    my $next = $fileDetail->nextMcdcTlaGroup($tla, $lineNo);
                    $href =
                        "SENSE";
                    # cache the href as 'next' lookup is moderately expensive
                    $tlaLinks{$tla} = $href;
                }
                # keep the link but update the sense character
                $href =~ s/SENSE/$char/;
                $href =~ s#TITLE#$title#;
                $current .= " $href ";
            } else {
                $current .=
                    " $char ";
            }
            $current .= "]" if ($close);
            $current_len += $len;
        }

        # Right-align result text
        if ($current_len < $mcdc_field_width) {
            $current = (" " x ($mcdc_field_width - $current_len)) . $current;
        }
        push(@result, $current);
    }

    return @result;
}

#
# format_count(count, width)
#
# Return a right-aligned representation of count that fits in width characters.
#

sub format_count($$)
{
    my ($count, $width) = @_;
    my $result;
    my $exp;
    my $negative = 0 > $count;
    if ($negative) {
        $width -= 2;
        $count = -$count;
    }

    $result = sprintf("%*.0f", $width, $count);
    while (length($result) > $width) {
        last if ($count < 10);
        $exp++;
        $count  = int($count / 10);
        $result = sprintf("%*s", $width, ">$count*10^$exp");
    }
    if ($negative) {
        # surround number with parens
        $result =~ s/^( *)(\S+)$/$1\($2\)/;
    }
    return $result;
}

#
# write_source_line(filehandle, cbdata, source, hit_count, brdata, mcdc,
#                   printCallbackStruct)
#
# Write formatted source code line. Return a line in a format as needed
# by gen_png()
#

sub write_source_line(*$$$$$$$)
{
    my ($handle, $srcline, $count, $showBranches, $brdata, $showMcdc, $mcdc,
        $cbdata)
        = @_;
    my $line         = $cbdata->lineNo();
    my $fileCovInfo  = $cbdata->lineData();
    my $sourceDetail = $cbdata->sourceDetail();
    my $source       = $srcline->text();
    my $src_owner    = $srcline->owner();
    my $src_age      = $srcline->age();
    my $source_format;
    my $count_format;
    my $result;
    my $anchor_start      = "";
    my $anchor_end        = "";
    my $count_field_width = $line_field_width - 1;
    my $html;
    my $tla;
    my $base_count;
    my $curr_count;
    my $bucket;

    my @prevData = ($cbdata->current('tla'),
                    $cbdata->current('owner'),
                    $cbdata->current('age'));

    # Get branch HTML data for this line
    my @br_html;
    @br_html = get_branch_html($brdata, $cbdata) if ($showBranches);

    my @mcdc_html;
    @mcdc_html = get_mcdc_html($mcdc, $cbdata) if ($showMcdc);

    my $thisline = $fileCovInfo->lineMap()->{$line};
    my $tlaIsHref;
    if (!defined($thisline) ||
        (!defined($count) &&
            !($thisline->in_base() || $thisline->in_curr()))
    ) {
        $result        = "";
        $source_format = "";
        $count_format  = " " x $count_field_width;
        $tla           = $cbdata->tla(undef, $line);
    } else {
        $base_count = $thisline->base_count();
        $curr_count = $thisline->curr_count();
        $bucket     = $thisline->tla();
        # use callback data to keep track of the most recently seen TLA -
        #   $tla is either "   " (3 spaces) if same as previous or if no TLA
        #   we just stick "TLA " (4 characters) into the fixed-with source
        #   line - right before the count.
        $tla = $cbdata->tla($bucket, $line);
        my $class = "tla$bucket";
        if ($main::show_tla && $tla =~ /$bucket/) {
            # maybe we want to link only the uncovered code categories?
            my $next = $sourceDetail->nextTlaGroup($bucket, $line);
            $tlaIsHref = 1;
            # if no next segment in this category - then go to top to page.
            $next = defined($next) ? "L$next" : "top";
            my $anchorClass = "";
            if ($bucket ne 'UNK') {
                $class .= " tlaBg$bucket";
                $anchorClass = " class=\"tlaBg$bucket\" ";
            }
            my $label =
                $main::use_legacyLabels ?
                $SummaryInfo::tlaToLegacySrcLabel{$bucket} :
                $bucket;
            my $popup = " title=\"Next $label group\"";
            $label .= ' ' x ($main::tla_field_width - length($label));
            $tla = "$label";
        }
        $source_format = "";

        my $pchar;
        if (exists($lcovutil::pngChar{$bucket})) {
            $pchar = $lcovutil::pngChar{$bucket};
        } else {
            lcovutil::ignorable_error($lcovutil::ERROR_UNKNOWN_CATEGORY,
                                      "unexpected TLA '$bucket'");
            $pchar = '';
            $count = 0 unless defined($count);
        }
        if ($bucket eq "ECB" ||
            $bucket eq "EUB") {
            !defined($count) or
                die("excluded code should have undefined count");
            # show previous count for excluded code that had been hit
            if ($bucket eq "ECB") {
                $count_format = format_count(-$base_count, $count_field_width);
            } else {
                $count_format = " " x $count_field_width;
            }
            $result = $pchar . $base_count;
        } else {
            if (!defined($count)) {
                # this is manufactured data...make something up
                $count = $curr_count;
            }
            defined($count) && "" ne $count or
                die("code should have defined count");
            if ($bucket eq 'LBC') {
                $count_format = format_count(-$base_count, $count_field_width);
            } else {
                $count_format = format_count($count, $count_field_width);
            }
            $result = $pchar . $count;
        }
        # '$result' is used to generate the PNG frame.
        # Only build the (per-source-line) diagnostic string when verbosity is
        # actually high enough to print it -- info() would otherwise discard
        # the freshly-concatenated string on every single line.
        info(2,
             "    $bucket: line=$line " .
                 (defined($count) ? "count= $count " : "") .
                 ' curr=' .
                 (defined($curr_count) ? $curr_count : '-') .
                 ' base=' .
                 (defined($base_count) ? $base_count : '-') . "\n"
        ) if ($lcovutil::verbose >= 2);
    }
    $result .= ":" . $source;

    # a coverpoint of a multi-line statement has the date and owner of the
    #   statement - see 'SourceFile::spanAnnotation' - which is where it is
    #   counted, in '_countLineTlaData':  the statement's own line coverpoint,
    #   and a baseline one on a later line of it, which the current data merged
    #   into it
    my $attribution;
    my ($stmtFirst, $stmtLast) = $fileCovInfo->statement($line);
    if (defined($stmtFirst) &&
        defined($bucket) &&
        ($SummaryInfo::tlaLocation{$bucket} & 0x1)) {
        $attribution = $sourceDetail->attribution($line);
        if ($attribution == $srcline) {
            $attribution = undef;
        } else {
            $src_owner = $attribution->owner();
            $src_age   = $attribution->age();
        }
    }

    my $tooltip = '';
    if (@SourceFile::annotateScript &&
        $SourceFile::annotateTooltip ne '') {
        my $format = sub {
            my $lineData = shift;
            my $commitId = $lineData->commit();
            return undef
                unless (defined($commitId) &&
                        $commitId ne 'NONE');
            my $text = $SourceFile::annotateTooltip;
            my $date = $lineData->date();
            $date =~ s/(T.+)$//;    # just the year/month/day part
            foreach my $p (['%U', $lineData->owner()],
                           ['%F', $lineData->full_name()],
                           ['%D', $lineData->date()],
                           ['%d', $date],
                           ['%A', $lineData->age()],
                           ['%C', $commitId],
                           ['%l', $lineData->line()],
            ) {
                # escape the substituted value, not the assembled tooltip:  the
                #   template is the user's own and may legitimately contain
                #   markup, but the annotation fields come from an external
                #   script - and a '"' in an author's full name terminates the
                #   attribute this ends up inside, while a bare '&' is an
                #   undefined entity reference.  The owner column, a hundred
                #   lines below, escapes the same value already
                my $value = escape_html($p->[1]);
                $text =~ s/$p->[0]/$value/g;
            }
            return $text;
        };
        my $own = &$format($sourceDetail->line($line));
        if (defined($attribution)) {
            # the statement's attribution, and then the line's own
            my $stmt = &$format($attribution);
            $own =
                (defined($stmt) ? $stmt : '') .
                " (statement $stmtFirst-$stmtLast)" .
                (defined($own) ? "
$own" : '');
        }
        $tooltip = " title=\"$own\"" if defined($own);
    }
    # the decorated source text of this line, computed here because the line
    #   number cell is written before the source text is
    my $callgraphSource = callgraphDecorateSource($source, $cbdata, $line)
        if $lcovutil::callgraph_coverage;

    # Write out a line number navigation anchor every $nav_resolution
    # lines if necessary
    $anchor_start = "";
    $anchor_end   = "";

    # *************************************************************

    $html = $anchor_start;
    # we want to colorize the date/owner part of un-hit lines only
    my $html_continuation_leader = "";    # for continued lines
    if (defined($main::show_dateBins) &&
        $sourceDetail->isProjectFile()) {
        DATE_SECTION: {
            my $ageLen   = $main::age_field_width;
            my $ownerLen = $main::owner_field_width;

            # need to space over on continuation lines
            $html_continuation_leader = ' ' x ($ageLen + $ownerLen + 2);

            if (!defined($count) &&
                (!defined($main::show_nonCodeOwners) ||
                    0 == $main::show_nonCodeOwners) &&
                (!defined($bucket) ||
                    !exists($SummaryInfo::excludedTla{$bucket}))
            ) {
                # suppress date- and owner entry on non-code lines
                $html .= $html_continuation_leader;
                last DATE_SECTION;
            }

            my $span    = "";
            my $endspan = "";
            my $bgclass = "";
            if (defined($count) && 0 == $count) {
                # probably want to pick a color based on which
                #   date bin it is in.
                # right now, picking based on TLA.
                $bgclass = " class=\"tlaBg$bucket\""
                    if (defined($bucket) &&
                        !exists($SummaryInfo::excludedTla{$bucket}));

                #$html .= "";
                if ("" ne $source_format) {
                    OWNER: {
                        if (!defined($src_owner) || !defined($src_age)) {
                            # maybe this should be a different error type?
                            lcovutil::ignorable_error(
                                      $lcovutil::ERROR_UNMAPPED_LINE,
                                      "undefined owner/age for $bucket $line " .
                                          $sourceDetail->path());
                            last OWNER;
                        }
                        # add a 'title="tooltip"' popup - to give owner, date, etc
                        #   (the owner name is escaped:  see the per-line tooltip)
                        my $who = escape_html($src_owner);
                        my $title =
                            'span title="' . $who . " $src_age days ago";
                        if (defined($main::show_dateBins)) {
                            my $bin = SummaryInfo::findAgeBin($src_age);
                            $title .=
                                " (bin $bin: " .
                                $SummaryInfo::ageGroupHeader[$bin] . ")";
                        }
                        $title .= "\"";
                        $span = $source_format;
                        $span =~ s/span/$title/;
                        $endspan = "";
                    }
                }
            }    # OWNER block

            # determine if this line is going to be the target of a 'date', 'owner'
            # or TLA navigation href.
            #  - it is possible for any of these to have changed from the previous
            #    line, even if the others are unchanged:
            #      - same TLA but different author
            #      - same author but different date bin, .. and so on
            # If it is a leader, then we need to insert the 'owner' and/or 'date'
            #   link to go to the next group in this bin - even if the owner or
            #   date bin has not changed from the previous line)
            my $tlaChanged = defined($bucket) && $prevData[0] ne $bucket;

            my $needOwnerHref = @SourceFile::annotateScript &&
                ($tlaChanged ||
                 (defined($bucket) && $prevData[1] ne $src_owner));

            my $newBin = SummaryInfo::findAgeBin($src_age);
            defined($prevData[2]) || $line == 1 or
                die("unexpected uninitialized age");

            my $needDateHref = (
                           $tlaChanged || (defined($bucket) &&
                               SummaryInfo::findAgeBin($prevData[2]) != $newBin)
            );

            if ($needDateHref) {
                my $matchLine = $cbdata->nextDate($newBin, $bucket);
                $needDateHref = 0
                    if (defined($matchLine) && $matchLine != $line);
            }
            if ($needOwnerHref) {
                # slightly complicated logic:
                #   - there can be non-code lines owned by someone else
                #     between code lines owned by '$src_owner', such that the
                #     all the code lines have the same TLA.
                # In that case, we just insert an href at the top of the
                # block to take us past all of them - to the next code block
                # owned by $src_owner with this TLA, which separated by at least
                # one line of code either owned by someone else, or with a different
                # TLA.
                my $matchLine = $cbdata->nextOwner($src_owner, $bucket);
                # don't insert the owner href if this isn't the line we wanted
                $needOwnerHref = 0
                    if (defined($matchLine) && $matchLine != $line);
            }

            my $age   = $cbdata->age($src_age, $line);
            my $owner = $cbdata->owner($src_owner);

            # this HTML block turns into
            #   "int name " <- note trailing space
            #  .. but the age and name might be hrefs

            $html .= $span;
            # then 5 characters of 'age' (might be empty)
            if ($needDateHref) {
                # next line with this TLA, in this date bin
                my $next =
                    $sourceDetail->nextInDateBin($newBin, $bucket, $line);
                $cbdata->nextDate($newBin, $bucket, $next);

                $next = defined($next) ? "L$next" : "top";
                my $dateBin = $SummaryInfo::ageGroupHeader[$newBin];
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$bucket} :
                    $bucket;
                my $popup =
                    " title=\"next $label in “$dateBin” bin\"";
                $html .= ((' ' x ($ageLen - length($src_age))) .
                          "$src_age ");
            } else {
                $html .= sprintf("%${ageLen}s ", $age);
            }

            if ($needOwnerHref) {
                # next line with this TLA, by this owner..
                my $next =
                    $sourceDetail->nextInOwnerBin($src_owner, $bucket, $line);
                $cbdata->nextOwner($src_owner, $bucket, $next);
                $next = defined($next) ? "L$next" : "top";
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$bucket} :
                    $bucket;
                my $popup =
                    " title=\"next $label in “" .
                    escape_html($src_owner) .
                    "” bin\"";
                # NOTE:  see note below about firefox nested span bug.
                #  this code just arranges to wrap an explicit 'span' around
                #  the space.
                my $nspace = $ownerLen - length($src_owner);
                my $space  = $nspace > 0 ? (' ' x $nspace) : '';
                my $href   = "" .
                    escape_html(substr($src_owner, 0, $ownerLen)) . '';
                if ('' ne $bgclass &&
                    '' ne $space) {
                    $html .= "$endspan$href$span";
                } else {
                    $html .= $href;
                }
                $html .= $space;
            } else {
                # pad first, then escape:  the padding is a column width in
                #   characters, and escaping makes the string longer
                $html .= escape_html(sprintf("%-${ownerLen}s", $owner));
            }
            $html .= $endspan . ' ';    # add trailing space
        }
    }    # DATE_SECTION

    # use a different HTML tag if there are deleted lines here
    my $deletedLines = $fileCovInfo->deletedLineData($line);
    my $lineNumTag   = 'lineNum';
    my $lineNumTitle = '';
    if (defined($deletedLines)) {
        my $first = $deletedLines->[0];
        my $last  = $deletedLines->[1];
        die("invalid deleted lines array") unless $first <= $last;
        my $count = $last - $first + 1;
        $lineNumTitle = ' title="'
            .
            (($first != $last) ?
                 "removed $count lines ([$first:$last] inclusive)" :
                 "removed line $first") .
            ' from baseline version"';
        $lineNumTag = 'lineNumWithDelete';
    }
    my $lineNumSpan = "";

    my $lineNumText = sprintf('%8d', $line);
    $html .= "$lineNumSpan$lineNumText ";
    #printf("tla= " . $tla);
    #printf("html= " . $html);

    $html .= shift(@br_html) . ":" if ($showBranches);
    $html .= shift(@mcdc_html) . ":" if ($showMcdc);

    $tla = ""
        if (!defined($main::show_tla));

    # NOTE: the 'html2lcov' tool reverses this rendering to recover the original
    #   source.  It relies on the row layout emitted below:  a fixed-width gutter
    #   (line-number, optional branch/MC/DC, and hit-count columns) followed by a
    #   ' : ' (space-colon-space) separator and then the verbatim source text.
    #   The gutter width is user-controllable (via the field-width options), so
    #   html2lcov does not hard-code it;  instead it locates the separator as the
    #   single ' : ' offset common to every row of the page.  That works only so
    #   long as ' : ' is the LAST thing before the source and appears nowhere
    #   else in the gutter -- the branch/MC/DC columns end with a bare ':' (no
    #   leading space) and the count column is right-justified digits/spaces.
    #   The source itself may legitimately contain ' : ' (a C/C++/Verilog/Perl
    #   ternary 'a ? b : c', a ':' label, a base-class list), which is fine
    #   because it is always to the RIGHT of this separator.  Preserve this
    #   property if you change the layout, or html2lcov will mis-recover source.
    die(  "html2lcov depends on ' : ' separating the hit-count column from the "
        . "source; hit-count field '$count_format' must not contain it")
        if (index($count_format, ' : ') >= 0);

    # 'source_format is the colorization, then the 3-letter TLA,
    #    then the hit count, then the source line
    if ($tlaIsHref) {
        # there seems to be a bug in firefox:
        #      link whatever
        #   renders as if the 'span' didn't exist (so the colorization of the
        #   link end - and the rest of the line doesn't pick up attributes
        #   from class 'foo'.
        # If I emit it as:
        #     link whatever
        #   (i.e., do not nest anchor inside the span) - then it works
        $html .= "$tla$source_format $count_format : ";
    } else {
        $html .= "$source_format$tla $count_format : ";
    }

    # a later line of a multi-line statement has no coverpoint of its own:
    #   its source text has the color of the statement's - but its count,
    #   category and date/owner columns stay empty
    my $continuation;
    if (!$source_format &&
        defined($stmtFirst) &&
        $stmtFirst < $line) {
        my $leader = $fileCovInfo->lineMap()->{$stmtFirst};
        $continuation = $leader->tla()
            if (defined($leader) && $leader->in_curr());
    }
    $html .= "" if defined($continuation);
    $html .=
        defined($callgraphSource) ? $callgraphSource : escape_html($source);
    $html .= "" if ($source_format || defined($continuation));
    $html .= $anchor_end . "\n";

    write_html($handle, $html);

    # Add lines for overlong branch and/or MC/DC information
    while (@br_html || @mcdc_html) {
        my $br = @br_html ? shift(@br_html) : '';
        my $mc = @mcdc_html ? shift(@mcdc_html) : '';
        if ($mc) {
            # space over far enough to line up with MC/DC extension column
            # remove the span and other HTML
            my $s = $br;
            foreach my $tag ('a', 'span') {
                $s =~ s/(<\/$tag>|<$tag.+?>)//g;
            }
            $br .= ' ' x ($br_field_width - length($s)) . ' ';
            die("unexpected lengths") unless $br_field_width >= length($s);
        }
        write_html($handle,
                   "$html_continuation_leader$lineNumSpan" .
                       ' ' x 8 .
                       " $br$mc\n");
    }
    # *************************************************************

    return ($result);
}

#
# write_source_epilog(filehandle)
#
# Write end of source code table.
#

sub write_source_epilog(*)
{
    # *************************************************************

    write_html($_[0], <
              
            
          
          
END_OF_HTML # ************************************************************* } # # write_html_epilog(filehandle, base_dir[, break_frames]) # # Write HTML page footer to FILEHANDLE. BREAK_FRAMES should be set when # this page is embedded in a frameset, clicking the URL link will then # break this frameset. # sub write_html_epilog(*$;$) { my $basedir = $_[1]; my $break_code = ""; my $epilog; if (defined($_[2])) { $break_code = " target=\"_parent\""; } my $f = defined($main::footer) ? $footer : "Generated by: $lcov_version"; # ************************************************************* write_html($_[0], < $f
END_OF_HTML $epilog = $html_epilog; $epilog =~ s/\@basedir\@/$basedir/g; write_html($_[0], $epilog); } # # write_frameset(filehandle, basedir, basename, pagetitle) # # 'basename' names the two pages this one frames - need the same name everywhere # sub write_frameset(*$$$) { my $frame_width = $overview_width + 40; # ************************************************************* write_html($_[0], < $_[3] <center>Frames not supported by your browser!<br></center> END_OF_HTML # ************************************************************* } # # sub write_overview_line(filehandle, basename, line, link) # # sub write_overview_line(*$$$) { my $y1 = $_[2] - 1; my $y2 = $y1 + $nav_resolution - 1; my $x2 = $overview_width - 1; # ************************************************************* write_html($_[0], < END_OF_HTML # ************************************************************* } # # write_overview(filehandle, basedir, basename, pagetitle, lines) # # 'basename' names the source page and the overview image this one links to - # see 'write_frameset' # sub write_overview(*$$$$) { my $index; my $max_line = $_[4] - 1; my $offset; # ************************************************************* write_html($_[0], < $_[3] END_OF_HTML # ************************************************************* # Make $offset the next higher multiple of $nav_resolution $offset = ($nav_offset + $nav_resolution - 1) / $nav_resolution; $offset = sprintf("%d", $offset) * $nav_resolution; # Create image map for overview image for ($index = 1; $index <= $_[4]; $index += $nav_resolution) { # Enforce nav_offset if ($index < $offset + 1) { write_overview_line($_[0], $_[2], $index, 1); } else { write_overview_line($_[0], $_[2], $index, $index - $offset); } } # ************************************************************* write_html($_[0], <
Top

Overview
END_OF_HTML # ************************************************************* } sub max($$) { my ($a, $b) = @_; return $a if ($a > $b); return $b; } sub sortedByCovType { # the version of the directory page '$page' - 'index-date.html', say - # which is sorted by '$covType', and so has that type's columns # leftmost, should be the target of the 'covtype' summary table link - # as long as that page exists (i.e., was not suppressed because it # is empty - see - see 'write_dir_page' - and '$page' itself if not my ($page, $summary, $covType) = @_; my %sortType = (SummaryInfo::LINE_DATA, $SORT_LINE, SummaryInfo::FUNCTION_DATA, $SORT_FUNC, SummaryInfo::BRANCH_DATA, $SORT_BRANCH, SummaryInfo::MCDC_DATA, $SORT_MCDC, SummaryInfo::CALLGRAPH_DATA, $SORT_CALLGRAPH); my $sort = $sortType{$covType}; return $page unless 1 < scalar($summary->sources()) && grep({ $_ == $sort } @main::fileview_sortlist); $page =~ s/\.\Q$html_ext\E$/$main::fileview_sortname[$sort].$html_ext/; return $page; } sub buildDateSummaryTable($$$$$$$$$) { my ($summary, $covType, $covCountCallback, $fileDetail, $nextLocationCallback, $title, $detailLink, $numRows, $activeTlaList) = @_; $detailLink = sortedByCovType($detailLink, $summary, $covType) if defined($detailLink); $title = "$title" if (defined($detailLink)); my @table; my @dateSummary = [undef, #width "subTableHeader", # class $title, # text $numRows, # colspan ]; # only insert the label if there is data my $first = 1; my $page = $detailLink; if (defined($page)) { $page =~ s/^index/index-bin_/; $page =~ s/^index-bin_-/index-bin_/; } my ($lim_med, $lim_high) = covLimits($covType); for (my $bin = 0; $bin <= $#SummaryInfo::ageGroupHeader; ++$bin) { my $ageval = $summary->age_sample($bin); my $found = &$covCountCallback($summary, "found", "age", $ageval); next if 0 == $found; my $hit = &$covCountCallback($summary, "hit", "age", $ageval); my $style = $rate_name[classify_rate($found, $hit, $lim_med, $lim_high)]; my $rate = rate($hit, $found, " %"); my $href = $SummaryInfo::ageGroupHeader[$bin]; if (defined($detailLink)) { $href = "$href"; } $hit -= $found # negative number if ($main::opt_missed); my @dataRow = ([undef, "headerItem", $href . ":"], [undef, "headerCovTableEntry$style", $rate], [undef, "headerCovTableEntry", $found]); if ($main::show_hitTotalCol) { push(@dataRow, [undef, "headerCovTableEntry", $hit]); } if ($main::show_tla) { for my $tla (@$activeTlaList) { my $value = &$covCountCallback($summary, $tla, "age", $ageval); my $class = !$main::use_legacyLabels && 0 != $value && exists($SummaryInfo::concerningTla{$tla}) ? "tla$tla" : "headerCovTableEntry"; # suppress zeros - make table less busy/easier to read if ("0" eq $value) { $value = ""; } elsif (!$main::no_sourceview && defined($fileDetail) && defined($nextLocationCallback) && !exists($SummaryInfo::deletedTla{$tla})) { # no link for deleted lines (DUB, DCB) because there # is no TLA for those categories at the location in # the 'current' sourceview. That location is just a # normal line - maybe "not code", maybe 'CBC' or some # other TLA. # link to first entry my $firstAppearance = &$nextLocationCallback($fileDetail, $bin, $tla); defined($firstAppearance) || !$fileDetail->isa('SourceFile') or die( "$tla: unexpected date bin $bin undef appearance for " . $fileDetail->path()); my $dateBin = $SummaryInfo::ageGroupHeader[$bin]; my ($label, $color); if ($main::use_legacyLabels) { $label = $SummaryInfo::tlaToLegacy{$tla}; $color = ""; } else { $label = $tla; $color = " class=\"tlaBg$tla\""; } my $popup = " title=\"goto first $label " . SummaryInfo::type2coverpoint($covType) . " in “$dateBin” bin\""; $value = '$value" if defined($firstAppearance); } push(@dataRow, [undef, $class, $value]); } } if ($first) { push(@table, \@dateSummary); $first = 0; } push(@table, \@dataRow); } return \@table; } sub buildOwnerSummaryTable($$$$$$$$$$) { my ($ownerList, $num_truncated, $summary, $covType, $fileDetail, $nextLocationCallback, $title, $detailLink, $numRows, $activeTlaList) = @_; $detailLink = sortedByCovType($detailLink, $summary, $covType) if defined($detailLink); $title .= " (containing " . ($main::show_ownerBins ? "" : "un-exercised ") . "code)"; $title .= ": $num_truncated author" . ($num_truncated == 1 ? '' : 's') . ' truncated' if $num_truncated; $title = "$title" if (defined($detailLink)); my $page = $detailLink; if (defined($page)) { $page =~ s/^index/index-bin_/; $page =~ s/^index-bin_-/index-bin_/; } my @table; my @ownerSummary = [undef, #width "subTableHeader", # class $title, # text $numRows, # colspan ]; my ($lim_med, $lim_high) = covLimits($covType); my $first = 1; # owners are sorted from most uncovered lines to least foreach my $od (@$ownerList) { # keyed by coverage type - see 'SummaryInfo::findOwnerList' my ($name, $bin) = @$od; my ($missed, $found) = @{$bin->{$covType}}; # only put user in table if they are responsible for at least one point next if $found == 0 or ($missed == 0 && $main::show_ownerBins ne 'all'); if ($first) { $first = 0; push(@table, \@ownerSummary); } my $hit = $found - $missed; my $style = $rate_name[classify_rate($found, $hit, $lim_med, $lim_high)]; my $rate = rate($hit, $found, ' %'); my $esc_name = escape_html($name); my $href = $esc_name; if (defined($detailLink)) { $href = "$esc_name"; } $hit -= $found # negative number if ($main::opt_missed); my @dataRow = ([undef, "headerItem", $href . ":"], [undef, "owner_coverPer$style", $rate], [undef, "ownerTla", $found]); if ($main::show_hitTotalCol) { push(@dataRow, [undef, "ownerTla", $hit]); } if ($main::show_tla) { for my $tla (@$activeTlaList) { my $value = $summary->owner_tlaCount($name, $tla, $covType); # suppress zeros - make table less busy/easier to read my $class = !$main::use_legacyLabels && 0 != $value && exists($SummaryInfo::concerningTla{$tla}) ? "tla$tla" : "ownerTla"; if ("0" eq $value) { $value = ""; } elsif (!$main::no_sourceview && defined($fileDetail) && defined($nextLocationCallback)) { my $firstAppearance = &$nextLocationCallback($fileDetail, $name, $tla); defined($firstAppearance) || !$fileDetail->isa('SourceFile') or die( "$tla: unexpected owner $name undef appearance for " . $fileDetail->path()); my ($label, $color); if ($main::use_legacyLabels) { $label = $SummaryInfo::tlaToLegacy{$tla}; $color = ""; } else { $label = $tla; $color = " class=\"tlaBg$tla\""; } my $popup = " title=\"goto first $label " . SummaryInfo::type2coverpoint($covType) . " in “$name” bin\""; $value = '$value" if defined($firstAppearance); } push(@dataRow, [undef, $class, $value]); } } push(@table, \@dataRow); } return \@table; } sub buildHeaderSummaryTableRow { my ($summary, $covType, $fileDetail, $nextLocationCallback, $activeTlaList) = @_; $fileDetail = undef if (!$main::show_tla && (defined($fileDetail) && !$fileDetail->isProjectFile())); my @row; for my $tla (@$activeTlaList) { my $value = $summary->get($tla, $covType); # for the moment, no background colorization for non-zero counts # in "concerning" categories. The table header is immediately # above - so no need to colorize to draw emphasis - just makes the # table busier my $showConcerningTla = 0; my $class = $showConcerningTla && !$main::use_legacyLabels && 0 != $value && exists($SummaryInfo::concerningTla{$tla}) ? "tla$tla" : "headerCovTableEntry"; if ("0" eq $value) { # suppress zeros - make table less busy/easier to read $value = ""; } elsif (!$main::no_sourceview && defined($fileDetail) && !exists($SummaryInfo::deletedTla{$tla})) { # deleted lines don't appear.. my $firstAppearance = &$nextLocationCallback($fileDetail, $tla); defined($firstAppearance) || !$fileDetail->isa('SourceFile') or die( "$tla: unexpected undef appearance for " . $fileDetail->path()); my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} : $tla; my $popup = " title=\"goto first $label " . SummaryInfo::type2coverpoint($covType) . "\""; $value = '$value" if defined($firstAppearance); } push(@row, [undef, $class, $value]); } return @row; } # build an HTML string for the directory or file pathname, such # that each element is clickable - and takes you to the 'index' file # in the corresponding (transitive) parent directory sub build_html_path($$$$$;$) { # '$sort' is the sort suffix of the parent views to link to my ($path, $key, $bin_type, $isFile, $isAbsolute, $sort) = @_; $path =~ s|^/||; my @path = File::Spec->splitdir($path); my $html_path = ""; if ($main::hierarchical && scalar(@path) > 1) { pop(@path); # remove 'self' - at the tail my $p = ""; my $sep = ""; # need one fewer "../" entries for file pathname because the # index file we are looking for is in the current directory (i.e., # not '../index.html' my $len = scalar(@path) - $isFile; $sort //= ''; foreach my $elem (@path) { my $base = "../" x $len; $elem = $lcovutil::dirseparator . $elem if $isAbsolute; $isAbsolute = 0; my $e = escape_html($elem); $html_path .= "$sep$e"; $sep = $lcovutil::dirseparator; --$len; } } return $html_path; } # # write_header(filehandle, ctrl, trunc_file_name, rel_file_name, # summaryInfo, optionalFileDetailInfo, optionalFunctionData, # optionalCallgraphPages) # ctrl = (type, primary_key, sort_type, bin_type) # # Write a complete standard page header. TYPE may be (0, 1, 2, 3, 4, 5, 6) # corresponding to (directory view header, file view header, source view # header, test case description header, function view header, callers view # header, callees view header) # # 'callgraphPages' says which of the call site pages of this file exist, and in # which orders - see 'callgraphPageSorts'. Only the four file-level headers use # it. # # bin_type in (undef, "", "-owner", "-date") # - if 'bin' is set, then create link to 'vanilla' view of self, and # to corresponding view of parent # - i.e., from 'owner detail' directory page to "owner detail" # toplevel, and to my corresponding vanilla directory page. # return hash of coverType -> list of non-zero TLAs for that type (or to # list of all TLA types if user has asked not to suppress all-zero # columns) sub write_header(*$$$$$$$;$) { local *HTML_HANDLE = shift; # '$callgraphPages' says which of the call site pages of this file exist, and # in which orders, indexed by CALLGRAPH_VIEW_: a file has a page for a # direction only when some function of it has something to say in that # direction, and a link strip which names a page that was never written is # worse than no link my ($callback_type, $ctrl, $trunc_name, $rel_filename, $summary, $fileDetail, $differentialFunctionMap, $callgraphPages) = @_; my ($type, $primary_key, $sort_type, $bin_type) = @$ctrl; my $base_dir; my $view; my @row_left; my @row_right; my $esc_trunc_name = escape_html($trunc_name); # Where the callgraph numbers of the header lead, which is not where the rest # of them do on a call site page. There is no source view below that # header, so none of the others leads anywhere; the callgraph ones lead # into the table below it, which lists what they count, and '$fileDetail' # is then that table's index - see CallgraphPageIndex. On the callers page # the numbers are not the file's own either: they count the calls into it # (see 'withCallgraphDestinations'), so their labels say so - as do those # of every other page, which count the calls made from what it shows my $callgraphDetail = $fileDetail; my @callgraphLocators = (\&SourceFile::nextCallgraphTlaGroup, \&SourceFile::nextCallgraphInDateBin, \&SourceFile::nextCallgraphInOwnerBin); my $callgraphTitle = $type == $HDR_CALLERS ? $CALLS_INTO_TITLE : sprintf($CALLS_FROM_TITLE, {top => 'project', directory => 'directory'}->{$summary->type()} // 'file'); if (isCallgraphHeader($type)) { $fileDetail = undef; @callgraphLocators = (\&CallgraphPageIndex::firstTlaGroup, \&CallgraphPageIndex::firstInDateBin, \&CallgraphPageIndex::firstInOwnerBin); } my $callgraphLabel = [undef, "headerItem", "Callgraph:", undef, $callgraphTitle]; my $callgraphDateLabel = _headerTitle("Callgraph coverage date bins:", $callgraphTitle); my $callgraphOwnerLabel = _headerTitle("Callgraph coverage ownership bins", $callgraphTitle); $bin_type = "" unless defined($bin_type); my $base_name = File::Basename::basename($rel_filename); my $show_dateBins = $main::show_dateBins && (!defined($fileDetail) || $fileDetail->isProjectFile()); my $key = $primary_key ne "name" ? "-bin_$primary_key" : ""; my $isAbsolutePath = ( $summary->is_directory(1) || ($summary->type() eq 'file' && $summary->parent()->is_directory(1)) ); # A directory page leads up to the view of each parent which is in the same # order. The parents are written later and might find that their table # has just a single entry (so page is suppressed). In that case, we go # back and munge the child page links. See 'unsortParentLinks' my $parentSort = ''; $parentSort = $main::fileview_sortname[$sort_type] // '' if $type == $HDR_FILE && defined($sort_type); my $html_path = build_html_path($trunc_name, $key, $bin_type, $summary->type() eq 'file', $isAbsolutePath, $parentSort); # Prepare text for "current view" field if ($type == $HDR_DIR) { # Main overview $base_dir = ""; if ($bin_type ne "" || $primary_key ne 'name') { # this is the header of the 'top-level' page, for either 'owner' # or 'date' binning - link back to vanilla top-level page $view = "$overview_title"; } else { $view = $overview_title; } } elsif ($type == $HDR_FILE) { # Directory overview $base_dir = get_relative_base_path($rel_filename); my $self_link; if ($main::hierarchical) { my $base = escape_html(File::Basename::basename($rel_filename)); if ($base eq $rel_filename && $isAbsolutePath) { $base = $lcovutil::dirseparator . $base; } $self_link = $html_path; $self_link .= $lcovutil::dirseparator if ('' ne $html_path); if ('name' ne $primary_key || '' ne $bin_type) { $self_link .= "$base"; } else { $self_link .= $base; } } else { $esc_trunc_name = $lcovutil::dirseparator . $esc_trunc_name if $isAbsolutePath; $self_link = $esc_trunc_name; if ('name' ne $primary_key || '' ne $bin_type) { # go back to the 'vanilla' view of this directory $self_link = "$esc_trunc_name"; } } $view = "" . "$overview_title - $self_link"; } elsif ($type == $HDR_SOURCE || $type == $HDR_FUNC || isCallgraphHeader($type)) { # File view my $dir_name = dirname($rel_filename); my $esc_base_name = escape_html($base_name); my $esc_dir_name = escape_html($dir_name); $esc_dir_name = $lcovutil::dirseparator . $esc_dir_name if $isAbsolutePath; $base_dir = get_relative_base_path($dir_name); # if using frames, to break frameset when clicking any of the links my $parent = $frames ? " target=\"_parent\"" : ""; $view = "" . "$overview_title - "; # The name of the file is the way back to its source view, so that the # link strip after it has only the detail pages to name. Plain text on # the source page itself, which is where it would lead my $baseLink = $esc_base_name; $baseLink = '$esc_base_name" if $type != $HDR_SOURCE; if ($main::flat) { # for flat view, point only to the top-level and show path # to this file. No other links $view .= File::Spec->catfile($esc_dir_name, $baseLink); } else { if ($main::hierarchical) { $html_path =~ s/$esc_dir_name"; } $view .= " - $baseLink"; } # ..and the detail pages of this file, the one being written named # without a link. A call site page exists only where the file has # callgraph data to put on it. Don't create the page or the hyperlink # if there isn't any if ($lcovutil::func_coverage && defined($differentialFunctionMap) && %$differentialFunctionMap) { my @links; if ($type == $HDR_FUNC) { push(@links, 'functions'); } else { my $suffix = $sort_tables ? '-c' : ''; push(@links, 'functions" ); } # ..one for each direction of the callgraph which this file has # anything to say in. A reader already on one of these pages chose # an order, so the link to the other direction keeps it for (my $dir = 0; $dir < CALLGRAPH_VIEW_COUNT; ++$dir) { next unless defined($callgraphPages) && $callgraphPages->[$dir]; my $name = $callgraphView[$dir]->{name}; if ($type == $callgraphView[$dir]->{hdr}) { push(@links, $name); } else { push( @links, '$name" ); } } $view .= ' (' . join(' / ', @links) . ')'; } } elsif ($type == $HDR_TESTDESC) { # Test description header $base_dir = ""; $view = "" . "$overview_title - test case descriptions"; } # Prepare text for "test" field my $test = escape_html($test_title); # Append link to test description page if available if (%test_description && ($type != $HDR_TESTDESC)) { if ($frames && ($type == $HDR_SOURCE || $type == $HDR_FUNC || isCallgraphHeader($type)) ) { # Need to break frameset when clicking this link $test .= " ( " . "" . "view descriptions )"; } else { $test .= " ( " . "" . "view descriptions )"; } } # Write header write_header_prolog(*HTML_HANDLE, $base_dir); # Left row push(@row_left, [["10%", "headerItem", "Current view:"], ["10%", "headerValue", $view] ]); my $label = defined($baseline_title) ? "Current" : "Test"; push(@row_left, [[undef, "headerItem", "$label:"], [undef, "headerValue", $test]]); push(@row_left, [[undef, "headerItem", "$label Date:"], [undef, "headerValue", $current_date] ]); if (defined($baseline_title)) { push(@row_left, [[undef, "headerItem", "Baseline:"], [undef, "headerValue", $baseline_title] ]); push(@row_left, [[undef, "headerItem", "Baseline Date:"], [undef, "headerValue", $baseline_date] ]); } my $hasBranchData = $lcovutil::br_coverage && 0 != $summary->branch_found(); my $hasMcdcData = $lcovutil::mcdc_coverage && 0 != $summary->mcdc_found(); my $hasCallgraphData = $lcovutil::callgraph_coverage && 0 != $summary->callgraph_found(); # if top-level page, link to command line and profile if ($summary->type() eq 'top' && $bin_type eq '' && $primary_key eq 'name' && $sort_type eq $main::SORT_FILE && defined($lcovutil::profile)) { push(@row_left, [[undef, 'headerItem', 'genhtml Process:'], [undef, 'headerValue', 'command line' ] ], [[undef, 'headerItem', ''], #blank [undef, 'headerValue', 'profile data' ] ]); } if ($type != $HDR_SOURCE && $type != $HDR_FUNC && !isCallgraphHeader($type) && (defined($main::show_ownerBins) || @SourceFile::annotateScript) ) { # we are going to have 3 versions of the page: # flat, with owner bin data, with date bin data # so label which one this is my ($tableLabel, $tableKey); if ($bin_type eq '-owner') { $tableLabel = 'Group by:'; $tableKey = 'Owner'; } elsif ($bin_type eq '-date') { $tableLabel = 'Group by:'; $tableKey = 'Date bin'; } else { $bin_type eq "" or die("unexpected bin detail type $bin_type"); $tableLabel = 'Summarize by:'; if ($primary_key eq 'date') { $tableKey = 'Date bin'; } elsif ($primary_key eq 'owner') { $tableKey = 'Owner'; } # else ($primary_key eq 'name'; } # label this only if there is both a primary and secondary push(@row_left, [[undef, 'headerItem', $tableLabel], [undef, 'headerValue', $tableKey] ]) if (defined($tableKey)); } # Right row if ($legend && ($type == $HDR_SOURCE || $type == $HDR_FUNC || isCallgraphHeader($type)) ) { # kind of hacky - using spaces to try to align my $text = <hit not hit END_OF_HTML if ($hasBranchData) { $text .= <Branches: + taken - not taken # not executed END_OF_HTML } if ($hasMcdcData) { $text .= <MC/DC:    T 'true' sensitized t 'true' not sensitized F 'false' sensitized f 'false' not sensitized END_OF_HTML } if ($hasCallgraphData) { # the source view underlines the identifier at a call site, marks a # site which can go to more than one place with how many, and names # the sites the source cannot point at in a trailing comment $text .= <Callgraph:  taken not taken count unknown dispatch n possible callees call at no identifier END_OF_HTML } push(@row_left, [[undef, "headerItem", "Legend:"], [undef, "headerValueLeg", $text] ]); } elsif ($legend && ($type != $HDR_TESTDESC)) { my $text = <low: < $med_limit % medium: >= $med_limit % high: >= $hi_limit % END_OF_HTML push(@row_left, [[undef, "headerItem", "Legend:"], [undef, "headerValueLeg", $text] ]); } my %activeTlaColsForType; my @nonZeroTlas; if ($type != $HDR_TESTDESC && $main::show_tla) { # compute which TLAs have non-zero entries for line/branch/function my @visit = (SummaryInfo::LINE_DATA); push(@visit, SummaryInfo::BRANCH_DATA) if $hasBranchData; push(@visit, SummaryInfo::MCDC_DATA) if $hasMcdcData; push(@visit, SummaryInfo::FUNCTION_DATA) if $lcovutil::func_coverage; push(@visit, SummaryInfo::CALLGRAPH_DATA) if $hasCallgraphData; foreach my $covtype (@visit) { if ($main::show_zeroTlaColumns) { $activeTlaColsForType{$covtype} = \@SummaryInfo::tlaOrderNoExclude; } else { my @found; for my $tla (@SummaryInfo::tlaOrderNoExclude) { push(@found, $tla) if (0 != $summary->get($tla, $covtype)); } $activeTlaColsForType{$covtype} = \@found; } } foreach my $tla (@SummaryInfo::tlaOrderNoExclude) { # check to see that there are non-zero entries next if exists($SummaryInfo::excludedCoverpointTla{$tla}); foreach my $covtype (@visit) { if (grep({ $tla eq $_ } @{$activeTlaColsForType{$covtype}})) { push(@nonZeroTlas, $tla); last; } } } } # if $showAllTlasInSummary is set, then put all TLA categories in the # summary tables at the top of the page (but only the non-zero categories # in the 'file detail' tables. # Otherwise, only show non-zero categories in both places my $showAllTlasInSummary = 0; # turn off for now my $tlaSummaryTypes = $showAllTlasInSummary ? \@SummaryInfo::tlaOrderNoExclude : \@nonZeroTlas; if ($type == $HDR_TESTDESC) { push(@row_right, [["80%"]]); } else { my $totalTitle = "Covered + Uncovered code"; my $hitTitle = "Exercised code only"; if (@main::base_filenames) { $totalTitle .= " (not including EUB, ECB, DUB, DCB categories)"; $hitTitle .= " (CBC + GBC + GNC + GIC)"; } my @headerRow = (["5%", undef, undef], ["5%", "headerCovTableHead", "Coverage"], ["5%", "headerCovTableHead", "Total", undef, $totalTitle ]); if ($main::show_hitTotalCol) { # legacy view, or we have all the differential categories #- thus also want a summary $hitTitle = $main::use_legacyLabels ? undef : $hitTitle; push(@headerRow, ["5%", "headerCovTableHead", $main::opt_missed ? "Missed" : "Hit", undef, $hitTitle ]); } if ($main::show_tla) { # Show selected TLAs in title row - either all of them, or # only types which have non-zero entries for my $tla (@$tlaSummaryTypes) { my ($title, $label, $class); if ($main::use_legacyLabels) { $label = $SummaryInfo::tlaToLegacy{$tla}; $class = "headerCovTableHead"; } else { $label = $tla; $title = $SummaryInfo::tlaToTitle{$tla}; $class = "headerCovTableHead$tla"; } push(@headerRow, ["5%", $class, $label, undef, $title]); } } push(@row_right, \@headerRow); } # Line coverage my $tot = $summary->lines_found(); #might have been modified... my $hit = $summary->lines_hit(); my $style = $rate_name[classify_rate($tot, $hit, $ln_med_limit, $ln_hi_limit)]; my $rate = rate($hit, $tot, " %"); $hit -= $tot if $main::opt_missed; # negative number my @dataRow = ([undef, "headerItem", "Lines:"], [undef, "headerCovTableEntry$style", $rate], [undef, "headerCovTableEntry", $tot]); if ($main::show_hitTotalCol) { push(@dataRow, [undef, "headerCovTableEntry", $hit]); } if ($main::show_tla) { my @tlaRow = buildHeaderSummaryTableRow($summary, SummaryInfo::LINE_DATA, $fileDetail, \&SourceFile::nextTlaGroup, $tlaSummaryTypes); push(@dataRow, @tlaRow); } push(@row_right, \@dataRow) if ($type != $HDR_TESTDESC); # Function coverage if ($lcovutil::func_coverage) { my $tot = $summary->function_found(); my $hit = $summary->function_hit(); $style = $rate_name[classify_rate($tot, $hit, $fn_med_limit, $fn_hi_limit)]; $rate = rate($hit, $tot, " %"); $hit -= $tot if $main::opt_missed; # negative number my @dataRow = ([undef, "headerItem", "Functions:"], [undef, "headerCovTableEntry$style", $rate], [undef, "headerCovTableEntry", $tot]); if ($main::show_hitTotalCol) { push(@dataRow, [undef, "headerCovTableEntry", $hit]); } if ($main::show_tla) { # no file position for function (yet) my @tlaRow = buildHeaderSummaryTableRow($summary, SummaryInfo::FUNCTION_DATA, undef, undef, $tlaSummaryTypes); push(@dataRow, @tlaRow); } push(@row_right, \@dataRow) if ($type != $HDR_TESTDESC); } # Branch coverage if ($hasBranchData) { my $tot = $summary->branch_found(); my $hit = $summary->branch_hit(); $style = $rate_name[classify_rate($tot, $hit, $br_med_limit, $br_hi_limit)]; $rate = rate($hit, $tot, " %"); $hit -= $tot if $main::opt_missed; # negative number my @dataRow = ([undef, "headerItem", "Branches:"], [undef, "headerCovTableEntry$style", $rate], [undef, "headerCovTableEntry", $tot]); if ($main::show_hitTotalCol) { push(@dataRow, [undef, "headerCovTableEntry", $hit]); } if ($main::show_tla) { my @tlaRow = buildHeaderSummaryTableRow($summary, SummaryInfo::BRANCH_DATA, $fileDetail, \&SourceFile::nextBranchTlaGroup, $tlaSummaryTypes); push(@dataRow, @tlaRow); } push(@row_right, \@dataRow) if ($type != $HDR_TESTDESC); } # MC/DC coverage if ($hasMcdcData) { my $tot = $summary->mcdc_found(); my $hit = $summary->mcdc_hit(); $style = $rate_name[classify_rate($tot, $hit, $mcdc_med_limit, $mcdc_hi_limit)]; $rate = rate($hit, $tot, " %"); $hit -= $tot if $main::opt_missed; # negative number my @dataRow = ([undef, "headerItem", "MC/DC:"], [undef, "headerCovTableEntry$style", $rate], [undef, "headerCovTableEntry", $tot]); if ($main::show_hitTotalCol) { push(@dataRow, [undef, "headerCovTableEntry", $hit]); } if ($main::show_tla) { my @tlaRow = buildHeaderSummaryTableRow($summary, SummaryInfo::MCDC_DATA, $fileDetail, \&SourceFile::nextMcdcTlaGroup, $tlaSummaryTypes); push(@dataRow, @tlaRow); } push(@row_right, \@dataRow) if ($type != $HDR_TESTDESC); } # Callgraph coverage. This is the 'per file' view of the metric - the call # sites contained in this file's functions, which is what 'FNCF'/'FNCH' # count. The 'per function' view (which functions call this one) is on the # function detail page and does not total to this. if ($hasCallgraphData) { my $tot = $summary->callgraph_found(); my $hit = $summary->callgraph_hit(); $style = $rate_name[ classify_rate($tot, $hit, $callgraph_med_limit, $callgraph_hi_limit) ]; $rate = rate($hit, $tot, " %"); $hit -= $tot if $main::opt_missed; # negative number my @dataRow = ($callgraphLabel, [undef, "headerCovTableEntry$style", $rate], [undef, "headerCovTableEntry", $tot]); if ($main::show_hitTotalCol) { push(@dataRow, [undef, "headerCovTableEntry", $hit]); } if ($main::show_tla) { my @tlaRow = buildHeaderSummaryTableRow($summary, SummaryInfo::CALLGRAPH_DATA, $callgraphDetail, $callgraphLocators[0], $tlaSummaryTypes); push(@dataRow, @tlaRow); } push(@row_right, \@dataRow) if ($type != $HDR_TESTDESC); } # Aged coverage if ($show_dateBins) { # make a space in the table between before date bins my $dateBinDetailPage; $dateBinDetailPage = "index-date.$html_ext" if ($type != $HDR_SOURCE && $type != $HDR_FUNC && !isCallgraphHeader($type)); my $table = buildDateSummaryTable($summary, SummaryInfo::LINE_DATA, \&SummaryInfo::lineCovCount, $fileDetail, \&SourceFile::nextInDateBin, "Line coverage date bins:", $dateBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$table) if ($type != $HDR_TESTDESC); if ($lcovutil::func_coverage) { my $fn_table = buildDateSummaryTable($summary, SummaryInfo::FUNCTION_DATA, \&SummaryInfo::functionCovCount, $fileDetail, undef, "Function coverage date bins:", $dateBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$fn_table) if ($type != $HDR_TESTDESC); } if ($hasBranchData) { my $br_table = buildDateSummaryTable($summary, SummaryInfo::BRANCH_DATA, \&SummaryInfo::branchCovCount, $fileDetail, \&SourceFile::nextBranchInDateBin, "Branch coverage date bins:", $dateBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$br_table) if ($type != $HDR_TESTDESC); } if ($hasMcdcData) { my $mcdc_table = buildDateSummaryTable($summary, SummaryInfo::MCDC_DATA, \&SummaryInfo::mcdcCovCount, $fileDetail, \&SourceFile::nextMcdcInDateBin, "MC/DC coverage date bins:", $dateBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$mcdc_table) if ($type != $HDR_TESTDESC); } if ($hasCallgraphData) { my $callgraph_table = buildDateSummaryTable($summary, SummaryInfo::CALLGRAPH_DATA, \&SummaryInfo::callgraphCovCount, $callgraphDetail, $callgraphLocators[1], $callgraphDateLabel, $dateBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$callgraph_table) if ($type != $HDR_TESTDESC); } } # owner bins.. if (defined($main::show_ownerBins)) { # first, make sure there is owner data here (ie., owner data # was collected, or both that there is owner data and some # owners have uncovered code) # This it the header table - so we want to truncate the owner # list if it is too long (and the user asked us to) my ($ownerList, $truncated) = $summary->findOwnerList($callback_type, 1, $main::show_ownerBins && $main::show_ownerBins eq 'all'); if (defined($ownerList)) { my $ownerBinDetailPage; $ownerBinDetailPage = "index-owner.$html_ext" if ($type != $HDR_SOURCE && $type != $HDR_FUNC && !isCallgraphHeader($type)); my $table = buildOwnerSummaryTable($ownerList, $truncated, $summary, SummaryInfo::LINE_DATA, $fileDetail, \&SourceFile::nextInOwnerBin, "Line coverage ownership bins", $ownerBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$table) if ($type != $HDR_TESTDESC); if ($hasBranchData) { my $br_table = buildOwnerSummaryTable($ownerList, $truncated, $summary, SummaryInfo::BRANCH_DATA, $fileDetail, \&SourceFile::nextBranchInOwnerBin, "Branch coverage ownership bins", $ownerBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$br_table) if ($type != $HDR_TESTDESC); } if ($hasMcdcData) { my $mcdc_table = buildOwnerSummaryTable($ownerList, $truncated, $summary, SummaryInfo::MCDC_DATA, $fileDetail, \&SourceFile::nextMcdcInOwnerBin, "MC/DC coverage ownership bins", $ownerBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$mcdc_table) if ($type != $HDR_TESTDESC); } if ($hasCallgraphData) { my $callgraph_table = buildOwnerSummaryTable( $ownerList, $truncated, $summary, SummaryInfo::CALLGRAPH_DATA, $callgraphDetail, $callgraphLocators[2], $callgraphOwnerLabel, $ownerBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$callgraph_table) if ($type != $HDR_TESTDESC); } } } # Print rows my $num_rows = max(scalar(@row_left), scalar(@row_right)); for (my $i = 0; $i < $num_rows; $i++) { my $left = $row_left[$i]; my $right = $row_right[$i]; if (!defined($left)) { $left = [[undef, undef, undef], [undef, undef, undef]]; } if (!defined($right)) { $right = []; } write_header_line(*HTML_HANDLE, @{$left}, [$i == 0 ? "5%" : undef, undef, undef], @{$right}); } # Fourth line write_header_epilog(*HTML_HANDLE, $base_dir); return \%activeTlaColsForType; } sub get_sort_code($$$) { my ($link, $alt, $base) = @_; my $png; my $link_start; my $link_end; if (!defined($link)) { $png = "glass.png"; $link_start = ""; $link_end = ""; } else { $png = "updown.png"; $link_start = ''; $link_end = ""; } my $help = " title=\"Click to sort table by $alt\""; $alt = "Sort by $alt"; return " " . $link_start . '' . $link_end . ''; } sub get_file_code($$$$$$) { my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_; my $result = $text; my $link; my $key = 'name' ne $primary_key ? "-bin_$primary_key" : ""; if ($sort_button) { $link = "index$key$bin_type"; $link .= '-detail' unless ($type == $HEAD_NO_DETAIL); $link .= ".$html_ext"; } $result .= get_sort_code($link, "file name", $base); return $result; } sub get_line_code($$$$$$$) { my ($type, $sort_type, $text, $sort_button, $bin_type, $primary_key, $base) = @_; my $result = $text; my $key = ('name' eq $primary_key) ? '' : "-bin_$primary_key"; my $sort_link; $sort_link = "index" . $key . $bin_type . "-sort-l.$html_ext" if $sort_button; if ($type == $HEAD_NO_DETAIL) { # Just text } elsif ($type == $HEAD_DETAIL_HIDDEN) { # Text + link to detail view my $help = "title=\"Click to go to per-testcase coverage details\""; my $detail_link = 'index' . $key . $bin_type . '-detail' . $fileview_sortname[$sort_type] . '.' . $html_ext; $result .= " ( show details )'; } else { # Text + link to standard view my $help = "title=\"Click to hide per-testcase coverage details\""; $result .= " ( hide details )'; } # Add sort button $result .= get_sort_code($sort_link, "line coverage", $base); # we don't have a 'detail' bin page $result =~ s/index-bin_(.+?)-detail((-sort-.)?\.)/index-$1$2/g; return $result; } sub get_func_code($$$$$$) { my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_; my $result = $text; my $link; my $key = 'name' ne $primary_key ? "-bin_$primary_key" : ""; if ($sort_button) { $link = "index$key$bin_type"; $link .= '-detail' unless ($type == $HEAD_NO_DETAIL); $link .= "-sort-f.$html_ext"; } $result .= get_sort_code($link, "function coverage", $base); return $result; } sub get_callgraph_code($$$$$$) { my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_; my $result = $text; my $link; my $key = 'name' ne $primary_key ? "-bin_$primary_key" : ""; if ($sort_button) { $link = "index$key$bin_type"; $link .= '-detail' unless ($type == $HEAD_NO_DETAIL); $link .= "-sort-c.$html_ext"; } $result .= get_sort_code($link, "callgraph coverage", $base); return $result; } sub get_br_code($$$$$$) { my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_; my $result = $text; my $link; my $key = 'name' ne $primary_key ? "-bin_$primary_key" : ""; if ($sort_button) { $link = "index$key$bin_type"; $link .= '-detail' unless ($type == $HEAD_NO_DETAIL); $link .= "-sort-b.$html_ext"; } $result .= get_sort_code($link, "branch coverage", $base); return $result; } sub get_mcdc_code($$$$$$) { my ($type, $text, $sort_button, $bin_type, $primary_key, $base) = @_; my $result = $text; my $link; my $key = 'name' ne $primary_key ? "-bin_$primary_key" : ""; if ($sort_button) { $link = "index$key$bin_type"; $link .= '-detail' unless ($type == $HEAD_NO_DETAIL); $link .= "-sort-m.$html_ext"; } $result .= get_sort_code($link, "MC/DC coverage", $base); return $result; } # # write_file_table(filehandle, callback_type, base_dir, perTestcaseData, # parentSummary, ctrlSettings, activeTlaCols) # ctrlSettings = [fileview, sort_type, details_type, sort_name] # perTestcaseData = [testhash, testfnchash, testbrhash] # activeTlaCols = {coverage_type, [tlas with nonzero count]} # # Write a complete file table. OVERVIEW is a reference to a hash containing # the following mapping: # # filename -> "lines_found,lines_hit,funcs_found,funcs_hit,page_link, # func_link" + other file details # # TESTHASH is a reference to the following hash: # # filename -> \%testdata # %testdata: name of test affecting this file -> \%testcount # %testcount: line number -> execution count for a single test # # Heading of first column is "Filename" if FILEVIEW is true, "Directory name" # otherwise. # sub write_file_table(*$$$$$$) { local *HTML_HANDLE = $_[0]; my $callback_type = $_[1]; my $base_dir = $_[2]; my $perTestcaseData = $_[3]; # undef or [lineCov, funcCov, branchCov, mcdcCov] my $dirSummary = $_[4]; # SummaryInfo object my ($fileview, $primary_key, $sort_type, $bin_type) = @{$_[5]}; my $activeTlaCols = $_[6]; # $fileview == 0 if listing directories, 1 if listing files # $primary_key in ("name", "owner", "date"). If $primary_key is: # - 'name': leftmost column is file/directory name - # this is the original/vanilla genhtml behaviour # - 'owner': leftmost column is author name. Details for that owner # (for all files in the project, or for all files in this directory) # are shown in a contiguous block. # - 'date': leftmost column is date bin. Details for that bin are # shown in a contiguous block. # $sort_type in ("", "-sort-l", "-sort-b", "-sort-f") # $bin_type in ("", "-owner", "-date") # - if $bin_type not "", expand non-empty entries after file/directory # overall count (i.e. - show all the "owners" for this file/directory, # or all date bins for this file/directory) # - $bin_type is applied only if $primary_key is "name" $primary_key eq "name" || $bin_type eq "" or die( "primary key '$primary_key' does not support '$bin_type' detail reporting" ); my $hasBranchData = $lcovutil::br_coverage && 0 != $dirSummary->branch_found(); my $hasMcdcData = $lcovutil::mcdc_coverage && 0 != $dirSummary->mcdc_found(); # ..and the same for functions - except that one of the two reasons the # columns can be missing has a footnote explaining it, so write the reason # down here rather than re-deriving it from '!$includeFunctionColumns' # further down (where "no function data at all" was being reported as # "owner not identified", in reports with no owner data anywhere in them) my $hasFunctionData = $lcovutil::func_coverage && 0 != $dirSummary->function_found(); my $elidedFunctionColumnsForOwner = $hasFunctionData && 'owner' eq $primary_key; my $includeFunctionColumns = $hasFunctionData && !$elidedFunctionColumnsForOwner; # ..and call sites, which - unlike functions - do have an owner. See # 'CovTypeSummaryCallback::owner' my $hasCallgraphData = $lcovutil::callgraph_coverage && 0 != $dirSummary->callgraph_found(); my @tableRows; if ($primary_key eq 'name') { # sorted list of all the file or directory names foreach my $name ($dirSummary->get_sorted_keys($sort_type, 1)) { my $entrySummary = $dirSummary->get_source($name); if ($entrySummary->type() eq 'directory') { if ('directory' eq $dirSummary->type()) { $name = File::Basename::basename($name); } elsif ($entrySummary->is_directory(1)) { $name = $lcovutil::dirseparator . $name; } } else { die("unexpected summary type") unless 'file' eq $entrySummary->type(); $name = File::Basename::basename($name); } push(@tableRows, FileOrDirectoryCallback->new($name, $entrySummary)); } } elsif ($primary_key eq 'owner') { # retrieve sorted list of owner names - alphabetically, by name # or by number of missed lines or missed branches my $all = defined($main::show_ownerBins) && $main::show_ownerBins; # {owner => {covType => [found, hit]}}: keyed by coverage type rather # than by a hand-assigned index, so that adding a metric here is one # line rather than a renumbering. Function coverage is not in this # list - an owner has no function count; see 'owner_tlaCount' my @ownerTypes = ([1, SummaryInfo::LINE_DATA, $SORT_LINE], [$lcovutil::mcdc_coverage, SummaryInfo::MCDC_DATA, $SORT_MCDC ], [$lcovutil::br_coverage, SummaryInfo::BRANCH_DATA, $SORT_BRANCH ], [$lcovutil::callgraph_coverage, SummaryInfo::CALLGRAPH_DATA, $SORT_CALLGRAPH ]); my %owners; foreach my $d (@ownerTypes) { my ($enable, $dataType) = @$d; next unless $enable; foreach my $owner ($dirSummary->owners($all, $dataType)) { $owners{$owner} = {map({ $_->[1] => [0, 0] } @ownerTypes)} unless exists($owners{$owner}); $owners{$owner}->{$dataType} = [ $dirSummary->owner_tlaCount($owner, 'found', $dataType), $dirSummary->owner_tlaCount($owner, 'hit', $dataType) ]; } } my @sorted = sort(keys(%owners)); # default sort order # now, sort the owner list... foreach my $d (@ownerTypes) { my (undef, $dataType, $s) = @$d; if ($sort_type eq $s) { # sort by number of missed coverpoints of that type @sorted = sort({ my $la = $owners{$a}->{$dataType}; my $lb = $owners{$b}->{$dataType}; ($lb->[0] - $lb->[1]) <=> ($la->[0] - $la->[1]) || $a cmp $b # then by name } keys(%owners)); last; } } # don't truncate the 'owner detail table' # if user asked to see the page, then they want to be able to navigate # to all users #if (defined($ownerTableElements) && # $ownerTableElements < scalar(@sorted)) && # (0 == scalar(@truncateOwnerTableLevels) || # grep(/$callback_type/, @truncateOwnerTableLevels)))) { # # truncate owner list in header table # splice(@sorted, $ownerTableElements); #} foreach my $owner (@sorted) { push(@tableRows, FileOrDirectoryOwnerCallback->new($owner, $dirSummary)); } } elsif ($primary_key eq 'date') { for (my $bin = 0; $bin <= $#SummaryInfo::ageGroupHeader; ++$bin) { my $ageval = $dirSummary->age_sample($bin); # ..any enabled metric with something in this bin puts a row in the # table next unless grep({ my $covCount = SummaryInfo::covCountMethod($_); SummaryInfo::metricEnabled($_) && 0 != $dirSummary->$covCount('found', 'age', $ageval); } @SummaryInfo::covTypes); push(@tableRows, FileOrDirectoryDateCallback->new($bin, $dirSummary)); } } else { die("unsupported primary key '$primary_key'"); } # A table keyed by date or owner has the categories its bins have - which # may be fewer than the directory's: a deleted coverpoint has no current # line, so it has neither a date nor an owner, and an owner whose code is # all exercised is not listed unless 'all' argument. A category - or a # whole coverage type - which is in none of the rows has no column if ($primary_key ne 'name' && !$main::show_zeroTlaColumns) { my @types = ([\$hasMcdcData, SummaryInfo::MCDC_DATA], [\$hasBranchData, SummaryInfo::BRANCH_DATA], [\$includeFunctionColumns, SummaryInfo::FUNCTION_DATA], [\$hasCallgraphData, SummaryInfo::CALLGRAPH_DATA]); my %binned; foreach my $covtype (SummaryInfo::LINE_DATA, map({ $_->[1] } @types)) { next unless $covtype == SummaryInfo::LINE_DATA || grep({ $_->[1] == $covtype && ${$_->[0]} } @types); my @cbs = map({ $_->totalCallback($covtype) } @tableRows); next unless grep({ 0 != ($_->get('found') // 0) } @cbs); my @tlas; foreach my $tla (@{$activeTlaCols->{$covtype} // []}) { push(@tlas, $tla) if grep({ 0 != ($_->get($tla) // 0) } @cbs); } $binned{$covtype} = \@tlas; } ${$_->[0]} &&= exists($binned{$_->[1]}) foreach (@types); $binned{SummaryInfo::LINE_DATA} //= []; $activeTlaCols = \%binned; } # Determine HTML code for column headings my $hide = $HEAD_NO_DETAIL; my $show = $HEAD_NO_DETAIL; if (($dirSummary->type() eq 'directory' || $main::flat || $main::hierarchical) && $show_details ) { # "detailed" if line coverage hash not empty my $detailed = defined($perTestcaseData) && scalar(%{$perTestcaseData->[0]}); $hide = $detailed ? $HEAD_DETAIL_HIDDEN : $HEAD_NO_DETAIL; $show = $detailed ? $HEAD_DETAIL_SHOWN : $HEAD_DETAIL_HIDDEN; } my $file_col_title = ($fileview || $main::flat) ? 'File' : 'Directory'; # don't insert the 'sort' controls if there is just a single source file my $use_sort_button = $sort_tables && 1 < scalar($dirSummary->sources()); my $file_code = get_file_code($hide, $file_col_title, $use_sort_button && $sort_type != $SORT_FILE, $bin_type, $primary_key, $base_dir); my $line_code = get_line_code($show, $sort_type, "Line Coverage", $use_sort_button && $sort_type != $SORT_LINE, $bin_type, $primary_key, $base_dir); my $func_code = get_func_code($hide, "Function Coverage", $use_sort_button && $sort_type != $SORT_FUNC, $bin_type, $primary_key, $base_dir); my $br_code = get_br_code($hide, "Branch Coverage", $use_sort_button && $sort_type != $SORT_BRANCH, $bin_type, $primary_key, $base_dir); my $mcdc_code = get_mcdc_code($hide, "MC/DC Coverage", $use_sort_button && $sort_type != $SORT_MCDC, $bin_type, $primary_key, $base_dir); my $callgraph_code = get_callgraph_code($hide, "Callgraph Coverage", $use_sort_button && $sort_type != $SORT_CALLGRAPH, $bin_type, $primary_key, $base_dir); my @head_columns; my @lineCovCols = (["Rate", 2], "Total"); my @mcdcCovCols = ("Rate", "Total"); my @branchCovCols = ("Rate", "Total"); my @functionCovCols = ("Rate", "Total"); my @callgraphCovCols = ("Rate", "Total"); if ($main::show_hitTotalCol) { my $t = $main::opt_missed ? "Missed" : "Hit"; push(@lineCovCols, $t); push(@mcdcCovCols, $t); push(@branchCovCols, $t); push(@functionCovCols, $t); push(@callgraphCovCols, $t); } if ($main::show_tla) { my @visit = ([SummaryInfo::LINE_DATA, \@lineCovCols]); push(@visit, [SummaryInfo::MCDC_DATA, \@mcdcCovCols]) if $hasMcdcData; push(@visit, [SummaryInfo::BRANCH_DATA, \@branchCovCols]) if $hasBranchData; push(@visit, [SummaryInfo::FUNCTION_DATA, \@functionCovCols]) if $includeFunctionColumns; push(@visit, [SummaryInfo::CALLGRAPH_DATA, \@callgraphCovCols]) if $hasCallgraphData; foreach my $d (@visit) { my ($covtype, $cols) = @$d; foreach my $tla (@{$activeTlaCols->{$covtype}}) { my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} : $tla; push(@$cols, [$label, 1, $main::use_legacyLabels ? undef : $SummaryInfo::tlaToTitle{$tla} ]); } } } # each column group ends with its coverage type, which puts the groups in # the order of the table - see metricColumnRank push(@head_columns, [$line_code, $#lineCovCols + 2, \@lineCovCols, SummaryInfo::LINE_DATA] ); push(@head_columns, [$mcdc_code, $#mcdcCovCols + 1, \@mcdcCovCols, SummaryInfo::MCDC_DATA]) if $hasMcdcData; push(@head_columns, [$br_code, $#branchCovCols + 1, \@branchCovCols, SummaryInfo::BRANCH_DATA ]) if $hasBranchData; push(@head_columns, [$func_code, $#functionCovCols + 1, \@functionCovCols, SummaryInfo::FUNCTION_DATA ]) if $includeFunctionColumns; push(@head_columns, [$callgraph_code, $#callgraphCovCols + 1, \@callgraphCovCols, SummaryInfo::CALLGRAPH_DATA ]) if $hasCallgraphData; my $rank = metricColumnRank($sort_type); @head_columns = _inColumnOrder($rank, 3, @head_columns); my $showBinDetail = undef; if ($bin_type eq '-date' && $dirSummary && $dirSummary->hasDateInfo()) { $showBinDetail = 'date'; } elsif ($bin_type eq '-owner' && $dirSummary && $dirSummary->hasOwnerInfo()) { $showBinDetail = 'owner'; } my $num_columns = write_file_table_prolog( *HTML_HANDLE, $file_code, defined($showBinDetail) ? $showBinDetail : undef, $primary_key, @head_columns); my $all = defined($main::show_ownerBins) && $main::show_ownerBins eq 'all'; # show only the 'Total' row if all row entries are suppressed # because they have no un-exercised coverpoints my $elideEmptyRows = 0; if ($primary_key eq 'owner') { $elideEmptyRows = $main::show_ownerBins ne 'all'; } elsif ($primary_key eq 'date') { $elideEmptyRows = 1; } my $suppressedEmptyRow = 0; # if only one secondary entry, we don't need the 'Total' header my $suppressedSecondaryHeader = 0; foreach my $primaryCb (@tableRows) { # we need to find the 'owner' and 'date' row data for this file before # we write anything else, because we need to know the number of # rows that the $primary cell will span my @secondaryRows; my $skippedSecondaryRows = 0; if (defined($showBinDetail)) { if (defined($primaryCb->summary())) { my $source = $primaryCb->summary(); if ($showBinDetail eq 'owner') { # do I need an option to suppress the list of owners? # maybe too much information, in some circumstances? # are there any non-empty owner tables here? # If user explicitly asks to see the 'owner detail' page, # then they must be interested in which code is written by # each author - so we should not truncate the list my ($ownerList, $truncated) = $primaryCb->findOwnerList($callback_type, 0, $all); die("unexpected truncate count") unless $truncated == 0; push(@secondaryRows, @$ownerList) if defined($ownerList); } if ($showBinDetail eq 'date') { for (my $bin = 0; $bin <= $#SummaryInfo::ageGroupHeader; ++$bin) { my $ageval = $source->age_sample($bin); # same {covType => [missed, total, callback]} shape as # 'findOwnerList' and 'findFileList' produce my %covBin = SummaryInfo::binCovData(sub { return $primaryCb->dateDetailCallback($ageval, shift); }); next unless grep({ 0 != $covBin{$_}->[1] } @SummaryInfo::covTypes); push(@secondaryRows, [$SummaryInfo::ageGroupHeader[$bin], \%covBin]); } } } } # if showBinDetail elsif ($primary_key ne 'name') { ($skippedSecondaryRows, @secondaryRows) = @{$primaryCb->findFileList($all)}; } # one (found, hit) pair per @SummaryInfo::covTypes, in that order my ($found, $hit, $fn_found, $fn_hit, $callgraph_found, $callgraph_hit, $br_found, $br_hit, $mcdc_found, $mcdc_hit, $page_link, $fileSummary, $fileDetails) = $primaryCb->data(); # a bit of a hack: if this is top-level page (such that the links # are to directory pages rather than to source code detail pages) # and this is the 'owner bin detail' (or the 'date bin detail') view, # then link to the same 'bin detail' view of the directory page # This enables the user who is tracking down code written by a # particular user (or on a particular date) to go link-to-link # without having to select the 'bin' link in the destination header. if ($fileview == 0 && $bin_type ne "") { $page_link =~ s/index.$html_ext$/index$bin_type.$html_ext/; } my @columns; my @tableCallbackData = ($primaryCb->name(), $primaryCb->summary(), $fileDetails, $page_link); my $showLineGraph = 1; # Line coverage push(@columns, [$found, $hit, $med_limit, $hi_limit, $showLineGraph, $primaryCb->totalCallback(SummaryInfo::LINE_DATA), SummaryInfo::LINE_DATA ]); # MC/DC coverage if ($hasMcdcData) { push(@columns, [$mcdc_found, $mcdc_hit, $mcdc_med_limit, $mcdc_hi_limit, 0, $primaryCb->totalCallback(SummaryInfo::MCDC_DATA), SummaryInfo::MCDC_DATA ]); } # Branch coverage if ($hasBranchData) { push(@columns, [$br_found, $br_hit, $br_med_limit, $br_hi_limit, 0, $primaryCb->totalCallback(SummaryInfo::BRANCH_DATA), SummaryInfo::BRANCH_DATA ]); } # Function coverage if ($includeFunctionColumns) { # no 'owner' callbacks for function... my $cbStruct = $primaryCb->totalCallback(SummaryInfo::FUNCTION_DATA); push(@columns, [$fn_found, $fn_hit, $fn_med_limit, $fn_hi_limit, 0, $cbStruct, SummaryInfo::FUNCTION_DATA ]); } # Callgraph coverage if ($hasCallgraphData) { push(@columns, [$callgraph_found, $callgraph_hit, $callgraph_med_limit, $callgraph_hi_limit, 0, $primaryCb->totalCallback(SummaryInfo::CALLGRAPH_DATA), SummaryInfo::CALLGRAPH_DATA ]); } my $elide_secondary_header = $compactSummaryTables && (scalar(@secondaryRows) == 1 && $skippedSecondaryRows == 0); $suppressedSecondaryHeader ||= $elide_secondary_header; if (!$elide_secondary_header) { # pass 'dirSummary' to print method: we omit the 'owner' column if # none of the files in this directory have any owner information # (i.e., none of them are found in the repo) my $numRows = (1 + scalar(@secondaryRows)); my $asterisk; if ($elideEmptyRows && (1 == $numRows || $skippedSecondaryRows)) { $asterisk = '∗'; $suppressedEmptyRow = 1; } @columns = _inColumnOrder($rank, 6, @columns); write_file_table_entry(*HTML_HANDLE, $base_dir, [$primaryCb->name(), \@tableCallbackData, $activeTlaCols, $numRows, $primary_key, 0, $fileview, "fileOrDir", $page_link, $dirSummary, $showBinDetail, $asterisk ], @columns); } # sort secondary rows... if ($sort_type == $SORT_FILE) { # alphabetic @secondaryRows = sort({ $a->[0] cmp $b->[0] } @secondaryRows); } else { # the secondary row bins are keyed by coverage type, so the sort key # is the type itself rather than a position in the row my %sortKey = ($SORT_LINE => SummaryInfo::LINE_DATA, $SORT_FUNC => SummaryInfo::FUNCTION_DATA, $SORT_CALLGRAPH => SummaryInfo::CALLGRAPH_DATA, $SORT_BRANCH => SummaryInfo::BRANCH_DATA, $SORT_MCDC => SummaryInfo::MCDC_DATA); my $sortElem = $sortKey{$sort_type}; @secondaryRows = sort({ my $ca = $a->[1]->{$sortElem}; my $cb = $b->[1]->{$sortElem}; # sort based on 'missed' - then by name, so a tie # is in the same order in every run $cb->[0] <=> $ca->[0] or $a->[0] cmp $b->[0]; } @secondaryRows) if defined($sortElem); } foreach my $secondary (@secondaryRows) { my ($name, $covBin) = @$secondary; my ($lineMissed, $lineTotal, $lineCb) = @{$covBin->{(SummaryInfo::LINE_DATA)}}; my $lineHit = $lineTotal - $lineMissed; my $fileInfo = $primaryCb->secondaryElementFileData($name); my $entry_type = $lineCb->cb_type(); my @ownerColData; push(@ownerColData, [$lineTotal, $lineHit, $med_limit, $hi_limit, $showLineGraph, $lineCb, SummaryInfo::LINE_DATA ]); foreach my $d ([$hasMcdcData, SummaryInfo::MCDC_DATA], [$hasBranchData, SummaryInfo::BRANCH_DATA], [$includeFunctionColumns, SummaryInfo::FUNCTION_DATA ], [$hasCallgraphData, SummaryInfo::CALLGRAPH_DATA] ) { my ($include, $covType) = @$d; next unless $include; my ($missed, $total, $callback) = @{$covBin->{$covType}}; # need to compute the totals... my ($lim_med, $lim_high) = covLimits($covType); push(@ownerColData, [$total, $total - $missed, $lim_med, $lim_high, 0, $callback, $covType ]); } if ($dirSummary->type() eq 'directory') { # use the basename (not the path or full path) in the # secondary key list: # - these are files in the current directory, so we lose # no information by eliding the directory part $name = File::Basename::basename($name); } $name = apply_prefix($name, @dir_prefix); @ownerColData = _inColumnOrder($rank, 6, @ownerColData); write_file_table_entry(*HTML_HANDLE, $base_dir, # 'owner' page type - no span, no page link [$name, $fileInfo, $activeTlaCols, 1, $primary_key, 1 + $elide_secondary_header, $fileview, $entry_type, ], @ownerColData); } next unless ($show_details && defined($perTestcaseData) && $primary_key eq "name"); # we know that the top-level callback item must hold a file. my $filename = $primaryCb->name(); my ($testhash, $testfnchash, $testbrhash, $testcase_mcdc) = @$perTestcaseData; my $testdata = $testhash->{$filename}; # Check whether we should write test specific coverage as well next if (!defined($testdata)); my $testfncdata = $testfnchash->{$filename}; my $testbrdata = $testbrhash->{$filename}; my $test_mcdc = $testcase_mcdc->{$filename}; # Filter out those tests that actually affect this file my $affecting_tests = get_affecting_tests($testdata, $testfncdata, $testbrdata, $test_mcdc); # Does any of the tests affect this file at all? if (!%$affecting_tests) { next; } while (my ($testname, $data) = each(%$affecting_tests)) { ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $mcdc_found, $mcdc_hit) = @$data; my $showgraph = 0; my @results; push(@results, [$found, $hit, SummaryInfo::LINE_DATA, TestcaseTlaCount->new($testdata->value($testname), $fileDetails, SummaryInfo::LINE_DATA) ]); # there might not be optional test data for some tests - that type was # not enabled push(@results, [$mcdc_found, $mcdc_hit, SummaryInfo::MCDC_DATA, TestcaseTlaCount->new($test_mcdc->value($testname), $fileDetails, SummaryInfo::MCDC_DATA) ]) if ($hasMcdcData && defined($test_mcdc->value($testname))); push(@results, [$br_found, $br_hit, SummaryInfo::BRANCH_DATA, TestcaseTlaCount->new($testbrdata->value($testname), $fileDetails, SummaryInfo::BRANCH_DATA) ]) if ($hasBranchData && defined($testbrdata->value($testname))); push(@results, [$fn_found, $fn_hit, SummaryInfo::FUNCTION_DATA, TestcaseTlaCount->new($testfncdata->value($testname), $fileDetails, SummaryInfo::FUNCTION_DATA) ]) if ($includeFunctionColumns && defined($testfncdata->value($testname))); my $href = $testname; # Insert link to description of available if ($test_description{$testname}) { $href = "" . "$testname"; } # ..and a column group the test has no data for is empty, so that # the groups after it line up with their headings my %present = map({ ($_->[2] => 1) } @results); foreach my $d ([$hasMcdcData, SummaryInfo::MCDC_DATA], [$hasBranchData, SummaryInfo::BRANCH_DATA], [$includeFunctionColumns, SummaryInfo::FUNCTION_DATA ], [$hasCallgraphData, SummaryInfo::CALLGRAPH_DATA] ) { my ($include, $covType) = @$d; push(@results, [undef, undef, $covType]) if $include && !exists($present{$covType}); } @results = _inColumnOrder($rank, 2, @results); pop(@results) while !defined($results[-1]->[0]); write_file_table_detail_entry(*HTML_HANDLE, $base_dir, $href, $showBinDetail, $activeTlaCols, @results); } } foreach my $note ( [ $suppressedEmptyRow, "∗ 'Detail' entries with no 'missed' coverpoints are elided. Use the '--show-owners all' flag to retain them." ], [ $suppressedSecondaryHeader, '∗∗ Bin \'Total\' header elided when bin contains only one entry.' ], [ $elidedFunctionColumnsForOwner, "Note: 'Function Coverage' columns elided as function owner is not identified." ] ) { next unless $note->[0]; write_html(*HTML_HANDLE, < $note->[1] END_OF_HTML } write_file_table_epilog(*HTML_HANDLE); } # # get_affecting_tests(testdata, testfncdata, testbrdata, pertest_mcdc) # # HASHREF contains a mapping filename -> (linenumber -> exec count). Return # a hash containing mapping filename -> "lines found, lines hit" for each # filename which has a nonzero hit count. # sub get_affecting_tests($$$$) { my ($testdata, $testfncdata, $testbrdata, $testmcdcdata) = @_; my %result; foreach my $testname ($testdata->keylist()) { # Get (line number -> count) hash for this test case my $testcount = $testdata->value($testname); my ($found, $hit) = $testcount->get_found_and_hit(); # no other coverpoints are hit, if the line isn't hit next if $hit == 0; # Calculate sum my $testfnccount = $testfncdata->value($testname); my $testbrcount = $testbrdata->value($testname); my $testmcdccount = $testmcdcdata->value($testname); # might be no data for this testcase my ($fn_found, $fn_hit) = defined($testfnccount) ? $testfnccount->get_found_and_hit() : (0, 0); my ($br_found, $br_hit) = defined($testbrcount) ? $testbrcount->get_found_and_hit() : (0, 0); my ($mcdc_found, $mcdc_hit) = defined($testmcdccount) ? $testmcdccount->get_found_and_hit() : (0, 0); $result{$testname} = [$found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $mcdc_found, $mcdc_hit ]; } return \%result; } # # write_source(filehandle, source_filename, count_data, checksum_data, # converted_data, func_data, sumbrcount, mcdc_summary, base_dir) # # Write an HTML view of a source code file. Returns a list containing # data as needed by gen_png(). # # Die on error. # sub write_source($$$$$$$$$) { local *HTML_HANDLE = shift; my ($srcfile, $count_data, $checkdata, $fileCovInfo, $funcdata, $sumbrcount, $mcdc_summary, $base_dir) = @_; my @result; # The @result list is only consumed to build the PNG overview / frameset, # i.e. when --frames is in effect. When frames are off (the common case) # accumulating it -- one push per source line, each string retained until # the whole file is rendered -- is pure time and memory overhead, so skip # the pushes entirely. my $collect = $frames; # suppress branch and MCDC columns if there is no data in the file my $showBranches = $lcovutil::br_coverage && 0 != $sumbrcount->found(); my $showMcdc = $lcovutil::mcdc_coverage && 0 != $mcdc_summary->found(); write_source_prolog(*HTML_HANDLE, $srcfile->isProjectFile(), $showBranches, $showMcdc); my $line_number = 0; # the call sites of this file, and the page whose table shows them: the # callees page, since a site in the source is one of the calls made by the # function which contains it. Both are in the same directory as this page, # so a bare file name links there. The order is the one a reader who # followed a link wants, which is by name - see callgraphPageName my ($callSites, $callgraphPage); if ($lcovutil::callgraph_coverage && defined($funcdata)) { # ..and the calls this file's text makes on behalf of a function of some # other file, whose table is on that other file's page - see # %callgraphForeignSites. The page is resolved here rather than in # the parent which collected them, because it is relative to this page my @foreign; my $foreignSites = $main::callgraphForeignSites{$srcfile->path()}; foreach my $group (@{$foreignSites || []}) { my $owner = filePageBase($group->[4], $base_dir); next unless defined($owner); push(@foreign, [@$group[0 .. 3], $owner . '.' . $callgraphView[CALLGRAPH_VIEW_CALLEES]->{name} . ".$html_ext" ]); } $callSites = callgraphSourceGroups($funcdata, \@foreign); $callgraphPage = callgraphPageName(File::Basename::basename($srcfile->path()), CALLGRAPH_VIEW_CALLEES); } my $cbdata = PrintCallback->new($srcfile, $fileCovInfo, $callSites, $callgraphPage); $cbdata->locate(0, $sumbrcount) if $showBranches; $cbdata->locate(1, $mcdc_summary) if $showMcdc; my ($region, $empty); if ($SummaryInfo::selectCallback) { $region = InInterestingRegion->new($srcfile, $fileCovInfo->lineMap()); $empty = ''; if ($srcfile->isProjectFile()) { $empty .= ' ' x $main::age_field_width; $empty .= ' ' x $main::owner_field_width; } # using 8 characters for line number field, with different # foreground/background - to make distinctive $empty .= ' ' . (' ' x (8 - 3)) . '...'; $empty .= ' ' x $main::br_field_width if $showBranches; $empty .= ' ' x $main::mcdc_field_width if $showMcdc; $empty .= ' ' x $main::tla_field_width; $empty .= ' ' x ($line_field_width - 1); $empty .= ' (elided NUM_LINES ignored lines)'; } my $prevLine = 0; foreach my $srcline (@{$srcfile->lines()}) { $line_number++; $cbdata->lineNo($line_number); if (defined($region) && !$region->interesting($line_number)) { lcovutil::info(1, "skip line $line_number\n"); next; } $main::pageShown->{gcov}->{$line_number} = 1 if defined($main::pageShown); if ($prevLine != $line_number - 1) { $cbdata->age(-1, $line_number); $cbdata->owner(undef, $line_number); $cbdata->tla(undef, $line_number); lcovutil::info(1, "continuation_line $line_number\n"); my $l = $empty; my $c = $line_number - ($prevLine + 1); $l =~ s/NUM_LINES/$c/; $l =~ s/lines/line/ if $c == 1; push(@result, $l) if $collect; write_html(*HTML_HANDLE, $l . "\n"); } $prevLine = $line_number; # Source code matches coverage data? lcovutil::ignorable_error($lcovutil::ERROR_MISMATCH, "checksum mismatch at " . $srcfile->path() . ":$line_number\n") if ( defined($checkdata->value($line_number)) && ($checkdata->value($line_number) ne md5_base64($srcline->text())) ); my $rendered = write_source_line(HTML_HANDLE, $srcline, $count_data->value($line_number), $showBranches, $sumbrcount->value($line_number), $showMcdc, $mcdc_summary->value($line_number), $cbdata); push(@result, $rendered) if $collect; } if ($prevLine != $line_number) { # need another blank line at the bottom... my $l = $empty; my $c = $line_number - $prevLine; $l =~ s/NUM_LINES/$c/; push(@result, $l) if $collect; write_html(*HTML_HANDLE, $l . "\n"); } write_source_epilog(*HTML_HANDLE); return (@result); } sub funcview_get_label($$$$) { my ($name, $base, $col, $sort_type) = @_; my $link; if (!defined($col)) { if ($sort_tables && $sort_type != $SORT_FILE) { $link = "$name.func.$html_ext"; } return "Function Name" . get_sort_code($link, "function name", $base); } elsif ($col eq 'hit') { if ($sort_tables && $sort_type != $SORT_LINE) { $link = "$name.func-c.$html_ext"; } return "Hit count" . get_sort_code($link, "function hit count", $base); } elsif ($col eq 'missed_line') { if ($sort_tables && $sort_type != $SORT_MISSING_LINE) { $link = "$name.func-l.$html_ext"; } return "Lines" . get_sort_code($link, "unexercised lines in function", $base); } elsif ($col eq 'missed_mcdc') { if ($sort_tables && $sort_type != $SORT_MISSING_MCDC) { $link = "$name.func-m.$html_ext"; } return "MC/DC" . get_sort_code($link, "unexercised MC/DC expressions in function", $base); } elsif ($col eq 'missed_callgraph') { if ($sort_tables && $sort_type != $SORT_MISSING_CALLGRAPH) { $link = "$name.func-x.$html_ext"; } return "Callgraph" . get_sort_code($link, "unexercised call sites in function", $base); } elsif ($col eq 'callers') { if ($sort_tables && $sort_type != $SORT_MISSING_CALLERS) { $link = "$name.func-r.$html_ext"; } return ucfirst($callgraphView[CALLGRAPH_VIEW_CALLERS]->{title}) . get_sort_code($link, $callgraphView[CALLGRAPH_VIEW_CALLERS]->{hitAlt}, $base); } else { die("unexpected sort $col") unless ($col eq 'missed_branch'); if ($sort_tables && $sort_type != $SORT_MISSING_BRANCH) { $link = "$name.func-b.$html_ext"; } return "Branches" . get_sort_code($link, "unexercised branches in function", $base); } } sub callgraphview_get_label($$$$$) { # The headings of a call site page which are also its orders: the name of # the function, which is how a reader looks one up; how much of that # function's own side of the callgraph was exercised, which is what the # reader who is not looking for a particular function came for; how much # of the other side was; and who last changed the function, and when. # The sort widget of the order the page is already in has no link, exactly as # in the function table: the reader is looking at that order my ($name, $base, $col, $view, $sort_type) = @_; my $page = $callgraphView[$view]; my ($order, $label, $alt); if (!defined($col)) { ($order, $label, $alt) = ($SORT_FILE, 'Function Name', 'function name'); } elsif ($col eq 'hit') { ($order, $label, $alt) = ($SORT_MISSING_CALLGRAPH, 'Rate', $page->{hitAlt}); } elsif ($col eq 'xref') { # ..the way to the other direction, which is that direction's proportion my $other = $callgraphView[1 - $view]; ($order, $label, $alt) = ($SORT_XREF, $other->{column}, $other->{hitAlt}); } elsif ($col eq 'owner') { ($order, $label, $alt) = ($SORT_OWNER, 'Owner', 'owner'); } else { die("unexpected sort $col") unless $col eq 'age'; ($order, $label, $alt) = ($SORT_AGE, 'Age', 'age'); } my $link; $link = callgraphPageName($name, $view, $order) if $sort_tables && $sort_type != $order; return $label . get_sort_code($link, $alt, $base); } # # funcview_get_sorted(funcdata, sort_type, mergedView) # # Depending on the value of sort_type, return a list of functions sorted # by name (type 0), by the associated call count (type 1) or by the # proportion of one of the columns of the function table. # sub funcview_get_sorted($$$) { my ($funcData, $type, $merged) = @_; my ($differential_func, $lineFuncData, $branchFuncData, $mcdcFuncData, $calleeFuncData, undef, $callgraphXref) = @$funcData; my @rtn = keys(%$differential_func); if ($type == $SORT_LINE) { @rtn = sort({ my $da = $differential_func->{$a}->hit(); my $db = $differential_func->{$b}->hit(); $da->[0] <=> $db->[0] or # sort by function name if count matches $a cmp $b } @rtn); } elsif ($type != $SORT_FILE) { # lowest to highest exercised proportion of the column the table is # sorted by - see '_rateOrder' - then by function name. The calls # which reach a function are none at all for one which is not on the # callers page my $map; if ($type == $SORT_MISSING_CALLERS) { $map = { map({ ($_ => $callgraphXref->{$_}->[CALLGRAPH_VIEW_CALLERS]) } keys(%$callgraphXref)) }; } else { my %column = ($SORT_MISSING_LINE => $lineFuncData, $SORT_MISSING_BRANCH => $branchFuncData, $SORT_MISSING_MCDC => $mcdcFuncData, $SORT_MISSING_CALLGRAPH => $calleeFuncData,); die("unexpected sort $type") unless exists($column{$type}); $map = $column{$type}; } @rtn = sort({ _rateOrder($map->{$a}, $map->{$b}) or $a cmp $b } @rtn); } elsif (!defined($main::no_sort)) { # sort alphabetically by function name @rtn = sort(@rtn); } return @rtn; } # Where each function of the report is defined, keyed by symbol, as # [file, line, leader name]. The callee cell of a call site row leads to that # callee's own callers page, and all the site record holds is a bare symbol: # the file says which page, and the leader name says which row of it - the # function table and both call site pages know a function by its leader, which # for an alias is some other symbol entirely. A symbol which # more than one file defines - a 'static', usually - goes into the second hash # instead, keyed by file as well, exactly as 'TraceFile::resolveCallSites' # indexes it, so that the two views agree about which sites have a callee # (These declarations follow the 'exit' of the main program, so they never # execute - 'our' does its work at compile time.) # LCOV_UNREACHABLE_START our %calleeLocation; our %calleeLocationDuplicated; # ..and the names a symbol represents, where any were merged into it (see # FunctionAlias::merged), keyed by "\0": what the callee cell of a row # marks the symbol with - see _mergedNameNote. our %calleeMergedNames; # ..and the symbols which are alias groups listed without a summary block, # keyed the same way: a call of one leads to the rows of its group # (...because the is no row for the function as a whole) - see # 'show_callgraph_summary' in man lcovrc(5) our %calleeGroupShown; # LCOV_UNREACHABLE_STOP sub build_callee_location_index($) { my $traceFile = shift; my %definedIn; foreach my $filename ($traceFile->files()) { my $functionMap = $traceFile->data($filename)->func(); my $measured = !$main::show_callgraph_summary && $functionMap->hasCallgraphData($callgraphCalledSymbols); foreach my $key ($functionMap->keylist()) { my $entry = $functionMap->findKey($key); my $where = [$filename, $entry->line(), $entry->name()]; my $grouped = $measured && _showAliasGroups($entry, $measured); foreach my $name ($entry->aliasNames()) { my $merged = $entry->alias($name)->merged(); $calleeMergedNames{"$filename\0$name"} = $merged if defined($merged); $calleeGroupShown{"$filename\0$name"} = 1 if $grouped; if (exists($calleeLocationDuplicated{$name})) { $calleeLocationDuplicated{$name}->{$filename} = $where; } elsif (exists($calleeLocation{$name})) { $calleeLocationDuplicated{$name} = { $definedIn{$name} => $calleeLocation{$name}, $filename => $where }; delete($calleeLocation{$name}); delete($definedIn{$name}); } else { $calleeLocation{$name} = $where; $definedIn{$name} = $filename; } } } } } # The call-site groups written in each file which belong to a function of some # other file, keyed by the file they are written in and carrying that other # file as a fifth element. The source view of a file decorates the calls its # own text makes, and a call in an '#include'd fragment is in the text of the # fragment while its data is on a function of the file which includes it - see # 'FNC:' in geninfo(1). Built in the parent, for the same reason # %calleeLocation is: the file whose text holds the call cannot find these # sites in its own data, and a forked child holds only its own segment our %callgraphForeignSites; # LCOV_UNREACHABLE_LINE - follows main's 'exit' sub build_callgraph_foreign_index($) { my $traceFile = shift; foreach my $filename ($traceFile->files()) { my $functionMap = $traceFile->data($filename)->func(); foreach my $key ($functionMap->keylist()) { my $entry = $functionMap->findKey($key); next if 0 == $entry->numCallSites(); foreach my $group (callSiteGroups($entry, $filename)) { next unless defined($group->[3]); push(@$group, $filename); push(@{$callgraphForeignSites{$group->[3]}}, $group); } } } } # What the pages of the file being written show, where '--select-script' leaves # out parts - undef where nothing is removed - i.e., when every page shows # everything. # A hash holding # file the file # gcov the lines its source view shows, as a set # callers the anchors its callers page has, likewise # callees the same for its callees page # A link to a line, a site or a function the filter removed goes nowhere # (would be a dangling reference) and so is suppressed: see _pageShows. # Held for an individula file - because the child only knows about this file. # The parts of some other file is decided in that file's own child our $pageShown; # LCOV_UNREACHABLE_LINE - follows main's 'exit' sub callgraphShownAnchors($$) { # the start of '$pageShown' for one file, from the entries its call site # pages list - see callgraphDetailEntries. Every entry on a page is named # by the anchor of its name cell, and every site group of the callees page, # and every row of one, by its own my ($filename, $entries) = @_; my %shown = (file => $filename, gcov => {}); foreach my $view (0 .. CALLGRAPH_VIEW_COUNT - 1) { my %anchors; foreach my $entry (@$entries) { next unless $entry->{has}->[$view]; $anchors{_calleeFuncAnchor($entry->{name})} = 1; if (defined($entry->{groupMetric}) && !$main::suppress_alias_groups) { while (my ($alias, $metric) = each(%{$entry->{groupMetric}})) { $anchors{_aliasGroupAnchor($alias)} = 1 if defined($metric->[$view]); } } next unless $view == CALLGRAPH_VIEW_CALLEES; foreach my $group (@{$entry->{groups}}) { my ($line, $column, $rows, $siteFile) = @$group; $anchors{_callSiteAnchor($line, $column, undef, $siteFile)} = 1; foreach my $position (1 .. scalar(@$rows)) { $anchors{_callSiteAnchor($line, $column, $position, $siteFile)} = 1; } } } $shown{$callgraphView[$view]->{name}} = \%anchors; } return \%shown; } sub _pageShows($$$) { # whether the '$page' page of '$filename' - 'gcov' for the source view, or the # name of a call site page - has '$anchor', which for the source view is a # line number. Which is to say: whether a link to it can be written my ($filename, $page, $anchor) = @_; return 1 unless defined($pageShown); return 0 unless $filename eq $pageShown->{file}; return exists($pageShown->{$page}->{$anchor}); } sub _shownSourceLine($$) { # the line a link to a function's source leads to: '$line', a couple of # lines above '$defined' so that the definition is not the top line of the # window - or, where the filter left those out, the first line from there # which is shown. The definition itself is: the function would not be # listed otherwise my ($line, $defined) = @_; return $line unless defined($pageShown); while ($line < $defined && !exists($pageShown->{gcov}->{$line})) { ++$line; } return $line; } sub categorize_all_call_sites($$$) { # Categorize the call sites of every file, in the parent, before any page is # written. The result is thrown away: what is wanted is the side effect of # 'FileCoverageInfo::categorizeCallSites' - each site's category stored on # the FunctionEntry which contains it. # # A file's own sites are categorized again when its page is built, which is # where they are attached to their lines. This pass exists because the # 'callers of this function' table shows the category of a site in some # *other* file, and that file may not have been processed yet - or may be # processed in a different child, whose memory the page writer never sees. # Doing it here is the same answer for every reader, whatever the file order # and whatever '--parallel' does. my ($currentTrace, $baseTrace, $diffMap) = @_; foreach my $filename ($currentTrace->files()) { my $base; if (defined($baseTrace)) { # The baseline is looked up exactly as the per-file pass looks it up # (see 'process_file'): through the diff's name mapping, with the # basename fallback, and answering an *empty* map rather than undef # when the baseline has no entry for the file. Those two are # different answers - undef means "this report has no baseline", # which categorizes every site as new code - and the two passes # disagreeing shows up as a 'callers of this function' table whose # category differs from the one the caller's own page displays $base = $baseTrace->data($diffMap->baseline_file_name($filename), 1); } my $current = $currentTrace->data($filename); FileCoverageInfo::categorizeCallSites( $filename, defined($base) ? $base->func() : undef, $current->func(), $diffMap, FileCoverageInfo::changedStatementLines( $filename, $base, $current, $diffMap )); } } sub calleeLocation($$) { # where the named callee is defined, as [file, line], or undef when it is # external to the report or is one of several same-named local functions and # not the one this caller's file defines. Those two answers are the same # answer - the site is counted where it is and has no callee-side row - # which is the treatment 'printf' gets my ($callee, $callerFile) = @_; return $calleeLocationDuplicated{$callee}->{$callerFile} if exists($calleeLocationDuplicated{$callee}); return exists($calleeLocation{$callee}) ? $calleeLocation{$callee} : undef; } sub filePageBase($$) { # the report pages of the named file, as a path relative to the page being # written, or undef when the report has no page for that file. '$base_dir' # is the writing page's own '../..' prefix my ($filename, $base_dir) = @_; return undef unless exists($filePageDir{$filename}); my $rel = File::Spec->catfile($filePageDir{$filename}, File::Basename::basename($filename)); # 'html_create' lower-cases the whole report-relative path, so the file on # disk is spelled that way and the link has to be too return $base_dir . ($lcovutil::case_insensitive ? lc($rel) : $rel); } sub pageName($$) { # one page of a file, named the way 'html_create' writes it: '.gcov', # '.func', '.callee'. '$base' is a file name and not a path, # so this is a link from another page of the same file my ($base, $kind) = @_; $base = lc($base) if $lcovutil::case_insensitive; return "$base.$kind.$html_ext"; } sub callgraphPageName($$;$) { # one call site page of a file, in one order. '$sort_type' is the order the # link should lead to and is omitted by a link from anywhere but a call # site page: a reader who followed such a link is looking for a particular # function, which is what the name order is for. A reader who is already # on one of these pages chose an order, so a link from there keeps it my ($base, $view, $sort_type) = @_; my $kind = $callgraphView[$view]->{name}; $kind .= _callgraphPageSuffix($sort_type) if defined($sort_type); return pageName($base, $kind); } sub _callgraphPageSuffix($) { # what tells the orders of one call site page apart in its name: nothing for # the name order, which is the page a link from anywhere else leads to my $sort_type = shift; return '' if $sort_type == $SORT_FILE; return '-c' if $sort_type == $SORT_MISSING_CALLGRAPH; return '-x' if $sort_type == $SORT_XREF; return '-o' if $sort_type == $SORT_OWNER; die("unexpected sort $sort_type") unless $sort_type == $SORT_AGE; return '-a'; } sub callgraphPageSorts($$) { # The orders one call site page of a file is written in, given the functions # of the file - see 'callgraphDetailEntries' - or 0 where the file has no # such page. An order is there only where the page has the column which # leads to it: the way to the other direction where the file has that page, # and the Age and Owner columns where the annotation callback answered for # one of the functions this page shows. A page in an order no column of it # offers is a page nothing leads to my ($entries, $view) = @_; my @shown = grep({ $_->{has}->[$view] } @$entries); return 0 unless @shown; my $other = grep({ $_->{has}->[1 - $view] } @$entries); my $annot = grep({ defined($_->{annot}) } @shown); return [ grep({ ($_ != $SORT_XREF || $other) && (($_ != $SORT_OWNER && $_ != $SORT_AGE) || $annot) } @callgraphview_sortlist) ]; } sub callgraphLinkSort($$$) { # the order a link from one call site page of a file to the other leads to: # the order the reader is already in, where that page is written in it - # and its name order where it is not, which is the page a reader who # arrived from anywhere else would see. '$pages' is the orders of both # pages, as 'callgraphPageSorts' returns them my ($pages, $view, $sort_type) = @_; return $sort_type if grep({ $_ == $sort_type } @{$pages->[$view]}); # Not seen in practice: the way to the other direction is on both pages # when it is on either, and the annotation callback answers for every line # of a file or for none - so the two pages of a file are in the same orders # LCOV_UNREACHABLE_START return $SORT_FILE; # LCOV_UNREACHABLE_STOP } sub _headerTitle($$) { # the label of one table of the page header, carrying '$title' as its tooltip # where there is one my ($label, $title) = @_; return $label unless defined($title); return "$label"; } sub isCallgraphHeader($) { # is this the header of a call site page? The two of them are detail pages # of one file exactly as the function table is, so nearly everything the # header does for one it does for the other - see write_header my $type = shift; return $type == $HDR_CALLERS || $type == $HDR_CALLEES; } sub _mergeSiteRow($$$) { # fold one more record into an existing display row: add the count, name the # callee if it is a symbol the row does not already name, and keep the # higher-priority of the two categories. Two records reach one row only # when they lead to the same place (see 'callSiteGroups'), which normally # means one category as well - the exception is two identically-located # calls of which the edit changed only one my ($row, $callee, $site) = @_; my ($count, $tla) = @$site; $row->[2] = _foldedRowTla($row->[2], $row->[1], $tla, $count); $row->[1] = lcovutil::add_site_count($row->[1], $count); push(@{$row->[0]}, $callee) unless grep({ $_ eq $callee } @{$row->[0]}); } sub _foldedRowTla($$$$) { # The category of a row once one more record is folded into it: the # higher-priority of the two - except that the category has to agree with # the count. A row which any of its records took is hit, so only the # categories of the records which were taken compete for it; else a row # which was taken could be called 'UIC' my ($rowTla, $rowCount, $tla, $count) = @_; return $rowTla unless defined($tla); return $tla unless defined($rowTla); my $wasHit = lcovutil::site_count_hit($rowCount); my $isHit = lcovutil::site_count_hit($count); return $tla if $isHit && !$wasHit; return $rowTla if $wasHit && !$isHit; return $SummaryInfo::tlaRank{$tla} < $SummaryInfo::tlaRank{$rowTla} ? $tla : $rowTla; } sub _callSiteGroupMap($$;$) { # 'callSiteGroups' without the ordering: the same groups keyed by the # location they are at, as "::". # Separate from the sort because the caller-side position index is a lookup # table and has no order to pay for - see '_callSitePositions' - and # sorting the site keys of a function which contains thousands of calls is # the expensive half of building them # The key names the file even for the ordinary same-file site, where the # group itself carries undef: 'FunctionEntry::addCallSite' normalizes the # site's own file away when it is the function's, so the two spellings # cannot both occur and one key is unambiguous # '$union' asks for the categories of the sites each row counts as well - # see 'callSiteGroups' my ($entry, $callerFile, $union) = @_; my %groupAt; # "file:line:column" -> the group being built my %rowAt; # "file:line:column" -> destination -> row # in whatever order the sites come out: a group is identified by its # location and a row within it by its destination, so nothing here depends # on the order and the two sorts below put the result in the displayed one foreach my $siteKey ($entry->callSiteKeys()) { my ($line, $column, $ordinal, $count, $callee, $siteFile, $set) = $entry->callSiteData($siteKey); my $at = (defined($siteFile) ? $siteFile : $callerFile) . ":$line:$column"; # ..and which definition the callee resolves to is decided by the # translation unit, which is where the *caller* is defined - not by the # file the call is written in, which a header included by two units does # not answer. Same rule as TraceFile::resolveCallSites my $where = calleeLocation($callee, $callerFile); $groupAt{$at} = [$line, $column, [], $siteFile] unless exists($groupAt{$at}); # the destination: where the callee is defined when the report has it, # and the symbol itself when it does not. That is what the row links # to, and so what identifies it. The prefix keeps a symbol which # happens to look like 'file:line' out of the other namespace my $destination = defined($where) ? "@" . $where->[0] . ':' . $where->[1] : '=' . $callee; my $tla = $entry->callSiteTla($line, $column, $ordinal, $siteFile); my $row = $rowAt{$at}->{$destination}; if (defined($row)) { _mergeSiteRow($row, $callee, [$count, $tla]); } else { $row = [[$callee], $count, $tla, $where, {}]; $rowAt{$at}->{$destination} = $row; push(@{$groupAt{$at}->[2]}, $row); } $row->[6]->{$tla} = 1 if $union && defined($tla); # ..and which aliases made the calls the row counts, with the count of # each alias set: the empty set is every alias. What an alias group # of the function shows of the row - see '_aliasGroupSites' my $bySet = $row->[4]; $set //= ''; $bySet->{$set} = lcovutil::add_site_count($bySet->{$set}, $count); } # ..in symbol order within a group. Not ordinal order, which is the # producer's business, and not count order, which would reshuffle the table # between two runs of the same report. It is the row's position in this # order that the two sides of the table both name, so it is part of building # a group and not of displaying one # ..the row's own symbols first, so that the name a row is ordered by is the # one it displays first. A row is named by every symbol which leads to its # destination and no symbol leads to two destinations, so the lowest of them # is unique within the group and this is a total order however the sites # happened to come out of the container foreach my $group (values(%groupAt)) { foreach my $row (@{$group->[2]}) { @{$row->[0]} = sort(@{$row->[0]}); } @{$group->[2]} = sort({ $a->[0]->[0] cmp $b->[0]->[0] } @{$group->[2]}); } return \%groupAt; } sub callSiteGroups($$;$) { # The 'calls made by this function' view: the sites contained in one # FunctionEntry, as a list of location groups # [line, column, [row, ..], file the call is written in or undef] # in (file, line, column) order, each row being # [[callee, ..], count, tla or undef, [file, line] of the callee or undef, # {alias set or '' => count}] # in callee-symbol order - and, where '$union' is set, a 7th element: # the categories of the sites the row counts, as {tla => 1} - see # _unionSiteTla. # The group's file is undef for a call written in the file which defines the # function - the ordinary case - and the name of the other file otherwise, # which is what a call in an '#include'd fragment looks like. It is part of # the location, so two files with a call at one line and column are two # groups. # One row per distinct hyperlink destination, not one per record. The # ordinal is a storage key - it exists so that two inputs describing the # same location merge deterministically - and it is neither displayed nor # navigable, so two records at one location which lead to the same place are # one row with their counts added. That covers two different symbols which # share a definition location, which is what identical code folding and two # instantiation aliases of one FunctionEntry produce my ($entry, $callerFile, $union) = @_; # a total order, the groups being one per location: the same table every run return sort({ (defined($a->[3]) ? $a->[3] : '') cmp (defined($b->[3]) ? $b->[3] : '') or $a->[0] <=> $b->[0] or $a->[1] <=> $b->[1] } values(%{_callSiteGroupMap($entry, $callerFile, $union)})); } # How much of the numbering '%callSitePositions' holds is worth keeping. It is # not kept per page: a function which calls many things is a caller on the page # of every file it calls into, and re-indexing it for each of them is most of # the work the index saved. It is not unbounded either, since the callers of a # whole report are eventually every function in it - so it is emptied once the # answers in it cover this many sites, a few tens of MB, and the hub which is # asked about constantly is simply indexed again. # A test lowers it, so that a fixture of a handful of sites reaches the emptying # and can show that the numbering does not depend on what is kept - the same # thing LCOV_FORCE_PARALLEL does for the fork paths. Read at compile time # because that is when a constant is made, which is early enough use constant CALLSITE_INDEX_LIMIT => exists($ENV{LCOV_CALLSITE_INDEX_LIMIT}) ? $ENV{LCOV_CALLSITE_INDEX_LIMIT} : 200000; sub _callSitePositions($$) { # where in its group the row for one callee is, for every site of one caller. # Every callee of a file asks this of every function which calls it, and a # function which calls thousands of things answers it thousands of times # with the same answer, so it is computed once per caller rather than once # per row: recomputing it per row is the product of the two and is what # made the page of a file called from one large function quadratic. # Still computed from the caller's own sites and not stored beside them, so # that the two sides of the table cannot disagree about the numbering my ($entry, $callerFile) = @_; # ..identified by what a FunctionEntry is identified by everywhere else. Not # by reference: the same C++ object reached twice through the XS layer is # not necessarily the same Perl scalar my $key = join("\0", $entry->file(), $entry->line(), $entry->name(), $callerFile); return $callSitePositions{$key} if exists($callSitePositions{$key}); my %positions; my $groups = _callSiteGroupMap($entry, $callerFile); foreach my $at (keys(%$groups)) { my $n = 0; foreach my $row (@{$groups->{$at}->[2]}) { ++$n; # a symbol resolves to one destination and so names one row of its # group; the check is what keeps a malformed one from renumbering foreach my $callee (@{$row->[0]}) { $positions{$at}->{$callee} = $n unless exists($positions{$at}->{$callee}); } } } if ($callSitePositionSites > CALLSITE_INDEX_LIMIT) { %callSitePositions = (); $callSitePositionSites = 0; } $callSitePositions{$key} = \%positions; $callSitePositionSites += $entry->numCallSites(); return \%positions; } sub callgraphSourceGroups($;$) { # every call-site group written in one file, keyed by the source line it is on # and in column order within the line. One line can carry the sites of more # than one function - a macro expansion, or a lambda body - so this is not # simply one function's groups # '$foreign' is the sites which are written in this file and belong to a # function of some other one, which is what a call in an '#include'd # fragment is: the file's own FunctionMap does not have them, so the parent # collects them for every file before the pages are written - see # 'build_callgraph_foreign_index'. By the same token a site of *this* # file's functions which is written in another file is not here; it is on # that file's page my ($functionMap, $foreign) = @_; my %byLine; foreach my $key ($functionMap->keylist()) { my $entry = $functionMap->findKey($key); next if 0 == $entry->numCallSites(); foreach my $group (callSiteGroups($entry, $entry->file())) { next if defined($group->[3]); push(@{$byLine{$group->[0]}}, $group); } } foreach my $group (@{$foreign || []}) { push(@{$byLine{$group->[0]}}, $group); } foreach my $line (keys(%byLine)) { @{$byLine{$line}} = sort({ $a->[1] <=> $b->[1] } @{$byLine{$line}}); } return \%byLine; } sub _calleeSimpleName($) { # the identifier a reader sees at a call site: no parameter list, no # template arguments, no qualification. 'a::b::f(int) const' -> 'f'. # Used only to compare the source text against the site's targets, so it # does not have to be reversible - just consistent between the two my $name = shift; $name =~ s/\s*\(.*$//s; # parameter list, and const/noexcept 1 while ($name =~ s/<[^<>]*>//g); # template arguments, innermost first $name =~ s/^.*:://s; # qualification return $name; } sub _calleeIdentifierAt($$) { # the identifier which starts at 1-based column '$column' of '$text', as # (offset, length), or the empty list when there is none there. Read # rightward over the characters a qualified C++ name can contain, taking a # balanced '<...>' - which nests - as part of the name. Whitespace ends # the name, which is what keeps 'a < b' from being read as a template my ($text, $column) = @_; return () if ($column < 1 || $column > length($text)); my $start = $column - 1; return () unless substr($text, $start, 1) =~ /[A-Za-z_~]/; my $end = $start; while ($end < length($text)) { my $c = substr($text, $end, 1); if ($c =~ /[A-Za-z0-9_:~]/) { ++$end; } elsif ($c eq '<') { my $depth = 0; my $p = $end; while ($p < length($text)) { my $d = substr($text, $p, 1); ++$depth if $d eq '<'; --$depth if $d eq '>'; ++$p; last if 0 == $depth; } return () if 0 != $depth; # unbalanced - not an argument list $end = $p; } else { last; } } return ($start, $end - $start); } sub _calleeIdentifierEndingAt($$) { # the identifier which ends just left of 1-based column '$column' of # '$text', as (offset, length), or the empty list when there is none # there. This is where GCC's C++ front end points a call: the column of # an edge in the callgraph info is the call's opening parenthesis, so the # name the reader sees ends at the column before it. (Its C front end # points at the name itself, which is what '_calleeIdentifierAt' reads - # measured on both, and the difference is why this has to try twice) # A template argument list belongs to the name and is taken with it, and # whitespace between the name and the parenthesis is skipped my ($text, $column) = @_; my $end = $column - 1; # one past the last character of the name return () if ($end < 1 || $end > length($text)); --$end while ($end > 0 && substr($text, $end - 1, 1) =~ /\s/); return () if 0 == $end; # nothing but whitespace to the left my $nameEnd = $end; if ('>' eq substr($text, $nameEnd - 1, 1)) { my $depth = 0; while ($nameEnd > 0) { my $c = substr($text, $nameEnd - 1, 1); ++$depth if $c eq '>'; --$depth if $c eq '<'; --$nameEnd; last if 0 == $depth; } return () if 0 != $depth; # unbalanced - not an argument list } my $start = $nameEnd; --$start while ($start > 0 && substr($text, $start - 1, 1) =~ /[A-Za-z0-9_~]/); return () if $start == $nameEnd; return () unless substr($text, $start, 1) =~ /[A-Za-z_~]/; return ($start, $end - $start); } sub _calleeSpanIsTarget($$$$$) { # do '$length' characters of '$text' at '$start' name one of the callees of # '$group', without overlapping the text already spliced ('$consumed')? my ($text, $start, $length, $consumed, $group) = @_; return 0 unless defined($start) && $start >= $consumed; my $name = _calleeSimpleName(substr($text, $start, $length)); return scalar( grep({ _calleeSimpleName($_) eq $name } _callgraphGroupTargets($group)) ); } our %CALLEE_KEYWORDS; # see _calleeIsKeyword BEGIN { # The words the relaxed read below must not underline. Not a complete # keyword list of any language - only the ones which turn up immediately # left of a call in real code, which is what that read looks at. Measured # on this project's own C++: of 134 spans the relaxed read added, 8 were # one of these ('return' six times, 'true' twice) and the rest named the # operand an operator call is written on my @keywords = qw(return new delete throw sizeof this nullptr true false if else while for do switch case default break continue goto and or not const static struct class union enum typedef void auto operator co_await co_return co_yield); %CALLEE_KEYWORDS = map({ ($_ => 1) } @keywords); } sub _calleeIsKeyword($) { # is this span of source text a keyword rather than a name? A call is # frequently written just right of one - 'return f()' - so the relaxed read # would otherwise underline the keyword and link it to the callee, which # reads as nonsense. The verified reads above need no such guard: a # keyword does not name a callee my $text = shift; return exists($CALLEE_KEYWORDS{$text}) ? 1 : 0; } sub _calleeRelaxedSpan($$$) { # Where the link for one site group belongs when no text on the line names # any of its callees: the identifier the column points at, taken on trust. # The name a call is written with is not always the callee's name - an # operator call is written as its operands, a call through a pointer or a # virtual call is written as the expression which produces the target, and # a call whose column GCC folded onto an enclosing one points at that one - # and in every one of those cases the site's own '.func' entry is still what # the reader wants to reach from the place the call is written. So the # compiler's column is believed rather than second-guessed: the alternative # is the trailing comment, which for a C++ report is most of the sites. # The C++ convention is tried first (the identifier ending just left of the # column, which is where an operand ends) because it is the one which # applies when a verified read has already failed my ($text, $group, $consumed) = @_; foreach my $read (\&_calleeIdentifierEndingAt, \&_calleeIdentifierAt) { my ($start, $length) = $read->($text, $group->[1]); next unless defined($start) && $start >= $consumed; next if _calleeIsKeyword(substr($text, $start, $length)); return ($start, $length); } return (); } sub _calleeSpliceSpan($$$) { # Where on '$text' the link for one site group belongs, as (offset, length), # or the empty list when the line has no text the link can go on - in which # case the site goes in the trailing comment. # The column is either the first column of the name or the column of the # call's opening parenthesis, depending on the compiler's front end, so # both are tried. GCC also folds a nested call's column onto the enclosing # call, so the identifier found may name a different function - hence the # check that it is really one of the callees, which is what makes these two # reads the trustworthy ones. # When neither of them matches, the fallback is 'strict_callee_match': set, # the site goes to the comment; clear (the default), the column is # believed anyway - see _calleeRelaxedSpan for why that is the better # answer for the many C++ calls whose source text is not the callee's name my ($text, $group, $consumed) = @_; my ($start, $length) = _calleeIdentifierAt($text, $group->[1]); return ($start, $length) if _calleeSpanIsTarget($text, $start, $length, $consumed, $group); ($start, $length) = _calleeIdentifierEndingAt($text, $group->[1]); return ($start, $length) if _calleeSpanIsTarget($text, $start, $length, $consumed, $group); return () if $lcovutil::strict_callee_match; return _calleeRelaxedSpan($text, $group, $consumed); } sub _callgraphCountUnknown($) { # is this the unknown call-site count (see FunctionEntry::SITE_COUNT)? # Spelled out rather than written as '$count eq "-"' at each of the places # which ask, because the answer for an absent count is 'no' and a bare # 'eq' against undef is a warning my $count = shift; return defined($count) && $count eq '-'; } sub _callgraphCellStyle($) { # how a call-site cell is coloured by its count alone: taken, not taken, or # - for the unknown count - neither of the two, since colouring it as not # taken would make a claim about the tests which the capture never made my $count = shift; return 'coverCallgraphUnknown' if _callgraphCountUnknown($count); return lcovutil::site_count_hit($count) ? 'coverCallgraphHi' : 'coverCallgraphLo'; } sub _callgraphCountCell($$$$) { # the count cell of either call-site table. '-' is rendered as itself, the # way the unknown 'taken' of a 'BRDA:' record is rendered in the source # view, and with a tooltip saying what it means: a '-' in a column of # numbers is a question. It is coloured and toured as a zero, though: no # test is known to have made the call - see _callgraphRowStyle # In a report whose only categories are 'hit' and 'missed' this cell is also # the answer to that question - it is coloured by the category already, and # the category is exactly whether the number beside it is zero - so the # category column is left out and the word it carried comes here, into the # tooltip. See the '$foldTlas' of 'write_callgraph_page' # '$tla' is the category this cell has taken over, and is passed only where # there is no column for it. '$href' is where the count leads: past the # run of calls it is in to the next one which went the same way - the next # taken call after a run of taken ones, the next missed one after a run of # missed ones. See _callgraphHitKey my ($count, $style, $tla, $href) = @_; my $unknown = _callgraphCountUnknown($count); my $text = $unknown ? '-' : $count; my $label = _callgraphTlaLabel($tla); my $title = $label; $title = 'the compiler recorded this call and the coverage data carries no ' . 'count for it' . (defined($label) ? " - $label" : '') if $unknown; # the tooltip goes on the link: one under the other is one the reader # cannot reach. Every count is on one of the two tours, so every count # cell has a link my $class = defined($tla) ? " class=\"tlaBg$tla\"" : ''; my $next = 'click for the next ' . _callgraphCountKey($count) . ' call'; $next = "$title - $next" if defined($title); return "$text"; } sub _callgraphExternalCountCell($$) { # the count cell of an '' row: 'E (R)', where E is the # number of calls the flagged sites outside the callgraph boundary made # and R the number deduced from the entry count, joined by a space which # does not break. Either is left out where that fact does not hold - so # '(R)' alone is a function with no flag, and '0' one whose flagged sites # made no call. E is '-' where it is not known, and R is never known # then. Coloured like any other # call count, by E + R - a flagged symbol which was never entered has a # known number of external calls, and that number is zero - but never a # link, since the origin of those calls is not on any page. On a page # whose counts are coloured by category the row has none, and # 'callgraphTlaCount' gives the cell the look of the category cells above # it rather than that of the call site cells beside it my ($counts, $hasTlas) = @_; my ($flagged, $external, $residual) = @$counts; my $class; my $text; my $title; if ($flagged && !defined($external)) { ($class, $text, $title) = ('coverCallgraphUnknown', '-', $EXTERNAL_COUNT_TITLE); } elsif (!defined($residual)) { ($class, $text, $title) = (_callgraphCellStyle($external), $external, "$external calls from call sites outside" . ' the callgraph boundary'); } elsif (!$flagged) { ($class, $text, $title) = (_callgraphCellStyle($residual), "($residual)", "($residual) calls deduced from the" . ' number of times the function was' . ' entered'); } else { my $total = lcovutil::add_count($external, $residual); ($class, $text, $title) = (_callgraphCellStyle($total), "$external ($residual)", "$total calls: $external from call sites" . ' outside the callgraph boundary and' . " ($residual) deduced from the number" . ' of times the function was entered'); } $class .= ' callgraphTlaCount' if $hasTlas; return "$text"; } sub _callgraphGroupCount($) { # the one count which stands for a whole group of destinations at one call # location: their sum, except that an unknown among destinations which # were all not taken leaves the group unknown rather than not taken. # "some call here was taken" is answerable from any one destination; "no # call here was taken" needs a count for every one of them my $rows = shift; my $total; my $unknown = 0; foreach my $row (@$rows) { $unknown = 1 if _callgraphCountUnknown($row->[1]); $total = lcovutil::add_site_count($total, $row->[1]); } return '-' if $unknown && !lcovutil::site_count_hit($total); return $total; } sub _callgraphGroupTargets($) { # every symbol the group leads to, in display order: the tooltip text, and # what the source identifier is checked against my $group = shift; my @names; foreach my $row (@{$group->[2]}) { push(@names, @{$row->[0]}); } return @names; } sub _callSiteClass($) { # how the identifier at a call site - or the site's name in the trailing # comment - is marked: taken, not taken, or not measured my $count = shift; return 'callSiteUnknown' if _callgraphCountUnknown($count); return lcovutil::site_count_hit($count) ? 'callSite' : 'callSiteLo'; } sub _callgraphGroupNames($) { # the escaped, simplified names a site group leads to, as one string my $group = shift; return escape_html(join(', ', map({ simplify_function_name($_) } _callgraphGroupTargets($group)) )); } sub _callgraphGroupCalls($) { # the escaped hover list of a site group: one line per destination - # 'count: names', the counts right-aligned so that the names start in one # column - joined by an encoded newline. An attribute value, which CSS # draws on hover: text in the line would come back from 'html2lcov' as # source, and a 'title' is one line of plain text, which can neither align # the names nor tell one line from the next my $group = shift; my @lines = map( { [$_->[1], join(', ', map({ simplify_function_name($_) } @{$_->[0]})) ] } @{$group->[2]}); my $width = 0; foreach my $l (@lines) { $width = length($l->[0]) if length($l->[0]) > $width; } return join(' ', map({ escape_html(sprintf('%*s: %s', $width, @$_)) } @lines)); } sub _callgraphGroupPage($$) { # which page's call-site table one group of the source view leads to: this # file's own, except for a call which belongs to a function of another file, # whose page 'write_source' resolved when it collected the group my ($page, $group) = @_; return defined($group->[3]) ? $group->[4] : $page; } sub _callgraphGroupHrefAttr($$) { # the 'href' attribute of a source view link to one group's row of the call # site table, or '' where that table does not have it: the link is still # written, because its hover list and its colour are about the call and not # about where it leads. A group of a function of another file is on that # file's page, which - where the filter leaves part of a file out - # this file cannot know what was and was not removed. my ($page, $group) = @_; my $anchor = _callSiteAnchor($group->[0], $group->[1], undef, $group->[3]); if (defined($pageShown)) { return '' if defined($group->[3]) || !_pageShows($pageShown->{file}, 'callees', $anchor); } return ' href="' . _callgraphGroupPage($page, $group) . "#$anchor\""; } sub _callgraphAnnotateSource($$) { # The sites on a line whose column holds no identifier to link - the # implicit destructor call at a closing brace, a nested call whose column # GCC folded onto the enclosing one - as a comment appended to the line, # one link per site. '' when there are none. # Every character the reader sees here is generated by CSS out of the # attributes: 'html2lcov' strips tags but keeps their text, and a comment # written as text would come back as part of the source line - which it is # not my ($page, $groups) = @_; return '' unless @$groups; my $html = ''; foreach my $group (@$groups) { $html .= ''; } return $html . ''; } sub callgraphDecorateSource($$$) { # Decorate one source line with its call sites: the identifier at each # verified site column wrapped in a link to that site's group in the 'calls # made by this function' table, and the sites which did not verify appended # as a comment. undef when the line has no site. # The splice happens before escaping, not after: columns index the raw line # and 'escape_html' changes its byte offsets, so each piece is escaped # independently and the anchor is put between them my ($text, $cbdata, $line) = @_; my $groups = $cbdata->callSites()->{$line}; return undef unless defined($groups) && @$groups; my $page = $cbdata->callgraphPage(); my @spliced; # [offset, length, group], left to right my @unverified; # the sites with no identifier at their column my $consumed = 0; foreach my $group (@$groups) { my ($start, $length) = _calleeSpliceSpan($text, $group, $consumed); if (defined($start)) { push(@spliced, [$start, $length, $group]); $consumed = $start + $length; } else { push(@unverified, $group); } } my $annotation = _callgraphAnnotateSource($page, \@unverified); my $html = ''; my $pos = 0; foreach my $s (@spliced) { my ($start, $length, $group) = @$s; my $rows = $group->[2]; # hovering answers "which ones, how often" with no navigation and no # second copy of the data. Every attribute value spliced into a source # line is escaped: a bare '>' in a template argument would end the tag # early, and 'html2lcov' recovers the source by stripping tags my $calls = _callgraphGroupCalls($group); my $class = _callSiteClass(_callgraphGroupCount($rows)); $html .= escape_html(substr($text, $pos, $start - $pos)); $html .= '" . escape_html(substr($text, $start, $length)) . ''; # a site with more than one destination is marked with how many, so that # the reader knows before clicking that there is a set to look at. # The digit is rendered by CSS from 'data-n' rather than written as # text: 'html2lcov' strips tags but keeps their content, and a text # digit spliced into the source would come back as source $html .= '' if 1 < scalar(@$rows); $pos = $start + $length; } $html .= escape_html(substr($text, $pos)); $html .= $annotation; return $html; } sub _foldCallerRow($$$) { # another call from the location of a caller row: the row counts both, and # takes the more significant of the two categories - see _foldedRowTla my ($row, $count, $tla) = @_; $row->[5] = _foldedRowTla($row->[5], $row->[4], $tla, $count); $row->[4] = lcovutil::add_site_count($row->[4], $count); } sub _mergeCallerLists(@) { # The caller lists of the aliases of one function as a single list - shown # by the page under '--filter function', where the function table # lists the function under one name. A location which called two of the # aliases is one row, with the sum of the counts, as two calls of one alias # from one location are (see callgraphCallerRows); its link goes to the # first of the caller's rows there. The classes combine as the note beside # the name combines them (see _callgraphCallerNote), and the counts of the # calls from elsewhere add up - unknown if any of them is my @lists = @_; my ($class, $external, $deduced, $flagged) = (lcovutil::CALLERS_NONE, 0, 0, 0); my (@rows, %rowAt); foreach my $list (@lists) { my ($aliasClass, $calls, $aliasRows) = @{$list}[2 .. 4]; $class |= $aliasClass; $external = (defined($external) && defined($calls->[0])) ? $external + $calls->[0] : undef; $deduced = (defined($deduced) && defined($calls->[1])) ? $deduced + $calls->[1] : undef; $flagged ||= $calls->[2]; foreach my $row (@$aliasRows) { my $at = join(':', @{$row}[0 .. 2]); if (exists($rowAt{$at})) { my $here = $rowAt{$at}; _foldCallerRow($here, $row->[4], $row->[5]); $here->[6] = $row->[6] if defined($row->[6]) && (!defined($here->[6]) || $row->[6] < $here->[6]); @{$here->[9]}{keys(%{$row->[9]})} = () if defined($row->[9]); next; } $rowAt{$at} = [@$row]; # ..its own copy of the categories, which the folding above adds to $rowAt{$at}->[9] = {%{$row->[9]}} if defined($row->[9]); push(@rows, $rowAt{$at}); } } return [undef, undef, $class, [$external, $deduced, $flagged], [ sort({ $a->[0] cmp $b->[0] or $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @rows) ] ]; } sub _unionSiteTla($$) { # The category of one unique call site of a function shown as alias groups # - see _showAliasGroups - the category its sites would have as one site: # the sum of their counts, '$count', vs the sum of their baseline counts. # Computed from '$tlas', the categories the sites have # ({tla => 1}), rather than from the counts, because the category says # what 'FileCoverageInfo::_categorize' asks - whether each is 0. # So the baseline sum is non-zero where a site is LBC or CBC, and there is # a baseline site at all where one is UBC, GBC, LBC or CBC. # The sites are located in the new code (or in a changed statement) for # all the sites or none of the sites: where one is GNC or UNC, so is # the union. undef where no site has a category my ($tlas, $count) = @_; return undef unless %$tlas; my $hit = lcovutil::site_count_hit($count) ? 1 : 0; return ($hit ? 'GNC' : 'UNC') if exists($tlas->{GNC}) || exists($tlas->{UNC}); return FileCoverageInfo::_categorize((exists($tlas->{LBC}) || exists($tlas->{CBC})) ? 1 : 0, $hit ) if grep({ exists($tlas->{$_}) } ('UBC', 'GBC', 'LBC', 'CBC')); return $hit ? 'GIC' : 'UIC'; } sub _summaryRowCmp($$) { # The order of the summary rows of a function shown as alias groups: the # most significant category first, in @SummaryInfo::tlaPriorityOrder # order (a row with none last), tie break to lowest count (the least # exercised, unknown count being 0), tie break to the name shown # in the table element (file, line, ordinal). # Each of the two is [tla or undef, count, file, line, column, name] my ($x, $y) = @_; my ($rx, $ry) = map({ defined($_->[0]) ? $SummaryInfo::tlaRank{$_->[0]} : scalar(@SummaryInfo::allTla) } ($x, $y)); return $rx <=> $ry || lcovutil::site_count_value($x->[1]) <=> lcovutil::site_count_value($y->[1]) || $x->[2] cmp $y->[2] || $x->[3] <=> $y->[3] || $x->[4] <=> $y->[4] || $x->[5] cmp $y->[5]; } sub _callerSummary($) { # The summary block of the callers page for a function shown with its alias # groups: the caller lists '$callers' of its groups as one list, as # _mergeCallerLists makes it - a row per unique call site, with the # sum of the counts there, and the calls from outside the callgraph # boundary combined - each row in the category of that sum # (see _unionSiteTla), in _summaryRowCmp order my $callers = shift; my $summary = _mergeCallerLists(@$callers); my $rows = $summary->[4]; $_->[5] = _unionSiteTla($_->[9], $_->[4]) foreach (@$rows); @$rows = map({ $_->[1] } sort({ _summaryRowCmp($a->[0], $b->[0]) } map({ [[@{$_}[5, 4, 0 .. 2], ''], $_] } @$rows))); return $summary; } sub _calleeSummary($) { # ..and of its callees page: the call site groups '$groups' of the # function - see callSiteGroups - a row per unique call site, and # the callee the row leads to, with the count of every call there. # Each row is a group of its own, in the category of its count # (see _unionSiteTla) and keeping the position it has among the # function's rows - in _summaryRowCmp order. my $groups = shift; my @rows; foreach my $group (@$groups) { my ($line, $column, $sites, $siteFile) = @$group; my $position = 0; foreach my $row (@$sites) { ++$position; my $tla = _unionSiteTla($row->[6] // {}, $row->[1]); push(@rows, [ [$tla, $row->[1], $siteFile // '', $line, $column, $row->[0]->[0] ], [$line, $column, [[$row->[0], $row->[1], $tla, $row->[3], undef, $position]], $siteFile ] ]); } } return [map({ $_->[1] } sort({ _summaryRowCmp($a->[0], $b->[0]) } @rows))]; } sub callgraphCallerRows($;$) { # The 'callers of this function' view: the sites which target one # FunctionAlias, as a list of # [site file, line, column, caller symbol, count, tla or undef, group, # caller's own file, the names the caller symbol represents or undef] # in (file, line, column) order - and, where '$union' is set, a 10th # element: the categories of the sites the row counts, as {tla => 1} - # see _unionSiteTla. 'group' is the 1-based position of this # destination in the caller's own site group - the anchor to link back # to. One row per call location, for the same reason as the other table: # every ordinal at one location in one caller links to the same anchor # The first element is where the call is *written* and the last is where the # function which makes it is defined. Those are two different files for a # call in an '#include'd fragment, and the row needs both: the location # leads to the source line and the caller's name to the table of everything # called at that site, which is on the page of the file which contains it my ($alias, $union) = @_; my @rows; my %rowAt; foreach my $caller ($alias->callers()) { my ($entry, $line, $column, $ordinal, $count, $siteFile) = @$caller; my $owner = $entry->file(); my $file = defined($siteFile) ? $siteFile : $owner; my $at = "$file:$line:$column"; my $tla = $entry->callSiteTla($line, $column, $ordinal, $siteFile); if (exists($rowAt{$at})) { _foldCallerRow($rowAt{$at}, $count, $tla); $rowAt{$at}->[9]->{$tla} = 1 if $union && defined($tla); next; } # which row of the caller's group this is. Read out of the caller's own # numbering rather than stored, so that the two sides cannot disagree my $numbering = _callSitePositions($entry, $owner)->{"$file:$line:$column"}; my $position = defined($numbering) ? $numbering->{$alias->name()} : undef; my $row = [$file, $line, $column, $entry->name(), $count, $tla, $position, $owner, _aliasMerged($entry, $entry->name()) ]; $row->[9] = defined($tla) ? {$tla => 1} : {} if $union; $rowAt{$at} = $row; push(@rows, $row); } return sort({ $a->[0] cmp $b->[0] or $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @rows); } sub _callgraphRowStyle($$) { # how one call-site row is coloured: by its differential category when there # is one - the same per-TLA CSS the line and function tables use - and by # taken/not-taken otherwise. The unknown count is binned as not taken (see # 'categorizeCallSites') and is coloured that way too: no test is known to # have made the call. The row of a call which came from outside the # boundary of the report is not a call site and is not coloured here - see # _callgraphCallerCells my ($count, $tla) = @_; return "tla$tla" if defined($tla); return 'coverCallgraphLo' if _callgraphCountUnknown($count); return _callgraphCellStyle($count); } sub _callgraphTlaLabel($) { # what one category is called on these pages: its TLA, or the word a report # with no baseline calls it - where the only two categories are 'hit' and # 'missed', 'GNC' and 'UNC' are differential jargon for a reader who asked # for no differential my $tla = shift; return undef unless defined($tla); return $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} : $tla; } sub _callgraphTlaCell($$$$) { # The category cell of one row, or '' in a report which has no categories. An # empty cell rather than no cell when the row itself has no category, which # is a site the categorizer never saw. # The cell leads to the start of the next group of its own category, which is # what the same cell of the source view leads to: a reader who is following # one category wants the next place it appears, not the next row. # '$span' is how many rows the cell covers - see _callgraphRowRuns - and is 0 # on a row whose cell some row above it wrote, which then writes none my ($showTlas, $tla, $href, $span) = @_; return '' unless $showTlas; return '' unless defined($tla); return '' unless $span; my $label = _callgraphTlaLabel($tla); my $cell = $label; $cell = "" . "$label" if defined($href); # ..and the cell says which category it is, where the reader points at the # cell rather than at its link return "$cell" if 1 == $span; return "$cell"; } sub _callgraphTlaHead($) { # 'TLA' unconditionally: the caller writes this column only where the # categories are TLAs, a report whose two categories are 'hit' and 'missed' # having folded the word onto the count instead. See '_callgraphCountCell' my $showTlas = shift; return '' unless $showTlas; return 'TLA'; } sub _nextGroupIndex($$) { # Where the group link of row '$i' leads: past the whole contiguous run of # rows which share its key, to the next row which has that key again - which # is what makes these links a tour of one category, one author or one date # bin rather than a walk down the table. undef when there is no later one. # This is 'SourceFile::nextTlaGroup' asked of the rows of a table instead of # the lines of a file, and it treats a row with no key the way that does a # line with no category: such a row neither continues a run nor breaks one, # so a caption in the middle of a run does not turn it into two groups my ($keys, $i) = @_; my $key = $keys->[$i]; return undef unless defined($key); my $last = scalar(@$keys); my $next = $i + 1; while ($next < $last) { last if defined($keys->[$next]) && $keys->[$next] ne $key; ++$next; } while ($next < $last) { return $next if defined($keys->[$next]) && $keys->[$next] eq $key; ++$next; } return undef; } sub _callgraphGroupHref($$$) { # ..and what that link says, or undef for a row which has no key and so no # group to follow. The last group of a key leads to the top of the page, # exactly as the category and annotation links of the source view do my ($keys, $i, $anchors) = @_; return undef unless defined($keys->[$i]); my $next = _nextGroupIndex($keys, $i); return '#' . (defined($next) ? $anchors->[$next] : 'top'); } sub _callgraphHitKey($) { # Which of the two tours of the count column one row is on: 'hit' where the # call was taken, 'missed' where it was not - or is not known to have been # taken, which is the unknown count. undef for a row which has no count of # its own: it is on neither tour, and so does not break a run of the rows # around it either, for the reason a row with no category does not break a # run of categories (see _nextGroupIndex) my $row = shift; return undef unless exists($row->{count}); return _callgraphCountKey($row->{count}); } sub _callgraphCountKey($) { # ..which is a question about the count alone my $count = shift; return 'missed' if _callgraphCountUnknown($count); return $count > 0 ? 'hit' : 'missed'; } sub _callgraphUnhitTla($) { # The colour of one function's date and owner cells: the category of its # calls on this page which were not taken - the first of them in category # order where there are several, which is the order the rest of the report # ranks them in - and undef where every call of it was taken. Same rule as # the source view, which colours those two columns of a line only where the # line was not exercised: the colour is there to be scanned for, so it is # worth nothing if every row carries it. A count which is not known counts # as a call which was not taken, as it does everywhere on these pages my $rows = shift; my $first; foreach my $row (@$rows) { my $tla = $row->{tla}; next unless defined($tla) && 'missed' eq (_callgraphHitKey($row) || ''); $first = $tla if !defined($first) || $SummaryInfo::tlaRank{$tla} < $SummaryInfo::tlaRank{$first}; } return $first; } sub _callgraphMergedRuns($$) { # The cells of one of the function columns - the date or the owner - once # contiguous functions which share a key have been merged: a run of them is # one cell, written by its first function and spanning the rows of all of # them. Returns two lists indexed by function # - how many rows its cell spans, or 0 where a function above it wrote # the cell, and # - the function whose cell the cell of a run leads to: the first of the # next run with the same key, or undef where there is none, and # - whether the cell of a run is the shaded one of the two a column # alternates between. Counted by cell rather than by function, as the # name column is: a merged cell is one cell, and two neighbours with # the same shade would read as one run where they are two # A function with no key is a cell of its own which merges with nothing - # it is an empty cell, and two empty cells are two functions nothing is # known about rather than one thing known about both - and it ends the run # above it, which is what makes the next run with that key a separate one my ($keys, $heights) = @_; my (@spans, @starts); for (my $i = 0; $i < scalar(@$keys); ++$i) { my $key = $keys->[$i]; if (@starts && defined($key) && defined($keys->[$i - 1]) && $key eq $keys->[$i - 1]) { $spans[$starts[-1]] += $heights->[$i]; push(@spans, 0); next; } push(@starts, $i); push(@spans, $heights->[$i]); } my (@next, %later, @shaded); $shaded[$starts[$_]] = $_ % 2 foreach (0 .. $#starts); foreach my $start (reverse(@starts)) { my $key = $keys->[$start]; next unless defined($key); $next[$start] = $later{$key}; $later{$key} = $start; } return (\@spans, \@next, \@shaded); } sub _callgraphRowRuns($) { # ..and the same for the category column, which is a column of rows rather # than of functions: how many rows the category cell of each row spans, 0 # where a row above it wrote the cell. A row with no category ends a run, # because the cell it writes is in the way of the span - a caption spans # the column, and a call with no category has an empty cell of its own - # although it does not end the tour of the category, which is where the # cell leads (see _nextGroupIndex) my $keys = shift; my @spans; my $start; for (my $i = 0; $i < scalar(@$keys); ++$i) { my $key = $keys->[$i]; if (defined($key) && defined($start) && $key eq $keys->[$start]) { ++$spans[$start]; push(@spans, 0); next; } $start = defined($key) ? $i : undef; push(@spans, 1); } return \@spans; } sub _callgraphTotals(@) { # found/hit over the counts of the rows one function has on one page: how # much of that function's own direction of the callgraph the tests # exercised, which is the number the page's second order sorts by. # A row whose count is not known is found and not hit, as # 'FunctionEntry::numCallSitesHit' counts one and as its category says: the # row is binned and toured as not hit, and a proportion which left it out # would call the function fully exercised beside a row the same page # categorizes as missed. The row is a ceiling no additional test can lift, # which is what '--filter unknown_callee_count' is for. # Only rows which carry a count are passed: the row which says that a call # crossed the boundary of the report is in neither total my $found = scalar(@_); my $hit = scalar(grep({ lcovutil::site_count_hit($_) } @_)); return [$found, $hit]; } sub _rateOrder($$) { # The order of two proportions, '[found, hit]' each, in every table which is # sorted by one: the lowest first, which is where the reader who did not # come for a particular row should start - and of two equal proportions the # one with the larger denominator, which is more coverpoints left at that # rate. Nothing to divide - no proportion at all, or a zero denominator - # is last: 'nothing is known here' is not the same claim as 'nothing here # was exercised', and such a row has no proportion to show. # 0 where the two are the same, so that the caller can break the tie my ($x, $y) = @_; my $empty_X = (!defined($x) || 0 == $x->[0]) ? 1 : 0; my $empty_Y = (!defined($y) || 0 == $y->[0]) ? 1 : 0; return $empty_X <=> $empty_Y if $empty_X || $empty_Y; # compared without dividing, so that two equal proportions are equal return ($x->[1] * $y->[0] <=> $y->[1] * $x->[0]) || ($y->[0] <=> $x->[0]); } sub _callgraphHitCell($$) { # How much of this function's own direction of the callgraph the tests # exercised, beside its name and spanning its rows: the proportion the page # is ordered by, so that the reader who did not come looking for a # particular function can start with the least exercised one. # Written as the proportions of the function table are written, and coloured # by the same thresholds: it is the same question asked of a different set # of coverpoints, and a reader should not have to learn a second spelling. # Except that an empty denominator is written as the unknown count of a # single call site is - '-', and nothing else: '0 / 0' invites the reader # to divide two numbers which are not a proportion my ($metric, $height) = @_; my ($found, $hit) = @$metric; my $style = $rate_name[classify_rate($found, $hit, $callgraph_med_limit, $callgraph_hi_limit)]; my $rate = rate($hit, $found, ' %'); $rate .= " ($hit / $found)" if 0 != $found; return ' ' . " 1 ? " rowspan=\"$height\"" : '') . ">$rate\n"; } sub _callgraphXrefTitle($) { # ..and what that cell says when the reader stops on it: which list is one # click away my $other = shift; return 'Click to go to list of ' . $other->{title}; } sub _callSiteAnchor($$;$$) { # the fragment identifying one call-site group, or one row within it. Shared # by the source view, the group's own cell and the back-link from a # callee's 'called from' row, so that all three spell it the same way # '$file' is the file the call is written in, and is undef - which is the # ordinary case - when that is the file whose function contains it. Two # files can have a call at one line and column and both groups are on the # one page, so the file has to be part of the fragment when there is one. # Not reversible and not meant to be read; what it has to be is the same # string every run, which is why it is the whole path my ($line, $column, $position, $file) = @_; my $anchor = 'site'; if (defined($file)) { my $tag = $file; $tag =~ s/[^A-Za-z0-9]/_/g; $anchor .= "_$tag"; } $anchor .= "_${line}_$column"; $anchor .= "_$position" if defined($position); return $anchor; } sub _calleeFuncAnchor($) { # the fragment identifying one function on either call site page: the leader # name, which is the name both pages list it under, so that the source view, # the function table and the other direction can all lead to it without # knowing which page they are leading to my $func = shift; return 'func_' . escape_id($func); } sub _callerRowAnchor($$) { # the fragment identifying one caller row of one function. Nothing outside # the callers page leads to a row of it - a link into that page names either # a function or the whole page - so this exists for the category links, # which need somewhere within the table to land my ($func, $index) = @_; return 'caller_' . escape_id($func) . "_$index"; } sub _showAliasGroups($$) { # Are the aliases of FunctionEntry '$entry' shown as the groups which # TraceFile::_collapseCallgraphAliases left - each surviving alias the # group of the names merged into it, with a row of its own under the # function in the function table and on both call site pages? Only where # that collapse ran and found something to distinguish the aliases: the # callgraph is measured for the file - '$measured', see # FunctionMap::hasCallgraphData - and more than one of its aliases # survived. Anywhere else the aliases are names of one function, and # are shown as such my ($entry, $measured) = @_; return $lcovutil::callgraph_coverage && $lcovutil::collapse_callgraph_alias && $measured && !$main::suppress_function_aliases && $entry->numAliases() > 1; } sub _aliasGroupAnchor($) { # the fragment identifying the rows of one alias group on either call site # page - see '_showAliasGroups' - and so what its cells in the function # table lead to. Not the function's own anchor, although the function is # listed under the name of one of its groups my $alias = shift; return 'group_' . escape_id($alias); } sub _aliasGroupSites($$) { # the call site groups '$groups' of one function - see 'callSiteGroups' - as # alias group '$alias' made them: each row which counts a call it made, # with the count of its calls alone, and the position the row has among # all of the function's rows - used in the hyperlink from the other side # of the table. A call with no alias set is every alias's my ($groups, $alias) = @_; my @mine; my %names; foreach my $group (@$groups) { my ($line, $column, $rows, $siteFile) = @$group; my @rows; my $position = 0; foreach my $row (@$rows) { ++$position; my $count; my $made = 0; while (my ($set, $c) = each(%{$row->[4]})) { next unless '' eq $set || exists( ($names{$set} //= { map({ ($_ => 1) } FunctionEntry::aliasSetNames($set)) })->{$alias}); $count = lcovutil::add_site_count($count, $c); $made = 1; } push(@rows, [@{$row}[0], $count, @{$row}[2, 3], undef, $position]) if $made; } push(@mine, [$line, $column, \@rows, $siteFile]) if @rows; } return \@mine; } sub _aliasGroupMetrics($$$) { # [found, hit] of each direction of the callgraph, as the function table # counts them, for each alias group of FunctionEntry '$entry' - see # '_showAliasGroups': the calls into the group (the caller rows # '$callers' lists for it) and the call sites the group made, each site # once. undef if the group has no sites in that direction (no callers # or no callees). my ($entry, $callers, $groups) = @_; my %metric; foreach my $caller (@$callers) { my $alias = $caller->[5]; next unless defined($alias); my $m = _callgraphTotals(map({ $_->[4] } @{$caller->[4]})); $metric{$alias}->[CALLGRAPH_VIEW_CALLERS] = $m if 0 != $m->[0]; } foreach my $alias ($entry->aliasNames()) { my ($found, $hit) = (0, 0); foreach my $group (@{_aliasGroupSites($groups, $alias)}) { foreach my $row (@{$group->[2]}) { ++$found; ++$hit if lcovutil::site_count_hit($row->[1]); } } $metric{$alias}->[CALLGRAPH_VIEW_CALLEES] = [$found, $hit] if $found; } return \%metric; } sub _aliasGroupsInOrder($$$@) { # The alias groups '@groups' of function '$entry' - each an array whose # element '$at' is the alias - in the order the page of direction # '$view' lists them where there is no summary block above them (see # 'show_callgraph_summary' in lcovrc(5)): the least exercised first, # which is the one the reader is most likely to care about - by rate, # then by the larger denominator (see _rateOrder), then by name my ($entry, $view, $at, @groups) = @_; my $metric = $entry->{groupMetric}; return sort({ my ($x, $y) = map({ $metric->{$_->[$at]} } ($a, $b)); _rateOrder(defined($x) ? $x->[$view] : undef, defined($y) ? $y->[$view] : undef) or $a->[$at] cmp $b->[$at] } @groups); } sub _countedCallSites($$) { # The call sites of function '$entry' - see callgraphDetailEntries - which # the callgraph numbers of the report count, as [category, {age, owner} # or undef] each: the rows its callees page shows, and of a function # shown as alias groups, the rows of its summary block, or where there is # none, the rows of every group (see 'show_callgraph_summary' in # lcovrc(5)). A site several groups share is then counted by each of # them, as the page lists it in each. Every row has a category: genhtml # categorizes every site, differential report or not # The date and owner are those of the line of '$srcfile' which the site # is counted on - see SourceFile::_countCallgraphTlaData - which for a # site written in another file is the line its function starts on my ($entry, $srcfile) = @_; my @groups = defined($entry->{summarySites}) ? @{$entry->{summarySites}} : defined($entry->{aliasGroups}) ? map({ @{$_->[1]} } @{$entry->{aliasGroups}}) : @{$entry->{groups}}; my $annotate = @SourceFile::annotateScript; my @sites; foreach my $group (@groups) { my ($line, undef, $rows, $siteFile) = @$group; $line = $entry->{entry}->line() if defined($siteFile); my $annot; my $who = ($annotate && $line > 0 && $line <= $srcfile->num_lines()) ? $srcfile->attribution($line) : undef; $annot = { age => $who->age(), owner => $who->owner() } if defined($who) && defined($who->age()); push(@sites, map({ [$_->[2], $annot] } @$rows)); } return @sites; } sub _aliasGroupLeadCells($$$$$$$) { # The cells which introduce one alias group's rows on a call site page - see # '_showAliasGroups' - laid out in a row in the same way as the # function detail table: the proportion of the other direction its # rows there hold, its name, indented under the function's and # hyperlinked to the function definition, with the aliases that merged # into it in the tooltip of its superscript, and the proportion of this # page's direction its rows hold. # Each spans the rows of the group. '$group' is # [alias, the aliases merged into it, its number of rows], and '$xref' # is [the other direction, its page] if the page has that column, and # page is undef if the function does not appear on that page (e.g., # has no caller or callee sites). # Not its category, which is that of a function: the category column of # the page is that of a call. # Where there is no summary block - see 'show_callgraph_summary' in # lcovrc(5) - '$alternate' is the colour of the function in the # alternation of the page (see _calleeFuncCell), and the groups are # written as the function itself would be: not indented, in that colour, # and on the callers page with the note beside the name of each. The # first of them, '$first', holds the anchor of the function, so that a # link to the function leads to it. '$alternate' is undef where the # groups are below a summary block my ($entry, $group, $view, $xref, $source, $alternate, $first) = @_; my ($alias, $merged, $height, $caller) = @$group; my $plain = defined($alternate); my $metric = $entry->{groupMetric}->{$alias}; my $anchor = _aliasGroupAnchor($alias); my $html = ''; if (defined($xref)) { my ($other, $page) = @$xref; my $m = defined($metric) ? $metric->[1 - $view] : undef; $html .= _calleeXrefCell((defined($m) && defined($page)) ? "$page#$anchor" : undef, $other, $m // [0, 0], $height, $plain && $alternate); } my $line = _shownSourceLine($entry->{startline}, $entry->{startline} + $func_offset); my $class = !$plain ? 'calleeFnAlias' : $alternate ? 'calleeFnAlt' : 'calleeFn'; my $funcSpan = ($plain && $first) ? '' : ''; $html .= " 1 ? " rowspan=\"$height\"" : '') . " id=\"$anchor\">$funcSpan" . "' . escape_html(simplify_function_name($alias)) . '' . _mergedNameNote($alias, $merged); # ..and the note on the callers of the group, which is the function's # note where the summary block has it my $note = ($plain && defined($caller)) ? _callgraphCallerNote([$caller]) : undef; $html .= '
' . escape_html($note->[0]) . '' if defined($note); $html .= "\n"; my $rate = (defined($metric) && defined($metric->[$view])) ? $metric->[$view] : [0, 0]; return $html . _callgraphHitCell($rate, $height); } sub _callgraphCaptionCell($$) { # which alias' callers the rows below belong to, for a function which has # more than one: a row of its own, because the heading of a column cannot # say it and none of the columns is for it my ($caption, $span) = @_; return ' ' . $caption . "\n"; } sub _callgraphCallerNote($) { # Which side of the boundary of the report this function's callers are on, # said beside its name rather than left to be counted out of its rows: # which of internal, external or both is the question the whole metric is # asked for an application whose testcases are elsewhere. # A call from outside the report is either asserted by the 'x' flag or # inferred from the callee being entered more often than the sites here # account for. The aliases of a function are one function, so their # classes combine - which the numbering of the classes does directly. # 'Internal' here is a call site of this report which names the function, and # not the CALLERS_INTERNAL class: that class is arithmetic on counts, so a # site whose count is unknown is not in it, and a note reading 'external # only' beside a row which does name a caller would contradict the rows the # reader can see. # Nothing is said when every caller is a site in this report: the rows say # that already. See lcovutil::classify_callers my $callers = shift; my $mask = lcovutil::CALLERS_NONE; my $internal = 0; foreach my $caller (@$callers) { $mask |= $caller->[2]; $internal = 1 if @{$caller->[4]}; } if ($mask & lcovutil::CALLERS_EXTERNAL) { return $internal ? [$CALLERS_BOTH_NOTE, $CALLERS_BOTH_TITLE] : [$CALLERS_EXTERNAL_NOTE, $CALLERS_EXTERNAL_TITLE]; } return [$CALLERS_UNKNOWN_NOTE, $CALLERS_UNKNOWN_TITLE] if $mask & lcovutil::CALLERS_INDETERMINATE; return undef; } sub _aliasMerged($$) { # the names merged into the alias '$name' of '$entry' - undef if none, or if # the entry has no alias of that name my ($entry, $name) = @_; my $alias = $entry->alias($name); return defined($alias) ? $alias->merged() : undef; } sub _mergedNameNote($$) { # The mark beside a name which other names were merged into (see # TraceFile::_collapseCallgraphAliases): how many names it represents - # itself and '$merged', the others (see FunctionAlias::merged) - with the # names and hit count of each in the tooltip. Wherever the name is # shown, so that the reader who sees 'f' calling 'g' is told that # the line is about 'f' too. Empty where nothing was merged into it # The tooltip is a list in the page, shown on hover by CSS, rather than a # 'title': a browser's own tooltip is one line of plain text, which # can't align the names nor use background color to distinguish them. #. One row per name - 'count: name', the counts right-aligned so that # the names start in the same place - in alternating colors my ($name, $merged) = @_; return '' unless defined($merged); my @names = sort(keys(%$merged)); my $total = scalar(@names) + 1; return "$total" if 1 == $alias_list_length; my @shown = map({ [$merged->{$_}, simplify_function_name($_)] } $alias_list_length ? splice(@names, 0, $alias_list_length - 1) : splice(@names)); push(@shown, ['', '...']) if @names; my $alt = 0; my $rows = join('', map({ '' . ('' eq $_->[0] ? '' : $_->[0] . ':') . '' . escape_html($_->[1]) . '' } @shown)); return "$total" . '' . escape_html(simplify_function_name($name) . " stands for $total names - merged into it:") . "$rows"; } sub _calleeFuncCell($$$$$) { # The name cell of a function: its name, linked to its definition - which is # the same pair of jobs the function table gives that column - and the # anchor every link to this function on this page leads to. # It spans every row the function has, so that the extent of one function is # readable where its rows run on, and its colour alternates from function to # function for the same reason. my ($entry, $source, $view, $height, $alternate) = @_; my $name = escape_html(simplify_function_name($entry->{name})); # ..and the names merged into it, unless its aliases are shown as groups: # then they are on the row of the group they are in - or, where the # groups are not shown, here: every name of every group my $merged; if (!defined($entry->{aliasGroups})) { $merged = _aliasMerged($entry->{entry}, $entry->{name}); } elsif ($main::suppress_alias_groups) { $merged = {}; foreach my $group (@{$entry->{aliasGroups}}) { my ($alias, undef, $names) = @$group; $merged->{$alias} = $entry->{entry}->alias($alias)->count(); @{$merged}{keys(%$names)} = values(%$names) if defined($names); } delete($merged->{$entry->{name}}); } my $mark = _mergedNameNote($entry->{name}, $merged); my $line = _shownSourceLine($entry->{startline}, $entry->{startline} + $func_offset); my $cell = ' 1 ? " rowspan=\"$height\"" : '') . ' id="' . _calleeFuncAnchor($entry->{name}) . "\">$name$mark"; # the note is about the callers of the function, so it belongs beside them my $note = $view == CALLGRAPH_VIEW_CALLERS ? _callgraphCallerNote($entry->{callers}) : undef; $cell .= '
' . escape_html($note->[0]) . '' if defined($note); return $cell . "\n"; } sub _calleeXrefCell($$$$$) { # The other direction of the callgraph for one function, in a column of its # own: the two pages are one question asked twice, and a reader looking at # the callers of a function is one click from what it calls. A column # rather than a note under the name, so that the reader who wants the other # question always finds the answer in the same place. # The cell is the proportion of that other direction the tests exercised - # '$metric' - written and coloured as the 'Hit' column beside it is, and # linked to '$href', this function's rows on the page of the other # direction. Empty where there is no such row, or no denominator: a link # which lands on a page the function is not listed on leads nowhere, and # there is no honest percentage of nothing my ($href, $other, $metric, $height, $alternate) = @_; my ($found, $hit) = @$metric; my $span = $height > 1 ? " rowspan=\"$height\"" : ''; return ' \n" if !defined($href) || 0 == $found; my $style = $rate_name[classify_rate($found, $hit, $callgraph_med_limit, $callgraph_hi_limit)]; return ' ' . "' . rate($hit, $found, ' %') . " ($hit / $found)\n"; } sub _callgraphAnnotCells($$$$$) { # Who last changed a function and how long ago, in the two columns the source # view has at the left of every line: the same two questions asked of the # function, which is to say of the line its definition starts on. The age # is the number of days the source view shows, so that the two views are # read the same way; the owner is the author's full name rather than the # truncated login, a table cell having room for it. # A cell is a run of contiguous functions rather than one of them: those # which share the author, or the date bin, and the colour are one cell, # written by the first of them and spanning the rows of all of them - see # _callgraphMergedRuns, whose three lists per column '$columns' holds. The # age a merged cell shows is the most recent of the ages in it, which is # the answer to "how long since any of these was changed". # Each cell leads to the next cell of its own group, exactly as the same two # columns of the source view lead to the next line of one - past the run it # is, to the next run of that date bin or that author in that colour. The # colour is that of the calls of the function this page shows which were # not taken, and there is none where every one of them was (see # _callgraphUnhitTla): a fully exercised function is not what a reader # following an author through the report is looking for my ($entries, $block, $colour, $columns, $anchors) = @_; my $html = ''; my $label = _callgraphTlaLabel($colour); my $color = defined($colour) ? " class=\"tlaBg$colour\"" : ''; for (my $col = 0; $col < 2; ++$col) { my ($spans, $next, $shaded) = @{$columns->[$col]}; my $height = $spans->[$block]; next unless $height; my $span = $height > 1 ? " rowspan=\"$height\"" : ''; my $class = 'calleeAnnot' . ($shaded->[$block] ? 'Alt' : ''); my $annot = $entries->[$block]->{annot}; # ..and nothing at all for a function the annotation callback had no # answer for, which is one row of a file whose other rows it did # answer for. Not seen in practice: every line of an annotated file # has an answer, including the lines '_synthesize' makes up to reach # the last function - and callgraph coverage requires function # coverage, so every function is reached if (!defined($annot)) { # LCOV_UNREACHABLE_START $html .= " \n"; next; # LCOV_UNREACHABLE_STOP } # ..and the cell itself says what it stands for, where the reader points # at the cell rather than at its link: the date the age is counted # from, and the author's full name my ($text, $group, $what); if (0 == $col) { my $recent = $annot; my $end = $block + 1; while ($end < scalar(@$entries) && 0 == $spans->[$end]) { my $a = $entries->[$end++]->{annot}; $recent = $a if $a->{age} < $recent->{age}; } $text = $recent->{age}; $what = $recent->{date}; $group = '“' . $SummaryInfo::ageGroupHeader[$annot->{bin}] . '” bin'; } else { $text = $annot->{name}; $what = $annot->{name}; $group = escape_html($annot->{owner}); } my $tip = defined($what) ? ' title="' . escape_html($what) . '"' : ''; $text = escape_html($text); my $target = $next->[$block]; my $href = '#' . (defined($target) ? $anchors->[$target] : 'top'); my $title = 'next ' . (defined($label) ? $label : 'group') . " by $group"; $html .= " " . "$text\n"; } return $html; } sub callgraphDetailEntries($$$) { # The functions of one file which have something to say about calls: it makes # some, or something called it. Each entry is a hash carrying # name the leader symbol, which is what both pages list it under # entry its FunctionEntry, for the calls it makes # callers one element per alias which has callers, each of them # [caption or undef, FunctionAlias, class, # [external calls, deduced calls, 'x' flag], # [caller row, ..]] - # see lcovutil::classify_callers. Under '--filter # function', one element for all of them, with no caption # and no FunctionAlias - see _mergeCallerLists. Where its # aliases are shown as groups - see _showAliasGroups - no # caption either, and a 6th element: the alias # aliasGroups where its aliases are shown as groups, one element per # group, in name order: [alias, its call site groups - # see _aliasGroupSites, the names merged into it] # groupMetric ..and [found, hit] per direction of each of them, by # alias - see _aliasGroupMetrics # summaryCallers ..and the rows of its summary on the callers page, # as one element of 'callers' - see _callerSummary. Undef # if the function does not have a summary block # summarySites ..and on the callees page, as call site groups - see # _calleeSummary. Undef where 'summaryCallers' is # groups the call site groups of its body - see callSiteGroups # startline the line its definition starts on # tla its differential category # annot who last changed it and when, or undef # destAnnot the same, wherever the header bins calls into it # siteAnnot the same for the line of each of its call sites, by line # metric [found, hit] per direction, indexed by CALLGRAPH_VIEW_: # of the rows the page counts - see _countedCallSites # has whether it has anything to say in each direction, likewise # The rows of both pages are built here and not where they are written, # because the proportion each page shows has to be the proportion of # exactly the rows that page displays: computing it anywhere else would let # the two disagree. One list, used both to decide which pages this file has # and to write them, so a link cannot name a page which does not exist my ($funcdata, $differentialMap, $srcfile) = @_; # ..and the annotation columns, which exist where the callback answered for # this file. Same guard as the source view's own two columns my $annotate = $main::show_dateBins && $srcfile->isProjectFile(); # ..and the same two questions asked of the line each call site is on, which # is what the date and owner bins of the page header count and so what a # link from one of them has to find - see CallgraphPageIndex. Asked # wherever the header has either of those tables my $annotateSites = ($main::show_dateBins || defined($main::show_ownerBins)) && $srcfile->isProjectFile(); # ..and whether this file's calls were measured at all, which decides how # much of what its functions received can be attributed to anyone - see # lcovutil::classify_callers. Where they were not, a function which # nothing here calls has nothing to say on either page, so the file has # neither of them my $measured = $funcdata->hasCallgraphData($callgraphCalledSymbols); my @entries; foreach my $func (keys(%$differentialMap)) { my $funcEntry = $differentialMap->{$func}; # a function which is not in the current source has no row in the # function table either next if exists($SummaryInfo::deletedTla{$funcEntry->hit()->[1]}); # 'CALLS' is not copied into the categorized entry and 'CALLERS' is # derived data which is never copied anywhere, so both are read from # the real FunctionEntry my $realEntry = $funcdata->findName($func); next unless defined($realEntry); my @callers; # the count of each call site which calls the function, by location: # a site which calls more than one of its aliases is one unique site, # and is hit if any of those calls was taken my %callerSites; my @aliasNames = sort($realEntry->aliasNames()); my $grouped = _showAliasGroups($realEntry, $measured); foreach my $aliasName (@aliasNames) { my $aliasData = $realEntry->alias($aliasName); # How this symbol's callers relate to the calls it received. Shown # even when no site in the report calls it, as long as something # did: "entered, and by nothing here" is a fact about the callee, # and the usual reason for it - 'main', a function pointer, an # uncaptured unit - is exactly what the reader is looking for. # A symbol which was never entered and has no site has nothing to # say, and so does one whose own unit's calls were never measured: # there the caller is as likely to be its neighbour as to be # anywhere else (lcovutil::classify_callers) my ($class, undef, $external, $deduced) = lcovutil::classify_callers($aliasData, $measured); next if 0 == $aliasData->numCallers() && lcovutil::CALLERS_NONE == $class; # the column heading says what the list is, so a caption is written # only when it says something the heading cannot - which of the # aliases this list belongs to. It says 'alias' outright, because # an alias name is not in the first column of the page and need not # be anywhere in the sources either: a name the reader cannot find # reads as a stray row rather than as this function under another # symbol my $caption = (1 == scalar(@aliasNames) || $grouped) ? undef : 'Callers of alias ' . escape_html(simplify_function_name($aliasName)) . _mergedNameNote($aliasName, $aliasData->merged()); my @rows = callgraphCallerRows($aliasData, $grouped); foreach my $row (@rows) { my $site = join("\0", @{$row}[0 .. 2]); $callerSites{$site} = lcovutil::add_site_count($callerSites{$site}, $row->[4]); } push(@callers, [$caption, $aliasData, $class, [$external, $deduced, $aliasData->has_external_callers()], \@rows, $grouped ? $aliasName : undef ]); } # ..as one list, where the function table lists the function under one # name - with no caption even when only one alias had callers. # Used only when alias groups are not shown - if they are, then this # table has a row for each of group (just like the function detail # table does). @callers = (_mergeCallerLists(@callers)) if $main::merge_function_aliases && @callers && !$grouped; # the caller side is held per alias and the call sites per entry, so a # function with neither has no row on either page - unless it called out # of the boundary of the report, which is a call with no site of its own # (see FunctionEntry::has_external_callees) my $external = $realEntry->has_external_callees(); my @groups = callSiteGroups($realEntry, $realEntry->file(), $grouped); next unless @callers || @groups || $external; my $startline = $funcEntry->line() - $func_offset; $startline = 1 if $startline < 1; my ($aliasGroups, $groupMetric, $summaryCallers, $summarySites); if ($grouped) { # ..unless there is no summary block - see 'show_callgraph_summary' # in lcovrc(5) if ($main::show_callgraph_summary) { $summaryCallers = _callerSummary(\@callers) if @callers; $summarySites = _calleeSummary(\@groups); } $aliasGroups = [ map({ [$_, _aliasGroupSites(\@groups, $_), $realEntry->alias($_)->merged() ] } @aliasNames) ]; $groupMetric = _aliasGroupMetrics($realEntry, \@callers, \@groups); } my @siteCounts; my %siteAnnot; # ..and the same two questions asked of the line the function starts on, # which is what the header of the callers page bins a call into it by my $destAnnot = $annotateSites ? _callgraphAnnotation($srcfile, $funcEntry->line()) : undef; foreach my $group (@groups) { push(@siteCounts, map({ $_->[1] } @{$group->[2]})); # a site written in another file is on a line of that file, whose # annotation this file's header does not count my $line = $group->[0]; $siteAnnot{$line} = _callgraphAnnotation($srcfile, $line) if $annotateSites && !defined($group->[3]) && !exists($siteAnnot{$line}); } # the rows which represent a call - which is every row of a page # except the one which says that a call crossed the callgraph # boundary of the report. # What we know about external calls is that there is at least one # of them - but we know nothing about proportions. # Each call site shown once, where the rows of alias groups list a # site in more than one of them - and where there is no summary block, # the rows of every group, which is what the page shows my $metric = [_callgraphTotals(values(%callerSites)), _callgraphTotals(@siteCounts) ]; if ($grouped && !$main::show_callgraph_summary) { foreach my $view (0 .. CALLGRAPH_VIEW_COUNT - 1) { my ($found, $hit) = (0, 0); foreach my $m (values(%$groupMetric)) { next unless defined($m->[$view]); $found += $m->[$view]->[0]; $hit += $m->[$view]->[1]; } $metric->[$view] = [$found, $hit]; } } push(@entries, { name => $func, entry => $realEntry, callers => \@callers, groups => \@groups, aliasGroups => $aliasGroups, groupMetric => $groupMetric, summaryCallers => $summaryCallers, summarySites => $summarySites, startline => $startline, tla => $funcEntry->hit()->[1], annot => $annotate ? $destAnnot : undef, destAnnot => $destAnnot, siteAnnot => \%siteAnnot, metric => $metric, has => [scalar(@callers) ? 1 : 0, (@groups || $external) ? 1 : 0], }); } return @entries; } sub _callgraphAnnotation($$) { # who last changed the line a function starts on, and how long ago, as the two # cells of that function want it: the age in days the source view shows, # the author's full name, and the date bin and login the source view groups # by. undef where the callback had no answer for that line, which is the # answer for a line outside the file it was asked about my ($srcfile, $line) = @_; return undef if $line < 1 || $line > $srcfile->num_lines(); my $srcline = $srcfile->line($line); return undef unless defined($srcline); my ($owner, $age) = ($srcline->owner(), $srcline->age()); return undef unless defined($owner) && defined($age); my $name = $srcline->full_name(); # ..and the date itself, for the tooltip of the age cell: the day, which is # what the age counts in my $date = $srcline->date(); $date =~ s/T.*$// if defined($date); return { age => $age, date => $date, name => defined($name) ? $name : $owner, owner => $owner, bin => SummaryInfo::findAgeBin($age), }; } sub callgraphViewEntries($$$) { # The functions one page shows, in that page's order. A function is on the # page of a direction only when it has something to say in that direction, # which is not the same question for the two of them: a leaf function has # callers and no callees, 'main' the other way about, and a row which says # nothing is a row the reader has to read before finding that out. # Every order is total, so that two runs of the same report give the same # page: the name breaks any tie before it, and the declaration line breaks # a tie in the name - which only two identical names can produce. A # function's rows are one block in each of them, so that the Age and Owner # cells merge the functions which end up next to each other my ($entries, $view, $sort_type) = @_; my @selected = grep({ $_->{has}->[$view] } @$entries); # the proportion the 'Rate' column of the page shows, least exercised # first: not the count of unexercised rows behind it - a function with two # of ten calls left is not the one to start with when another has its only # call left my $rate = sub { return _rateOrder($_[0]->{metric}->[$view], $_[1]->{metric}->[$view]); }; my $key; if ($sort_type == $SORT_XREF) { # ..the proportion the column which leads to the other direction shows, # which is empty where the function is not on that page $key = sub { my ($x, $y) = map({ $_->{has}->[1 - $view] ? $_->{metric}->[1 - $view] : undef } @_); return _rateOrder($x, $y); }; } elsif ($sort_type == $SORT_OWNER || $sort_type == $SORT_AGE) { # ..the author the Owner column shows, alphabetically, or the age the # Age column shows, the most recently changed first - and of the # functions which share it, the least exercised first. A function the # annotation callback had no answer for has an empty cell, and is last my $owner = $sort_type == $SORT_OWNER; $key = sub { my ($x, $y) = map({ $_->{annot} } @_); return (defined($y) <=> defined($x)) if !defined($x) || !defined($y); return ($owner ? ($x->{name} cmp $y->{name}) : ($x->{age} <=> $y->{age})) || &$rate(@_); }; } elsif ($sort_type == $SORT_MISSING_CALLGRAPH) { $key = $rate; } else { die("unexpected sort $sort_type") unless $sort_type == $SORT_FILE; $key = sub { return 0; }; } return sort({ &$key($a, $b) or $a->{name} cmp $b->{name} or $a->{startline} <=> $b->{startline} } @selected); } sub _callSiteGroupCells($$$$$$) { # the rows of '_callSiteCells' for the call site groups '$groups' of # function '$entry': the location cell of each group spans its rows. A # group or row whose anchor is in '$seen' was written already, and is # written again without it my ($entry, $groups, $source, $base_dir, $sort_type, $seen) = @_; my @cells; foreach my $group (@$groups) { my ($line, $column, $rows, $siteFile) = @$group; my $anchor = _callSiteAnchor($line, $column, undef, $siteFile); my $first = !$seen->{$anchor}++; # the location cell is 'taken' if any destination of it was my $groupCount = _callgraphGroupCount($rows); # ..and a call written in another file is named with that file, and links # to its page rather than to this one - see 'FNC:' in geninfo(1) my $location = "$line:$column"; my $sitePage = $source; my $lineFile = $entry->{entry}->file(); if (defined($siteFile)) { $lineFile = $siteFile; $location = escape_html(File::Basename::basename($siteFile)) . ":$location"; $sitePage = filePageBase($siteFile, $base_dir); $sitePage = "$sitePage.gcov.$html_ext" if defined($sitePage); } $location = "{siteTitle} . "\">$location" if defined($sitePage) && _pageShows($lineFile, 'gcov', $line); my $rowspan = scalar(@$rows) > 1 ? ' rowspan="' . scalar(@$rows) . '"' : ''; my $locationCell = ' $location\n"; my $position = 0; foreach my $row (@$rows) { my ($callees, $count, $tla, $where, undef, $at) = @$row; ++$position; # ..which, in the list of one alias group, is the position the row # has among all of the function's rows $at //= $position; my $id = _callSiteAnchor($line, $column, $at, $siteFile); $id = undef if $seen->{$id}++; # every symbol which leads to this one place, named in the one cell my $name = join(', ', map({ escape_html(simplify_function_name($_)) . (defined($where) ? _mergedNameNote($_, $calleeMergedNames{$where->[0] . "\0$_"}) : '') } @$callees)); # ..and leading to the callers of the callee, which is the list this # reader is one step away from wanting: "who else calls what I # call" is the question this cell is the start of. Not a link when # the report has no page for the callee, which is a call which left # the boundary, or when the page does not list it (because it was # removed by a 'select' callback) if (defined($where)) { my $page = _callgraphForeignPage($where->[0], $base_dir, CALLGRAPH_VIEW_CALLERS, $sort_type); my $shown = $where->[0] . "\0$callees->[0]"; my $anchor = exists($calleeGroupShown{$shown}) ? _aliasGroupAnchor($callees->[0]) : _calleeFuncAnchor($where->[2]); $name = "$name" if defined($page) && _pageShows($where->[0], 'callers', $anchor); } push(@cells, { id => $id, tla => $tla, site => defined($siteFile) ? undef : $entry->{siteAnnot}->{$line}, head => $locationCell . ' ' . "$name\n", count => $count, }); $locationCell = ''; # spanned by the first row of the group } } return @cells; } sub _callSiteCells($$$$$) { # 'Calls made by this function' - the rows of the callees page for one # function. These are the calls the file's own FNCF/FNCH are computed # from: a location with several destinations needs: a virtual call, # a call through a function pointer, a call in a template body - is one # group of rows sharing a location cell, each destination with a count # of its own. # Each row is a hash carrying # id the anchor the source view and a caller's name cell lead to, # which is on the row because the row is what they mean # tla its category, for the column and for the category links # site who last changed the line the call is on and when, or undef - # see CallgraphPageIndex # head the cells before the category column, and 'tail' those after # - or 'note', a row which spans the columns instead of filling them. # group on the first row of an alias group, [alias, the names merged # into it, its number of rows] - see _aliasGroupLeadCells # summary on a row of the summary block of a function shown as alias # groups # Where the aliases of the function are shown as groups - see # _showAliasGroups - the function's rows are a summary block, a row per # unique call site (see _calleeSummary), and then each group, the calls # that group made: a site more than one of them made is in the list of # each. The summary has the anchors, being the first. Unless the # groups are suppressed - see 'suppress_alias_groups' in lcovrc(5) - # or there is no summary block, and the groups are the function's rows, # least exercised first - see 'show_callgraph_summary' in lcovrc(5) my ($entry, $source, $base_dir, $span, $sort_type) = @_; my @cells; my %seen; my $summary = $entry->{summarySites}; my @aliasGroups; if (defined($summary)) { @cells = _callSiteGroupCells($entry, $summary, $source, $base_dir, $sort_type, \%seen); push(@cells, _externalCalleeRow($entry)); $_->{summary} = 1 foreach (@cells); @aliasGroups = @{$entry->{aliasGroups}} unless $main::suppress_alias_groups; } elsif (defined($entry->{aliasGroups})) { @aliasGroups = _aliasGroupsInOrder($entry, CALLGRAPH_VIEW_CALLEES, 0, @{$entry->{aliasGroups}}); } else { @aliasGroups = ([undef, $entry->{groups}]); } # ..and the calls out of the report are the function's, not a group's: # where there is no summary block, they are the last row of the first # group which is written - or of the first group, where none made a call my @external = defined($summary) ? () : _externalCalleeRow($entry); my ($home) = grep({ @{$_->[1]} } @aliasGroups); $home //= $aliasGroups[0]; foreach my $aliasGroup (@aliasGroups) { my ($alias, $groups, $merged) = @$aliasGroup; my @rows = _callSiteGroupCells($entry, $groups, $source, $base_dir, $sort_type, \%seen); push(@rows, @external) if $aliasGroup == $home; # the first row of a group carries the cells of the group $rows[0]->{group} = [$alias, $merged, scalar(@rows)] if defined($alias) && @rows; push(@cells, @rows); } return @cells; } sub _externalCalleeRow($) { # The last row of the callees of function '$entry', or of its summary # block: the calls which went out of the boundary of the report. The # callee is deliberately not part of it, so there is nothing to name and # nothing to link to, and the site the call was recorded at went with it. # Hence a row with neither a location, a category nor a count: what is # left of such a call is that the edge is there. None where there is no # such call my $entry = shift; return () unless $entry->{entry}->has_external_callees(); return ({ id => undef, tla => undef, head => ' ' . "\n" . ' $EXTERNAL_CALLEE_ROW\n", tail => ' ' . "\n", }); } sub _callgraphCellLine($) { # one cell of a call-site row on its own line of the page, or nothing at all # when the report has no category column - see _callgraphTlaCell my $cell = shift; return '' if $cell eq ''; return ' ' . $cell . "\n"; } sub _callgraphForeignPage($$$$) { # one call site page of another file, as a link from this one: which page is # named by the file the call leads to, and which of its orders by the order # the reader is already in - following a call across files should not # silently change the order of the next table. undef when the report has no # page for that file, which is a call which left the callgraph boundary # The order is kept where every call site page is written in it, and is the # name order otherwise: which of the others that file's page has depends # on columns of it this page cannot see my ($filename, $base_dir, $view, $sort_type) = @_; my $base = filePageBase($filename, $base_dir); return undef unless defined($base); my $kind = $callgraphView[$view]->{name}; $kind .= _callgraphPageSuffix($SORT_MISSING_CALLGRAPH) if $sort_type == $SORT_MISSING_CALLGRAPH; return "$base.$kind.$html_ext"; } sub _callgraphCallerCells($$$$) { # 'Callers of this function' - the rows of the callers page for one function, # which are cross-file by nature: the location cell leads to the caller's # source line and the caller's name to that caller's own site group, so that # the two cells answer the two different questions a reader has here ("show # me the code" and "show me everything called there"). # One list for every alias of the function, each alias' rows introduced by a # caption row where there is more than one alias - or each one a group of # its own, where the groups are shown - below a summary block of them all, # see _callerSummary, and unless they are suppressed (see # 'suppress_alias_groups' in lcovrc(5)). The rows are the shape # '_callSiteCells' returns # Where there is no summary block, the groups are the function's rows, # least exercised first - see 'show_callgraph_summary' in lcovrc(5) my ($entry, $base_dir, $span, $sort_type) = @_; my @cells; my $index = 0; my $summary = $entry->{summaryCallers}; my @callers = @{$entry->{callers}}; if (defined($summary)) { @cells = _callerListCells($entry, $summary, $base_dir, $sort_type, \$index); $_->{summary} = 1 foreach (@cells); @callers = () if $main::suppress_alias_groups; } elsif (defined($entry->{aliasGroups})) { @callers = _aliasGroupsInOrder($entry, CALLGRAPH_VIEW_CALLERS, 5, @callers); } foreach my $caller (@callers) { push(@cells, { id => undef, tla => undef, note => _callgraphCaptionCell($caller->[0], $span) }) if defined($caller->[0]); my $first = scalar(@cells); push(@cells, _callerListCells($entry, $caller, $base_dir, $sort_type, \$index)); # ..and the first row of a group carries the cells of the group my $alias = $caller->[5]; $cells[$first]->{group} = [$alias, $caller->[1]->merged(), scalar(@cells) - $first, $caller] if defined($alias) && $first < scalar(@cells); } return @cells; } sub _callerListCells($$$$$) { # the rows of '_callgraphCallerCells' for the caller list '$caller' of # function '$entry' - see callgraphDetailEntries. '$index' counts the # rows of the function, which is used to number their anchors my ($entry, $caller, $base_dir, $sort_type, $index) = @_; my ($calls, $rows) = @{$caller}[3, 4]; my @cells; foreach my $row (@$rows) { my ($file, $line, $column, $callerName, $count, $tla, $position, $owner, $merged) = @$row; my $page = filePageBase($file, $base_dir); my $location = escape_html(File::Basename::basename($file)) . ":$line:$column"; $location = "{siteTitle} . "\">$location" if defined($page) && _pageShows($file, 'gcov', $line); # ..and the caller's name leads to the calls that caller makes, at the # site corresponding to this row: that is the list which holds # this call, and it is on the page of the file which defines the # caller (not this page), and not the file the call is written in my $callees = _callgraphForeignPage($owner, $base_dir, CALLGRAPH_VIEW_CALLEES, $sort_type); my $name = escape_html(simplify_function_name($callerName)); if (defined($callees) && defined($position)) { my $anchor = _callSiteAnchor($line, $column, $position, $owner eq $file ? undef : $file); $name = "' . "$name" if _pageShows($owner, 'callees', $anchor); } $name .= _mergedNameNote($callerName, $merged); push(@cells, { id => _callerRowAnchor($entry->{name}, $$index++), tla => $tla, head => " $name\n" . ' ' . "$location\n", count => $count, }); } # ..and, last, the calls which came from somewhere this report doesn't # know about (external callers) # Two different facts can indicate that external callers exist any; # we name the row for which of them is true: # the flag is evidence that a call site outside the callgraph boundary # calls this function - the caller is outside it, exactly as the callee # of an '' row - and the arithmetic deduces that an # external caller exists because the symbol was executed more often # than its call sites above account for (see # lcovutil::deduced_external_calls). # Either, both or neither can be true. Where a '-' site above leaves # the arithmetic undecided and there is no flag, neither is known, and # the note beside the name says so instead of a row. # The origin is not known either way, so the row never has a link. Its # count cell says which of the two facts hold - see # _callgraphExternalCountCell - and so does its label: the same thing # twice, but the label is what a reader going down the table reads my ($external, $residual, $flagged) = @$calls; my $deduced = defined($residual) && $residual > 0; if ($flagged || $deduced) { my ($label, $title) = !$deduced ? ($EXTERNAL_CALLER_ROW, $EXTERNAL_CALLER_TITLE) : !$flagged ? ($DEDUCED_CALLER_ROW, $DEDUCED_CALLER_TITLE) : ($DEDUCED_AND_EXTERNAL_CALLER_ROW, "$EXTERNAL_CALLER_TITLE. In addition, $DEDUCED_CALLER_TITLE"); push(@cells, { id => undef, tla => undef, head => ' $label\n" . $CALLGRAPH_EMPTY_CELL, external => [$flagged, $external, $deduced ? $residual : undef], }); } return @cells; } sub _functionSourceHref($$) { # Where a proportion of the function table leads: the first coverpoint of # that kind in the source view, a couple of lines above it so that it is not # the top line of the window - the same courtesy the name column does. # There is nowhere to go where the kind has no coverpoint with a line of its # own my ($source, $line) = @_; return undef unless defined($line); my $start = $line - $func_offset; $start = 1 if $start < 1; return "$source#L" . _shownSourceLine($start, $line); } sub _functionProportionCell($$$$$) { # One of the proportions of the function table: how much of one kind of # coverpoint of this function the tests exercised, coloured by it and # carrying the counts it came from. '$href' is what the cell leads to and # is undef where there is nowhere to go my ($metric, $medLimit, $hiLimit, $href, $title) = @_; my ($found, $hit) = @$metric; # colorize based on hit proportion my $style = $rate_name[classify_rate($found, $hit, $medLimit, $hiLimit)]; my $text = rate($hit, $found, ' %') . " ($hit / $found)"; $text = '' . $text . '' if defined($href); return "$text"; } # # write_function_table(filehandle, funcData, source_file, sumcount, funcdata, # testfncdata, sumbrcount, testbrdata, mcdc_summary, testcase_mcdc, # base_name, base_dir, sort_type) # # Write an HTML table listing all functions in a source file, including # also function call counts and line coverages inside of each function. # # Die on error. # sub write_function_table(*$$$$$$$$$$$$) { local *HTML_HANDLE = shift; my ($funcData, $source, $sumcount, $funcdata, $testfncdata, $sumbrcount, $testbrdata, $mcdc_summary, $testcase_mcdc, $name, $base, $type) = @_; my ($differentialMap, $funcLineCovMap, $funcBranchCovMap, $funcMcdcCovMap, $funcCallgraphCovMap, $callgraphPages, $callgraphXref) = @$funcData; # The two directions of the callgraph, as a column each at the right of the # table: they are proportions of this function like the columns to their # left, so they belong with them and not between the reader and the name. # A direction the file has no page for has no column here - see # callgraphDetailEntries - and a function with nothing to say in a # direction the file does have gets an empty cell my @callgraphPageName = map( { (defined($callgraphPages) && $callgraphPages->[$_]) ? callgraphPageName($name, $_) : undef } 0 .. CALLGRAPH_VIEW_COUNT - 1); my $callerPage = $callgraphPageName[CALLGRAPH_VIEW_CALLERS]; my $calleePage = $callgraphPageName[CALLGRAPH_VIEW_CALLEES]; # ..and whether this file's calls were measured, which determines whether # the aliases of a function are shown as its groups - see _showAliasGroups my $measured = $lcovutil::callgraph_coverage && $funcdata->hasCallgraphData($callgraphCalledSymbols); # Get HTML code for headings my $func_code = funcview_get_label($name, $base, undef, $type); my $count_code = funcview_get_label($name, $base, 'hit', $type); my $line_code = funcview_get_label($name, $base, 'missed_line', $type); my $branch_code = funcview_get_label($name, $base, 'missed_branch', $type); my $mcdc_code = funcview_get_label($name, $base, 'missed_mcdc', $type); my $callgraph_code = funcview_get_label($name, $base, 'missed_callgraph', $type); my $showTlas = $main::show_tla && 0 != scalar(keys %$differentialMap); my $tlaRow = ""; my $lineProportionRow = ''; my $branchProportionRow = ''; my $mcdcProportionRow = ''; my $callerProportionRow = ''; my $callgraphProportionRow = ''; if ($showTlas) { # no width of its own: this column holds a TLA or one short word, and a # tenth of the table was an order of magnitude more room than that - the # space it was given came out of the columns which have something long # to show, the function name first among them my $label = $main::use_legacyLabels ? 'Hit?' : 'TLA'; $tlaRow = "$label"; } if ($main::show_functionProportions) { $lineProportionRow = "$line_code" if (%$funcLineCovMap); $branchProportionRow = "$branch_code" if $lcovutil::br_coverage && %$funcBranchCovMap; $mcdcProportionRow = "$mcdc_code" if $lcovutil::mcdc_coverage && %$funcMcdcCovMap; # the calls which reach this function.. $callerProportionRow = '' . funcview_get_label($name, $base, 'callers', $type) . '' if $lcovutil::callgraph_coverage && defined($callerPage); # ..and the sites this function contains, which is the 'per file' view of # the metric - not the sites which target it, which is the other table # on the call site page $callgraphProportionRow = "$callgraph_code" if $lcovutil::callgraph_coverage && %$funcCallgraphCovMap; } # ..which are written in the order of the table's columns - see # functionTableColumns my @columnOrder = functionTableColumns($type); my $proportionCells = sub { my %cell = @_; return join("\n", map({ ' ' . $cell{$_} } @columnOrder)); }; die("no functions in file") unless (%$differentialMap); my $cells = $proportionCells->(line => $lineProportionRow, branch => $branchProportionRow, mcdc => $mcdcProportionRow, callers => $callerProportionRow, callgraph => $callgraphProportionRow); write_html(*HTML_HANDLE, < $tlaRow $cells END_OF_HTML foreach my $func ( funcview_get_sorted($funcData, $type, $main::merge_function_aliases)) { my $funcEntry = $differentialMap->{$func}; my ($count, $tla) = @{$funcEntry->hit()}; # don't display deleted functions next if exists($SummaryInfo::deletedTla{$tla}); my $startline = $funcEntry->line() - $func_offset; my $name = simplify_function_name($func); my $countstyle; # Escape special characters $name = escape_html($name); # ..and where the aliases are shown as groups, the group of the name the # function is listed under is a row of its own below, with its mark my $realEntry = $measured ? $funcdata->findName($func) : undef; my $grouped = defined($realEntry) && _showAliasGroups($realEntry, $measured); my $mark = $grouped ? '' : _mergedNameNote($func, _aliasMerged($funcEntry, $func)); if ($startline < 1) { $startline = 1; } $startline = _shownSourceLine($startline, $funcEntry->line()); if ($count == 0) { $countstyle = "coverFnLo"; } else { $countstyle = "coverFnHi"; } my $tlaRow = ""; my $lineProportionRow = ''; my $branchProportionRow = ''; my $mcdcProportionRow = ''; my $callerProportionRow = ''; my $callgraphProportionRow = ''; if ($showTlas) { my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} : $tla; $tlaRow = ""; } if ($main::show_functionProportions) { # Each proportion leads to the coverpoints it counts: a reader who # sees that something in this function was never exercised wants the # code, and the top of the function is not it where the function is # longer than the screen if (exists($funcLineCovMap->{$func})) { $lineProportionRow = _functionProportionCell( $funcLineCovMap->{$func}, $ln_med_limit, $ln_hi_limit, _functionSourceHref( $source, $funcLineCovMap->{$func}->[2] ), 'Click to go to the first line of this function' ); } else { $lineProportionRow = "" if %$funcLineCovMap; } if ($lcovutil::br_coverage && %$funcBranchCovMap) { $branchProportionRow = exists($funcBranchCovMap->{$func}) ? _functionProportionCell( $funcBranchCovMap->{$func}, $br_med_limit, $br_hi_limit, _functionSourceHref( $source, $funcBranchCovMap->{$func}->[2] ), 'Click to go to the first branch of this function' ) : ""; } if ($lcovutil::mcdc_coverage && %$funcMcdcCovMap) { $mcdcProportionRow = exists($funcMcdcCovMap->{$func}) ? _functionProportionCell( $funcMcdcCovMap->{$func}, $mcdc_med_limit, $mcdc_hi_limit, _functionSourceHref($source, $funcMcdcCovMap->{$func}->[2]), 'Click to go to the first MC/DC expression of this function' ) : ""; } # ..and the two directions of the callgraph, which lead to this # function's own entry on the page that direction is listed on # rather than into the source: the calls which reach a function are # not in it, and the calls it makes are a table of their own if ($lcovutil::callgraph_coverage && defined($callerPage)) { my $metric = defined($callgraphXref) && exists($callgraphXref->{$func}) ? $callgraphXref->{$func}->[CALLGRAPH_VIEW_CALLERS] : undef; # An empty cell where there is nothing to say: a function whose # callers are all outside the report is not on that page at all, # and one none of whose rows there carries a known count has no # proportion - a column of proportions is no place to write that # the denominator is zero $callerProportionRow = (defined($metric) && 0 != $metric->[0]) ? _functionProportionCell( $metric, $callgraph_med_limit, $callgraph_hi_limit, $callerPage . '#' . _calleeFuncAnchor($func), 'Click to go to the callers of this function' ) : ""; } if ($lcovutil::callgraph_coverage && %$funcCallgraphCovMap) { # every function counted here has call sites, so it has an entry # on that page to lead to $callgraphProportionRow = exists($funcCallgraphCovMap->{$func}) ? _functionProportionCell( $funcCallgraphCovMap->{$func}, $callgraph_med_limit, $callgraph_hi_limit, defined($calleePage) ? $calleePage . '#' . _calleeFuncAnchor($func) : undef, 'Click to go to the call sites of this function' ) : ""; } } my $cells = $proportionCells->(line => $lineProportionRow, branch => $branchProportionRow, mcdc => $mcdcProportionRow, callers => $callerProportionRow, callgraph => $callgraphProportionRow); write_html(*HTML_HANDLE, < $tlaRow $cells END_OF_HTML if ((!defined($main::suppress_function_aliases) || 0 == $main::suppress_function_aliases) && $funcEntry->numAliases() > 1 ) { my @aliasList = $funcEntry->aliasNames(); if ($main::show_functionProportions) { $lineProportionRow = "" if %$funcLineCovMap; $branchProportionRow = $lineProportionRow if $lcovutil::br_coverage && %$funcBranchCovMap; $mcdcProportionRow = $lineProportionRow if $lcovutil::mcdc_coverage && %$funcMcdcCovMap; # the callgraph pages list a function under its leader symbol, # which is the row above: an alias has no entry of its own to # lead to $callerProportionRow = "" if $lcovutil::callgraph_coverage && defined($callerPage); $callgraphProportionRow = "" if $lcovutil::callgraph_coverage && %$funcCallgraphCovMap; } if (0 != $type) { @aliasList = sort({ $funcEntry->alias($a)->count() <=> $funcEntry->alias($b)->count() or $a cmp $b } @aliasList); } else { @aliasList = sort(@aliasList); } # the rows of a group are set off by subdued colours my $rowClass = $grouped ? ' class="fnGroupRow"' : ''; my $groupMetric = ($grouped && exists($callgraphXref->{$func})) ? $callgraphXref->{$func}->[CALLGRAPH_VIEW_COUNT] : undef; foreach my $alias (@aliasList) { my $aliasData = $funcEntry->alias($alias); # ..and where the aliases are groups, the calls of each group, # leading to that group's rows on the page of the direction - # unless the pages do not show them (see # 'suppress_alias_groups' in lcovrc(5)) if ($grouped && $main::show_functionProportions) { my $metric = $groupMetric->{$alias}; my $anchor = _aliasGroupAnchor($alias); foreach my $cell ( [CALLGRAPH_VIEW_CALLERS, \$callerProportionRow, $callerPage, 'Click to go to the callers of this alias group', defined($callerPage) ], [CALLGRAPH_VIEW_CALLEES, \$callgraphProportionRow, $calleePage, 'Click to go to the call sites of this alias group', 0 != scalar(%$funcCallgraphCovMap) ] ) { my ($view, $row, $page, $title, $shown) = @$cell; next unless $shown; my $m = defined($metric) ? $metric->[$view] : undef; $$row = (defined($m) && 0 != $m->[0]) ? _functionProportionCell( $m, $callgraph_med_limit, $callgraph_hi_limit, (defined($page) && !$main::suppress_alias_groups) ? "$page#$anchor" : undef, $title) : ""; } } my ($hit, $tla) = ($aliasData->count(), $aliasData->tla()); # don't display deleted functions next if exists($SummaryInfo::deletedTla{$tla}); my $style = "coverFnAlias" . ($hit == 0 ? "Lo" : "Hi"); $tlaRow = ""; if ($showTlas) { my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} : $tla; $tlaRow = ""; } # Escape special characters my $aliasMark = _mergedNameNote($alias, $aliasData->merged()); $alias = escape_html(simplify_function_name($alias)); my $cells = $proportionCells->(line => $lineProportionRow, branch => $branchProportionRow, mcdc => $mcdcProportionRow, callers => $callerProportionRow, callgraph => $callgraphProportionRow); write_html(*HTML_HANDLE, < $tlaRow $cells END_OF_HTML } } } write_html(*HTML_HANDLE, <
END_OF_HTML } # # remove_unused_descriptions() # # Removes all test descriptions from the global hash %test_description which # are not present in %current_data. # sub remove_unused_descriptions() { my $filename; # The current filename my %test_list; # Hash containing found test names my $test_data; # Reference to hash test_name -> count_data my $before; # Initial number of descriptions my $after; # Remaining number of descriptions $before = scalar(keys(%test_description)); foreach $filename ($current_data->files()) { ($test_data) = $current_data->data($filename)->get_info(); foreach ($test_data->keylist()) { $test_list{$_} = ""; } } # Remove descriptions for tests which are not in our list foreach (keys(%test_description)) { if (!exists($test_list{$_})) { delete($test_description{$_}); } } $after = scalar(keys(%test_description)); if ($after < $before) { info("Removed " . ($before - $after) . " unused descriptions, $after remaining.\n"); } } # # apply_prefix(filename, PREFIXES) # # If FILENAME begins with PREFIX from PREFIXES, remove PREFIX from FILENAME # and return resulting string, otherwise return FILENAME. # sub apply_prefix($@) { my $filename = shift; foreach my $prefix (@_) { if ($prefix eq $filename) { return 'root'; } if ($prefix ne "" && $filename =~ /^\Q$prefix$lcovutil::dirseparator\E(.*)$/) { return substr($filename, length($prefix) + 1); } } return $filename; } # # get_html_prolog(FILENAME) # # If FILENAME is defined, return contents of file. Otherwise return default # HTML prolog. Die on error. # sub get_html_prolog($) { my $filename = $_[0]; my $result = ""; if (defined($filename)) { local *HANDLE; open(HANDLE, "<", $filename) or die("cannot open html prolog $filename: $!\n"); while () { $result .= $_; } close(HANDLE) or die("unable to close HTML handle: $!\n"); } else { $result = < \@pagetitle\@ END_OF_HTML } return $result; } # # get_html_epilog(FILENAME) # # If FILENAME is defined, return contents of file. Otherwise return default # HTML epilog. Die on error. # sub get_html_epilog($) { my $filename = $_[0]; my $result = ""; if (defined($filename)) { local *HANDLE; open(HANDLE, "<", $filename) or die("cannot open html epilog $filename: $!\n"); while () { $result .= $_; } close(HANDLE) or die("unable to close HTML handle: $!\n"); } else { $result = < END_OF_HTML } return $result; } # # parse_dir_prefix(@dir_prefix) # # Parse user input about the prefix list # sub parse_dir_prefix(@) { my (@opt_dir_prefix) = @_; return if (!@opt_dir_prefix); # A prefix is a leading part of an output path, and it is matched against the # name this platform's path modules decompose - so it is spelled the same # way. That lets '--prefix C:\proj' work on a POSIX machine reading a # Windows '.info' file, which is the spelling that file suggests. # See 'lcovutil::native_path' foreach my $item (@opt_dir_prefix) { if ($item =~ $lcovutil::split_pattern) { # Split and add comma-separated parameters push(@dir_prefix, map { lcovutil::native_path($_) } split($lcovutil::split_pattern, $item)); } else { # Add single parameter push(@dir_prefix, lcovutil::native_path($item)); } } } # # simplify_function_name($name) # # apply @function_simplify_patterns to $name and return # goal is to shorten really long demangled names/template expansions # sub simplify_function_name($) { my $name = shift; if ($simplifyFunctionCallback) { eval { $name = $simplifyFunctionCallback->simplify($name); }; if ($@) { my $context = MessageContext::context(); lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, "simplify($name) failed$context: $@"); } } return $name; }

$func_code$count_code
$label$name$mark$count
$label$alias$aliasMark$hit