#!/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. # Ocober 2020 / Henry Cox - Mediatek, inc # Add "--hierarchical" display option. # use strict; use warnings; use File::Basename; use File::Copy; 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 qw(dclone); use FindBin; use Time::HiRes; # for profiling use Storable; use POSIX; use Data::Dumper; use lib "$FindBin::RealBin/../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"; lcovutil::set_tool_name(basename($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; # 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; # Sort types our $SORT_FILE = 0; our $SORT_LINE = 1; our $SORT_FUNC = 2; our $SORT_BRANCH = 3; our $SORT_MCDC = 4; # function detail sort types our $SORT_MISSING_LINE = 5; # by number of not-hit lines in function our $SORT_MISSING_BRANCH = 6; # by number of not-hit branches in function our $SORT_MISSING_MCDC = 7; # by number of not-hit MC/DC expressions in function # 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(@); # 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 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($$$$$@); package SummaryInfo; our @selectCallbackScript; our $selectCallback; our @cleanDirectoryList; our @tlaPriorityOrder = ("UNC", "LBC", "UIC", "UBC", "GBC", "GIC", "GNC", "CBC", "EUB", "ECB", "DUB", "DCB",); our %tlaLocation = ("UNC" => 1, "LBC" => 3, "UIC" => 3, "UBC" => 3, "GBC" => 3, "GIC" => 3, "GNC" => 1, "CBC" => 3, "EUB" => 3, "ECB" => 3, "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", "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 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 # coverage data list for type DATA => 0, AGE => 1, OWNERS => 2, # not used by Function coverage - no owner }; sub type2str { my $t = shift; return 'line' if ($t == LINE_DATA); return 'branch' if ($t == BRANCH_DATA); return 'MC/DC' if ($t == MCDC_DATA); die("unexpected type '$t'") unless ($t == FUNCTION_DATA); return 'function'; } sub _initCounts { my %hash; foreach my $key ('found', 'hit', 'GNC', 'UNC', 'CBC', 'GBC', 'LBC', 'UBC', 'ECB', 'EUB', 'GIC', 'UIC', 'DCB', 'DUB' ) { $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'); %tlaToTitle = ('UNC' => 'Not Hit', 'GNC' => 'Hit',); } sub setAgeGroups { #my $numGroups = scalar(@_) + 1; @cutpoints = sort({ $a <=> $b } @_); if (@ageGroupHeader) { # labels were specified by user @ageGroupHeader = split($lcovutil::split_char, 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'); } for my $type (LINE_DATA, BRANCH_DATA, MCDC_DATA, FUNCTION_DATA) { $self->[$type]->[DATA] = _initCounts(); # no age data unless annotations are enabled next unless @SourceFile::annotateScript; my $ageList = $self->[$type]->[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); } } bless $self, $class; return $self; } # deserialization: copy the coverage portion of the undumped data sub copyGuts { my ($self, $that) = @_; my @copy = (RELATIVE_DIR, FULL_DIR, LINE_DATA, BRANCH_DATA, MCDC_DATA, FUNCTION_DATA); 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_BRANC, $SORT_MCDC) 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; } 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 eq MCDC_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) 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} if ('EUB' ne $k && 'ECB' ne $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 findOwnerList { # return [ [owner, lineCovData, branchCovData, functionCov]] for each owner # where lineCovData = [missedCount, totalCount] # branchCovData = [missed, total] or undef if not enabled # functionCov = [missed, total] or undef if not enabled # - sorted in descending order number of missed lines my ($self, $callback_type, $truncate_me, $all) = @_; my @owners; foreach my $owner (keys(%{$self->[LINE_DATA]->[OWNERS]})) { my $lineMissed = $self->owner_tlaCount($owner, 'missed', LINE_DATA); my $branchMissed = $lcovutil::br_coverage ? $self->owner_tlaCount($owner, 'missed', BRANCH_DATA) : 0; my $funcMissed = $lcovutil::func_coverage ? $self->owner_tlaCount($owner, 'missed', FUNCTION_DATA) : 0; my $mcdcMissed = $lcovutil::mcdc_coverage ? $self->owner_tlaCount($owner, 'missed', MCDC_DATA) : 0; # filter owners who have unexercised code, if requested if ($all || (0 != $lineMissed || 0 != $branchMissed || 0 != $funcMissed)) { my $lineCb = OwnerDetailCallback->new($self, $owner, LINE_DATA); my $branchCb = OwnerDetailCallback->new($self, $owner, BRANCH_DATA); my $mcdcCb = OwnerDetailCallback->new($self, $owner, MCDC_DATA); my $functionCb = OwnerDetailCallback->new($self, $owner, FUNCTION_DATA); my $lineTotal = $self->owner_tlaCount($owner, 'found', LINE_DATA); my $branchTotal = $lcovutil::br_coverage ? $self->owner_tlaCount($owner, 'found', BRANCH_DATA) : 0; my $mcdcTotal = $lcovutil::mcdc_coverage ? $self->owner_tlaCount($owner, 'found', MCDC_DATA) : 0; my $funcTotal = $lcovutil::func_coverage ? $self->owner_tlaCount($owner, 'found', FUNCTION_DATA) : 0; push(@owners, [$owner, [$lineMissed, $lineTotal, $lineCb], [$branchMissed, $branchTotal, $branchCb], [$funcMissed, $funcTotal, $functionCb], [$mcdcMissed, $mcdcTotal, $mcdcCb] ]); } } @owners = sort({ $b->[1]->[0] <=> $a->[1]->[0] || # missed $b->[1]->[1] <=> $a->[1]->[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 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 (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) { 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 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 (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) { for (my $bin = 0; $bin <= $#ageGroupHeader; ++$bin) { foreach my $key (keys %{$self->[$covType]->[DATA]}) { # duplicate line-coverage buckets my $ageval = $self->age_sample($bin); if ($covType == LINE_DATA) { $self->lineCovCount($key, "age", $ageval, $record->lineCovCount($key, "age", $ageval)); } elsif ($covType == BRANCH_DATA) { $self->branchCovCount($key, "age", $ageval, $record->branchCovCount($key, "age", $ageval)); } elsif ($covType == MCDC_DATA) { $self->mcdcCovCount($key, "age", $ageval, $record->branchCovCount($key, "age", $ageval)); } else { $self->functionCovCount($key, 'age', $ageval, $record->functionCovCount($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}; } 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}; } 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); # LCOV_EXCL_START unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected key '$key' for bin '$bin'"); return; } # LCOV_EXCL_STOP $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } 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}; } 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); # LCOV_EXCL_START unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected key '$key' for bin '$bin'"); return; } # LCOV_EXCL_STOP $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } 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}; } 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); # LCOV_EXCL_START unless (exists($a->[$bin]) && exists($a->[$bin]->{$key})) { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected key '$key' for bin '$bin'"); return; } # LCOV_EXCL_STOP $a->[$bin]->{$key} += $delta; return $a->[$bin]->{$key}; } 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}; } 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; } } } 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 selected { my $rtn; eval { $rtn = $selectCallback->select(@_); }; if ($@) { $rtn = 1; # return everything lcovutil::ignorable_error($lcovutil::ERROR_CALLBACK, "select(..) failed: $@"); } 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; } else { die("unknown type $t") unless 'branch' eq $t; $type = BRANCH_DATA; } my $str = ''; my $sep = ''; my $d = $self->[$type]->[DATA]; foreach my $tla (@tlaPriorityOrder) { my $count = $d->{$tla}; next if $count == 0; $str .= $sep . $tla . ':' . $count; $sep = ' '; } return $str; } 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 (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) { my $key = type2str($t); my $d = $self->[$t]->[DATA]; foreach my $k (keys %$d) { # $k will be 'hit', 'found' or one of the TLAs my $count = $d->{$k}; next if $count == 0; if (exists $data{$key}) { $data{$key}->{$k} = $count; } else { $data{$key} = {$k => $count}; } } } if (grep(/^date$/, @CoverageCriteria::criteriaCallbackTypes)) { foreach my $t (LINE_DATA, FUNCTION_DATA, BRANCH_DATA, MCDC_DATA) { my $key = type2str($t); next unless exists($self->[$t]->[AGE]); my $ageBins = $self->[$t]->[AGE]; foreach my $i (0 .. $#cutpoints + 1) { my $bin = $ageBins->[$i]; foreach my $k (@tlaPriorityOrder, 'found', 'hit') { my $count = $bin->{$k}; next if $count == 0; my $b; if (exists($data{$key})) { $b = $data{$key}; } else { $b = {}; $data{$key} = $b; } if (exists($b->{$i})) { $b->{$i}->{$k} = $count; } else { $b->{$i} = {$k => $count}; } } } } } if (grep(/^owner$/, @CoverageCriteria::criteriaCallbackTypes)) { foreach my $t (LINE_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)) { my $b; if (exists($data{$key})) { $b = $data{$key}; } else { $b = {}; $data{$key} = $b; } my $d = {}; $b->{$owner} = $d; foreach my $k (@tlaPriorityOrder, 'found', 'hit') { next unless exists($bin->{$k}); my $count = $bin->{$k}; next if $count == 0; $d->{$k} = $count; } } } } 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 eq $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)); return $summary->lineCovCount($key, 'age', $age) if $covType == SummaryInfo::LINE_DATA; return $summary->functionCovCount($key, 'age', $age) if $covType == SummaryInfo::FUNCTION_DATA; return $summary->mcdcCovCount($key, 'age', $age) if $covType == SummaryInfo::MCDC_DATA; die('$covType coverage not yet implemented') if $covType != SummaryInfo::BRANCH_DATA; return $summary->branchCovCount($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//; $page_link .= $lcovutil::dirseparator . "index"; } $page_link .= '.' . $main::html_ext; return $lcovutil::case_insensitive ? lc($page_link) : $page_link; } sub data { my $self = shift; # ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $page_link, # $fileSummary, $fileDetails) my $summary = $self->summary(); my @rtn; foreach my $type (SummaryInfo::LINE_DATA, SummaryInfo::FUNCTION_DATA, SummaryInfo::BRANCH_DATA, SummaryInfo::MCDC_DATA ) { my $hash = $summary->[$type]->[SummaryInfo::DATA]; push(@rtn, $hash->{found}, $hash->{hit}); } my $link = $self->page_link(); my $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 = $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, lineCovData, branchCovData]] for each owner # where lineCovData = [missedCount, totalCount, callback] # branchCovData = [missed, total, callback] or undef if not enabled # - 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) = @_; $covtype == SummaryInfo::LINE_DATA || $covtype == SummaryInfo::BRANCH_DATA || $covtype == SummaryInfo::MCDC_DATA || $covtype == SummaryInfo::FUNCTION_DATA or die("'$covtype' type not supported"); return DateDetailCallback->new($self->summary(), $ageval, $covtype); } sub ownerDetailCallback { # callback to compute count in particular owner bin my ($self, $owner, $covtype) = @_; $covtype == SummaryInfo::LINE_DATA || $covtype == SummaryInfo::BRANCH_DATA or $covtype == SummaryInfo::MCDC_DATA or die("'$covtype' type not supported"); return OwnerDetailCallback->new($self->summary(), $owner, $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; my $lineCb = OwnerDetailCallback->new($self->[1], $self->[0], SummaryInfo::LINE_DATA); my $found = $lineCb->get('found'); my $hit = $lineCb->get('hit'); my $branchCb = OwnerDetailCallback->new($self->[1], $self->[0], SummaryInfo::BRANCH_DATA); my $mcdcCb = OwnerDetailCallback->new($self->[1], $self->[0], SummaryInfo::MCDC_DATA); my $fn_found = 0; # ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $page_link, # $fileSummary, $fileDetails) # this is the 'totals' callback for this owner - so there is no # associated file or summary info. Pass undef. return ($lineCb->get('found'), $lineCb->get('hit'), 0, 0, # fn_found, fn_hit $branchCb->get('found'), $branchCb->get('hit'), $mcdcCb->get('found'), $mcdcCb->get('hit')); } 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::BRANCH_DATA || $covtype == SummaryInfo::MCDC_DATA); return OwnerDetailCallback->new($self->[1], $self->name(), $covtype); } sub findFileList { my ($self, $all) = @_; # return [ [filename, lineCovData, branchCovData]] for each file # such that this owner has at least 1 line. # where lineCovData = [missedCount, totalCount, OwnerDetailCallback] # branchCovData = [missed, total, dateDetailCallback] # or undef if not enabled # - 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 $lineCb = OwnerDetailCallback->new($source, $owner, SummaryInfo::LINE_DATA); my $brCb = OwnerDetailCallback->new($source, $owner, SummaryInfo::BRANCH_DATA); my $mcdcCb = OwnerDetailCallback->new($source, $owner, SummaryInfo::MCDC_DATA); my $funcCb = OwnerDetailCallback->new($source, $owner, SummaryInfo::FUNCTION_DATA); my $total = $lineCb->get('found'); my $br_total = $lcovutil::br_coverage ? $brCb->get('found') : 0; my $fn_total = $lcovutil::func_coverage ? $funcCb->get('found') : 0; my $mcdc_total = $lcovutil::mcdc_coverage ? $mcdcCb->get('found') : 0; next if (0 == $total && 0 == $br_total && 0 == $mcdc_total && 0 == $fn_total); my $missed = $lineCb->get('missed'); my $br_missed = $lcovutil::br_coverage ? $brCb->get('missed') : 0; my $mcdc_missed = $lcovutil::mcdc_coverage ? $mcdcCb->get('missed') : 0; my $fn_missed = $lcovutil::func_coverage ? $funcCb->get('missed') : 0; if ($all || 0 != $missed || 0 != $br_missed || 0 != $fn_missed || 0 != $mcdc_missed) { push(@files, [$file, [$missed, $total, $lineCb], [$br_missed, $br_total, $brCb], [$mcdc_missed, $mcdc_total, $mcdcCb], [$fn_missed, $fn_total, $funcCb] ]); } 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 = $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::LINE_DATA, SummaryInfo::FUNCTION_DATA, SummaryInfo::BRANCH_DATA, SummaryInfo::MCDC_DATA ) { my $cb = DateDetailCallback->new($self->[2], $self->[1], $covType); push(@rtn, $cb->get('found'), $cb->get('hit')); } # ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $page_link, # $fileSummary, $fileDetails) # 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 [ [filename, lineCovData, branchCovData]] for each file # such that this owner has at least 1 line. # where lineCovData = [missedCount, totalCount, OwnerDetailCallback] # branchCovData = [missed, total, dateDetailCallback] # or undef if not enabled # - 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 $lineCb = DateDetailCallback->new($source, $ageval, SummaryInfo::LINE_DATA); my $brCb = DateDetailCallback->new($source, $ageval, SummaryInfo::BRANCH_DATA); my $mcdcCb = DateDetailCallback->new($source, $ageval, SummaryInfo::MCDC_DATA); my $funcCb = DateDetailCallback->new($source, $ageval, SummaryInfo::FUNCTION_DATA); my $total = $lineCb->get('found'); my $br_total = $lcovutil::br_coverage ? $brCb->get('found') : 0; my $mcdc_total = $lcovutil::mcdc_coverage ? $mcdcCb->get('found') : 0; my $fn_total = $lcovutil::func_coverage ? $funcCb->get('found') : 0; next if (0 == $total && 0 == $br_total && 0 == $mcdc_total && 0 == $fn_total); my $missed = $lineCb->get('missed'); my $br_missed = $lcovutil::br_coverage ? $brCb->get('missed') : 0; my $mcdc_missed = $lcovutil::mcdc_coverage ? $mcdcCb->get('missed') : 0; my $fn_missed = $lcovutil::func_coverage ? $funcCb->get('missed') : 0; if ($all || 0 != $missed || 0 != $br_missed || 0 != $mcdc_missed || 0 != $fn_missed) { push(@files, [$file, [$missed, $total, $lineCb], [$br_missed, $br_total, $brCb], [$mcdc_missed, $mcdc_total, $mcdcCb], [$fn_missed, $fn_total, $funcCb] ]); } 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 = $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 ($covType eq SummaryInfo::LINE_DATA || $covType eq SummaryInfo::BRANCH_DATA || $covType eq SummaryInfo::MCDC_DATA || $covType eq SummaryInfo::FUNCTION_DATA); 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; die("CovTypeSummaryCallback::owner not supported for " . $self->[1]) unless ($self->[1] == SummaryInfo::BRANCH_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, }; sub new { my ($class, $sourceFileStruct, $lineCovInfo) = @_; my $self = [$sourceFileStruct, $lineCovInfo, "", # current TLA "", # owner "", # age {}, # next header line for corresponding owner {}, # next header line for corresponding date bin undef # line number ]; bless $self, $class; return $self; } 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 age 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 open { my ($self, $filename) = @_; my $diffmap = $self->[1]; if (defined($diffmap) && $diffmap->containsFile($filename)) { # if there are any diffs, then need to load the current source, then # walk the diff to insert and remove changed lines my $currentSrc = $self->_load($filename, 'baseline'); my $src = $diffmap->recreateBaseline($filename, $currentSrc); return $self->parseLines($filename, $src); } # else no diff data here - just read the file return ReadCurrentSource::open($self, $filename, 'baseline'); } 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, # 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 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 $br = $branch->getBlock($block); my @b; push(@data, \@b); foreach my $b (@$br) { my ($br, $tla, $d) = @$b; my ($base_count, $curr_count) = @$d; 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()) { my $br = $branch->getBlock($block); 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) = @_; 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, "inconsistent $which line location $loc: " . $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; } } return $linecov->[LINE_CURRENT]; } sub _mergeBranchData { my ($self, $loc, $branchData, $filename) = @_; my $branch = $self->[BRANCH_DATA]; if (defined($branch->[$loc])) { my $current = $branch->[$loc]; foreach my $branchId ($current->blocks()) { # LCOV_EXCL_START if (!$branchData->hasBlock($branchId)) { # don't know how to get here...but someone on the internet # managed to do it - so we need to handle the error my $which = $loc == DATA_BASELINE ? 'baseline' : 'current'; lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, $filename . ':' . $current->line() . ": '$which' line " . $self->lineNo($which) . " merge block (line " . $branchData->line() . ") does not contain branch $branchId." ); next; } # LCOV_EXCL_STOP my $c = $current->getBlock($branchId); my $d = $branchData->getBlock($branchId); # handle case of inconsistent branch data my $nc = scalar(@$c); my $nd = scalar(@$d); # LCOV_EXCL_START if ($nc != $nd) { # similarly: this should not happen - but it might my $which = $loc == DATA_BASELINE ? 'baseline' : 'current'; lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, $filename . ':' . $current->line() . ": '$which' line " . $self->lineNo($which) . " branch $branchId contains $nc elements but merge data contains $nd." ); } # LCOV_EXCL_STOP for (my $i = ($nc > $nd ? $nd : $nc) - 1; $i >= 0; --$i) { my $br = $d->[$i]; $c->[$i]->merge($br); } # now append any new values from 'branchData': # (go here only if |D| > |C| and we ignore the mismatch error for (my $i = $nc; $i < $nd; ++$i) { push(@$c, Storable::dclone($d->[$i])); } } } else { $branch->[$loc] = Storable::dclone($branchData); } } sub baseline_branch { my ($self, $branchData, $filename) = @_; die("has baseline branch data but not in baseline") if (defined($branchData) && !defined($self->[LINENO_BASE])); if (defined($branchData)) { $self->_mergeBranchData(DATA_BASELINE, $branchData, $filename); } my $branch = $self->[BRANCH_DATA]; return $branch->[DATA_BASELINE]; } sub current_branch { my ($self, $branchData, $filename) = @_; 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, $filename); } 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, $filename) = @_; my $mcdc = $self->[MCDC_DATA]; if (defined($mcdc->[$loc])) { my $current = $mcdc->[$loc]; $current->merge($mcdcData); } else { $mcdc->[$loc] = Storable::dclone($mcdcData); } } sub baseline_mcdc { my ($self, $mcdcData, $filename) = @_; 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, $filename); } my $mcdc = $self->[MCDC_DATA]; return $mcdc->[DATA_BASELINE]; } sub current_mcdc { my ($self, $mcdcData, $filename) = @_; 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, $filename); } 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]; if (defined($function->[$loc])) { my $current = $function->[$loc]; $current->merge($functionData); } else { # 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]; } # structure holding coverage data for a particular file: # - associated with a line 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, }; 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)); # line coverage categorization includes date- and owner- bins in # the vanilla case when there is no baseline. $self->_categorizeLineCov($filename, $base_data, $current_data, $diffMap, $verbose); $self->_categorizeBranchCov($filename, $base_data, $current_data, $diffMap, $verbose) if ($lcovutil::br_coverage); $self->_categorizeMcdcCov($filename, $base_data, $current_data, $diffMap, $verbose) if ($lcovutil::mcdc_coverage); $self->_categorizeFunctionCov($filename, $base_data, $current_data, $diffMap, $verbose) if ($lcovutil::func_coverage); while (my ($lineNo, $deleted) = each(%{$self->[DELETED_LINE_LEADER]})) { @$deleted = sort({ $a->lineNo('base') <=> $b->lineNo('base') } @$deleted); } 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 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 $branchId ($br->blocks()) { my $diff = $br->getBlock($branchId); foreach my $b (@$diff) { my $tla = $b->[1]; if (exists($remap{$tla})) { $b->[1] = $remap{$tla}; } } } } # if branch data # MC/DC coverage... if ($lcovutil::mcdc_coverage && defined($data->differential_mcdc())) { my $mcdc = $data->differential_mcdc(); while (my ($groupSize, $group) = each(%{$mcdc->groups()})) { foreach my $cond (@$group) { # remap both the true and false sense.. foreach my $sense (0, 1) { my $c = $cond->count($sense); my $tla = $c->[0]; if (exists($remap{$tla})) { $c->[0] = $remap{$tla}; } } } } } # if MCDC data # function coverage.. if ($lcovutil::func_coverage && defined($data->differential_function())) { my $func = $data->differential_function(); my $hit = $func->hit(); my $tla = $hit->[1]; if (exists($remap{$tla})) { $hit->[1] = $remap{$tla}; } while (my ($alias, $data) = each(%{$func->aliases()})) { my $tla = $data->[1]; if (exists($remap{$tla})) { $data->[1] = $remap{$tla}; } } } } # if function data } sub _categorize { my ($baseCount, $currCount) = @_; my $tla; if (0 == $baseCount) { $tla = (0 == $currCount) ? "UBC" : "GBC"; } elsif (0 == $currCount) { $tla = "LBC"; } else { $tla = "CBC"; } return $tla; } 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 ne "delete") { if (!defined($lineDataMap->{$current_lineNo})) { $linedata = LineData->new($type); $lineDataMap->{$current_lineNo} = $linedata; $linedata->lineNo('current', $current_lineNo); } else { $linedata = $lineDataMap->{$current_lineNo}; } $linedata->lineNo('base', $base_lineNo); } 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); $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; for (my $i = $base_lineNo - 1; $i > 0; --$i) { if ('delete' ne $diffMap->type($filename, $diffMap->OLD, $i)) { $c = $diffMap->lookup($filename, $diffMap->OLD, $i); last; } } if (!defined($c)) { # there were no 'current' lines above me - so I must be # at the first line in the file. It must not be deleted $c = 1; die("$filename:1: incorrectly marked 'delete'") if ( 'delete' eq $diffMap->type($filename, $diffMap->OLD, $c)); } die("$filename: no current block for deleted line $base_lineNo") unless defined($c); $linedata->lineNo('current', -$c); # keep track of where deleted lines were - so we can # mark them in the source view if (exists($self->[DELETED_LINE_LEADER]->{$c})) { push(@{$self->[DELETED_LINE_LEADER]->{$c}}, $linedata); } else { $self->[DELETED_LINE_LEADER]->{$c} = [$linedata]; } } 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(); if ($verbose) { print("categorize lines $filename\n"); } # $lineCovBase, $lineCovCurrent are CountData objects my $lineCovBase = $base_data->sum() if defined($base_data); my $lineCovCurrent = $current_data->sum(); # 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); # LCOV_EXCL_START if ($type eq 'delete') { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "'current' line $filename:$line should not be marked 'delete'"); delete($branchCurrent->{$line}); next; } # LCOV_EXCL_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 eq '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}; } my $val = $lineCovCurrent->value($line); $linedata->lineNo("current", $line); $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); } 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() eq "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() eq "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() eq "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 _cloneBranchEntry { my ($cloneInto, $cloneFrom, $missTla, $hitTla) = @_; foreach my $branchId ($cloneFrom->blocks()) { my $block = $cloneInto->addBlock($branchId); foreach my $br (@{$cloneFrom->getBlock($branchId)}) { my $count = $br->count(); my $tla = (0 == $count) ? $missTla : $hitTla; push(@$block, [$br, $tla, [undef, $count]]); } } } # categorize branch coverage numbers sub _categorizeBranchCov { my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; my $lineDataMap = $self->lineMap(); my $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 probablilty 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); # append this branch data for the line my $currBranchData = $branchCurrent->value($line); $data->current_branch($currBranchData, $filename); if (!defined($branchBaseline)) { my $categorized = BranchEntry->new($line); $data->differential_branch($categorized); _cloneBranchEntry($categorized, $currBranchData, 'UNC', 'GNC'); } } # 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 ne 'delete') { $branchCovLines{$curr_line} = 1; } else { # the line has been deleted...just record the data my $deleteKey = "<<<" . $base_line; } my $baseBranchData = $branchBaseline->value($base_line); $data->baseline_branch($baseBranchData, $filename); } # foreach line in baseline data # go through all the branch data for each line, and categorize everything foreach my $line (keys(%branchCovLines)) { next if ($line <= 0); # ignore bogus my $data = $self->lineMap()->{$line}; my $type = $data->type(); my $curr = $data->current_branch(); my $base = $data->baseline_branch(); my $categorized = BranchEntry->new($line); $data->differential_branch($categorized); # handle case that baseline and/or current do not contain branch data my @currBlocks = defined($curr) ? $curr->blocks() : (); my @baseBlocks = defined($base) ? $base->blocks() : (); if ($type eq '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); _cloneBranchEntry($categorized, $curr, 'UNC', 'GNC'); } elsif ($type eq '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); _cloneBranchEntry($categorized, $base, 'DUB', 'DCB'); } else { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected branch coverage type $type at $filename:$line") if $type ne 'equal'; # branch might or might not be in both baseline and current foreach my $branchId (@baseBlocks) { my $b = $base->getBlock($branchId); my $branchData = $categorized->addBlock($branchId); if (defined($curr) && $curr->hasBlock($branchId)) { my $c = $curr->getBlock($branchId); my $num_base = scalar(@$b); my $num_curr = scalar(@$c); my $max = $num_base > $num_curr ? $num_base : $num_curr; my $tla; for (my $i = 0; $i < $max; ++$i) { if ($i < $num_base && $i < $num_curr) { my $base_br = $b->[$i]; my $curr_br = $c->[$i]; $tla = _categorize($base_br->count(), $curr_br->count()); push(@$branchData, [$curr_br, $tla, [$base_br->count(), $curr_br->count()] ]); } elsif ($i < $num_base) { my $base_br = $b->[$i]; $tla = (0 == $base_br->count()) ? 'EUB' : 'ECB'; push(@$branchData, [$base_br, $tla, [$base_br->count(), undef]]); } else { my $curr_br = $c->[$i]; $tla = (0 == $curr_br->count()) ? 'UIC' : 'GIC'; push(@$branchData, [$curr_br, $tla, [undef, $curr_br->count()]]); } } } else { # branch not found in current... foreach my $base_br (@$b) { my $tla = (0 == $base_br->count()) ? 'EUB' : 'ECB'; push(@$branchData, [$base_br, $tla, [$base_br->count(), undef]]); } } } # now check for branches that are in current but not in baseline... foreach my $branchId (@currBlocks) { next if defined($base) && $base->hasBlock($branchId); # already processed my $c = $curr->getBlock($branchId); my $branchData = $categorized->addBlock($branchId); foreach my $curr_br (@$c) { my $tla = (0 == $curr_br->count()) ? 'UIC' : 'GIC'; push(@$branchData, [$curr_br, $tla, [undef, $curr_br->count()]]); } } # foreach branchId in current that isn't in base } } } sub _cloneMcdcEntry { my ($cloneInto, $cloneFrom, $missTla, $hitTla) = @_; while (my ($groupSize, $group) = each(%{$cloneFrom->groups()})) { foreach my $expr (@$group) { foreach my $sense (0, 1) { my $count = $expr->count($sense); my $tla = (0 == $count) ? $missTla : $hitTla; $count = [$tla, undef, $count] ; # [TLA, baseline value, current value] $cloneInto->insertExpr('unknownFile', $groupSize, $sense, $count, $expr->index(), $expr->expression()); } } } } sub _categorizeMcdcCov { my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; my $lineDataMap = $self->lineMap(); my $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 probablilty 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); # append this MC/DC data for the line (this is an MCDC_Block) my $currMcdcData = $mcdcCurrent->value($line); $data->current_mcdc($currMcdcData, $filename); if (!defined($mcdcBaseline)) { my $categorized = MCDC_Block->new($line); $data->differential_mcdc($categorized); _cloneMcdcEntry($categorized, $currMcdcData, 'UNC', 'GNC'); } } # 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 ne 'delete') { $mcdcCovLines{$curr_line} = 1; } else { # the line has been deleted...just record the data my $deleteKey = "<<<" . $base_line; } my $baseMcdcData = $mcdcBaseline->value($base_line); $data->baseline_mcdc($baseMcdcData, $filename); } # 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 conta MC/DC data my $currGroups = defined($curr) ? $curr->groups() : (); my $baseGroups = defined($base) ? $base->groups() : (); if ($type eq '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'); } elsif ($type eq '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 ne 'equal'; # group might or might not be in both baseline and current while (my ($groupSize, $bgroup) = each(%$baseGroups)) { if (defined($currGroups) && exists($currGroups->{$groupSize})) { my $cgroup = $currGroups->{$groupSize}; for (my $i = 0; $i < $groupSize; ++$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 $tla = _categorize($b_count, $c_count); $categorized->insertExpr('unknownFile', $groupSize, $sense, [$tla, $b_count, $c_count], $i, $c->expression()); } } } 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', $groupSize, $sense, [$tla, $b_count, undef], $b->index(), $b->expression()); } } } } #foreach group in base } # 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]); my $aliases = $cloneFrom->aliases(); foreach my $alias (keys %$aliases) { $hit = $aliases->{$alias}; $tla = (0 == $hit) ? $missTla : $hitTla; $cloneInto->addAliasDifferential($alias, [$hit, $tla]); } } sub _categorizeFunctionCov { my ($self, $filename, $base_data, $current_data, $diffMap, $verbose) = @_; die("map should not be defined yet") unless !defined($self->functionMap()); my $differentialMap = {}; push(@$self, $differentialMap); my $lineDataMap = $self->lineMap(); my $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 = $base_data->sum() if defined($base_data); my $lineCovCurrent = $current_data->sum(); 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 eq '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() eq $type; } # we expect that the line number matches... $data->lineNo("current", $line); # 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'); } } # foreach function in current data return unless (defined($funcBase)); # 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 my $data = $self->_findLineData($diffMap, $filename, $line); my $type = $data->type(); my $curr_line = $data->lineNo('current'); if ($type ne '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 eq 'delete'; _cloneFunctionEntry($categorized, $curr, $type eq 'insert' ? 'UNC' : 'UIC', $type eq '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 eq 'insert'; _cloneFunctionEntry($categorized, $base, $type eq 'delete' ? 'DUB' : 'EUB', $type eq 'delete' ? 'DCB' : 'ECB'); } else { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "unexpected function coverage type $type at $filename:$line") if $type ne '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: # - line is new and is source code, OR # - line is unchanged and was code before and isn't code # now, or line wasn't code before and is now if ($type eq 'insert' && defined($lineCovCurrent->value($line))) # line is code { $changed = 1; last; } else { # line is same my $bline = $diffMap->lookup($filename, $diffMap->NEW, $line); if (defined($lineCovBase->value($bline)) ^ defined($lineCovCurrent->value($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. # note that we already checked unchanged lines, above if ($type ne 'equal' && # line is in old but not in new defined($lineCovBase->value($bline)) ) # line is code { $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 my $base_aliases = $base->aliases(); my $curr_aliases = $curr->aliases(); foreach my $alias (keys %$base_aliases) { my $hit = $base_aliases->{$alias}; my $tla; if (exists($curr_aliases->{$alias})) { my $hitCurr = $curr_aliases->{$alias}; $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]); } # now look for aliases that are in current but not in baseline foreach my $alias (keys %$curr_aliases) { next if exists($base_aliases->{$alias}); my $hit = $curr_aliases->{$alias}; my $tla = (0 == $hit) ? "UIC" : "GIC"; $categorized->addAliasDifferential($alias, [$hit, $tla]); } } 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); } } } } package DiffMap; # @todo Could convert this use 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 => 0, NEW => 1, TYPE => 2, _START => 0, _END => 1 }; sub new { my $class = shift; my $self = [{}, # linemap {}, # filemap {}, # 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 numbern {} # unchanged ]; bless $self, $class; return $self; } sub load { my ($self, $path, $info, $buildDirs) = @_; $self->_read_udiff($path); if (@$buildDirs) { # find list of soft links in [buildDirs] which point to files in $info my @stack = @$buildDirs; my %alias; 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); $alias{$path} = $l if (-f $l); # really, this should be a file... die("unexpected soft link $path to directory") unless -f $l; } # else just ignore file entry } closedir($dh); } # now look through list of filenames in the current .info file, and see # if any match the soft links found above foreach my $f ($info->files()) { my $path = $f; $path = File::Spec->catfile($main::cwd, $f) unless File::Spec->file_name_is_absolute($f); if (exists($alias{$path})) { #lcovutil::info("found alias $alias{$path} of $f\n"); $self->[ALIASES]->{$path} = $alias{$path}; } } } 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 recreateBaseline { my ($self, $filename, $currentSrcLines) = @_; my $diffs = $self->[LINEMAP]->{$self->findName($filename)}; die("no diff data for $filename") unless defined $diffs; my $deleted = $self->[BASELINE]->{$self->findName($filename)}; my @lines; foreach my $chunk (@$diffs) { if ($chunk->[TYPE] eq 'equal') { my ($from, $to) = @{$chunk->[NEW]}; push(@lines, @{$currentSrcLines}[($from - 1) .. ($to - 1)]); } elsif ($chunk->[TYPE] eq 'delete') { my $r = $chunk->[OLD]; for (my $i = $r->[_START]; $i <= $r->[_END]; ++$i) { die("missing baseline line $i") unless defined($deleted) && exists($deleted->{$i}); push(@lines, $deleted->{$i}); } } # else 'insert': nothing to do/those lines are not in baseline } return \@lines; } sub lookup { my ($self, $file, $vers, $line) = @_; $file = $self->findName($file); if (!exists($self->[LINEMAP]->{$file})) { #mapping is identity when no diff was read return $line; } my @candidates = grep { $_->[$vers]->[_START] < $line } @{$self->[LINEMAP]->{$file}}; # candidates is empty if $line==1 - which is unusual, as there is typically # a comment, copyright notice, #include, or whatever on the first line return $line unless @candidates; my $chunk = pop @candidates; my $alt = ($vers == OLD) ? NEW : OLD; if ($line > $chunk->[$vers]->[_END]) { return ($chunk->[$alt]->[_END] + ($line - $chunk->[$vers]->[_END])); } return ($chunk->[$alt]->[_START] + ($line - $chunk->[$vers]->[_START])); } 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 "equal"; # categories will be "GIC", "UIC" } else { return "insert"; # categories will be "GNC", "UNC" } } if (!defined($self->[FILEMAP]->{$file})) { #mapping with no filemap when baseline file was deleted return "delete"; } # ->{start} equal $line only if beginning of range or omitted in ->{type} my @candidates = grep { $_->[$vers]->[_START] <= $line } @{$self->[LINEMAP]->{$file}}; my $chunk = pop @candidates; my $prev = pop @candidates; while (defined($prev) && $line >= $prev->[$vers]->[_START] && $line <= $prev->[$vers]->[_END]) { $chunk = $prev; $prev = pop @candidates; } if (!defined($chunk)) { warn "DiffMap::type(): got undef chunk at $file, $vers, $line\n"; return "undef chunk"; } if (!defined($chunk->[TYPE])) { warn "DiffMap::type(): got undef type at $file, $vers, $line\n"; return "undef type"; } return $chunk->[TYPE]; } 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 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; my %diffBaseMap; foreach my $f ($self->files()) { $diffMap{$f} = 0; my $b = File::Basename::basename($f); $diffBaseMap{$b} = [[], {}] unless exists($diffBaseMap{$b}); push(@{$diffBaseMap{$b}->[0]}, $f); } foreach my $f (keys %{$self->[UNCHANGED]}) { # unchanged in baseline and current $diffMap{$f} = 3; } my %missed; 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}; } else { $missed{$curr} = 1; # in current but not in diff } } 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}; } else { # 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); if (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}); } if (scalar(@unused)) { my $type; # my $baseData = $baseline->data($f); # my $baseLocation = join(":", ${$baseData->location()}); # my $currData = $current->data($f); # my $currLocation = join(":", ${$currData->location()}); 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", $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; 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 { $self->[DIFF_ROOT] = $1; last; }; # Filename of unchanged file: # === /^=== (.+)$/ && do # note: filename may contain whitespace { if ($filename) { die("not case insensitive") unless !$lcovutil::case_insensitive || ($filename eq lc($filename)); push(@{$self->[LINEMAP]->{$filename}}, $chunk); undef $filename; } my $file = $1; $file = lcovutil::strip_directories($file, $main::strip); my $key = ReadCurrentSource::resolve_path($file, 1); $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; # LCOV_EXCL_START if (exists($self->[LINEMAP]->{$key})) { 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_EXCL_STOP $self->[UNCHANGED]->{$key} = 1; last; }; # 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::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::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) { # LCOV_EXCL_START if (exists($self->[UNCHANGED]->{$key})) { 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_EXCL_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; # 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 @@ /^\@\@\s+-(\d+),(\d+)\s+\+(\d+),(\d+)\s+\@\@.*$/ && do { if ($1 > ($chunk->[OLD]->[_END])) { # old start skips "equal" lines if ($chunk->[TYPE] ne "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; $old_block = $2; $new_block = $4; #printf "equal [%d:%d] [%d:%d]\n", $l[0], $l[1], $l[2], $l[3]; last; }; # Unchanged line # /^ / && do { if ($old_block == 0 && $new_block == 0) { last; } if ($chunk->[TYPE] ne "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] ne "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] ne "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] ne "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); } # default root $self->[DIFF_ROOT] = $main::cwd unless defined($self->[DIFF_ROOT]); 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 = $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 = $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, }; sub new { my $class = shift; # [lineNo, text, abbrev_name, full_name, date, age, commitID, lineTLA, branchTLA, # MCDC_TLA, functionTLA] 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 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 => 5, MCDC_CATEGORIES => 6, }; 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 {}, # owner -> hash of TLS->list of lines {}, # TLA -> list of lines {} # owner -> hash of TLA->list of lines ]; 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); 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(); # no annotations for deleted line my $annotateData = $self->line($line) unless grep(/$tla/, ('DUB', 'DCB')); # callback arguments are (LineData, SourceLine) if (SummaryInfo::selected($lne, $annotateData, $self->path(), $line)) { push(@lineArray, $line); } } @lineArray = sort({ $a <=> $b } @lineArray); $self->[INTERESTING_LINES] = \@lineArray; $inRegion = InInterestingRegion->new($self, $lineCovData); } 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); $ka <=> $kb } keys(%{$lineCovData})) ) { # is this line within N of an interesting one? my $lne = $lineCovData->{$line}; my $tla = $lne->tla(); # deleted line associated with first 'current' line above - # or line below, if there is no current line above my $lineNum = grep(/$tla/, ('DUB', 'DCB')) ? -$lne->lineNo('current') : $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); $self->_countBranchTlaData($line, $lne, $fileSummary) if ($lcovutil::br_coverage && defined($lne->differential_branch())); $self->_countMcdcTlaData($line, $lne, $fileSummary) if ($lcovutil::mcdc_coverage && defined($lne->differential_mcdc())); $self->_countFunctionTlaData($line, $lne, $fileSummary) if ($lcovutil::func_coverage && defined($lne->differential_function())); } 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 grep(/$tla/, ('DUB', 'DCB')); 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 grep(/$tla/, ('DUB', 'DCB')); 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 grep(/$tla/, ('DUB', 'DCB')); my $line = $self->nextMcdcInDateBin($bin, $tla); $keep{$line} = 1 if defined($line); } } } # retain first location of each TLA (globally) and each TLA for each owner foreach my $bin (LINE_OWNERS, LINE_CATEGORIES, BRANCH_OWNERS, BRANCH_CATEGORIES, MCDC_OWNERS, MCDC_CATEGORIES ) { while (my ($key, $list) = each(%{$self->[$bin]})) { if ('ARRAY' eq ref($list)) { next if grep(/$key/, ('DUB', 'DCB')); # 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 grep(/$tla/, ('DUB', 'DCB')); # 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 %foundBranchTlas; my ($src_age, $developer, $srcLine); my $lineTla = $lineData->tla(); $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. $src_age = $srcLine->age(); $developer = $srcLine->owner(); if (defined($developer)) { my $shash = $self->[BRANCH_OWNERS]; if (!exists($shash->{$developer})) { $shash->{$developer} = {}; $shash->{$developer}->{lines} = []; } push(@{$shash->{$developer}->{lines}}, $line); } } } my %recorded; foreach my $branchId ($differentialData->blocks()) { my $diff = $differentialData->getBlock($branchId); foreach my $b (@$diff) { my $tla = $b->[1]; # LCOV_EXCL_START unless (defined($tla)) { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef TLA for branch $branchId of " . $self->path() . ":$line - lineTLA:$lineTla taken:" . $b->[0]); next; } # LCOV_EXCL_STOP $fileSummary->[SummaryInfo::BRANCH_DATA]->[SummaryInfo::DATA] ->{$tla} += 1; # keep track of all the branch TLAs found on this line... if (!exists($foundBranchTlas{$tla})) { $foundBranchTlas{$tla} = 1; $self->[BRANCH_CATEGORIES]->{$tla} = [] unless exists($self->[BRANCH_CATEGORIES]->{$tla}); } push(@{$self->[BRANCH_CATEGORIES]->{$tla}}, $line); 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)) { # just count totals $fileSummary->branchCovCount($tla, 'noGroup', undef, 1); 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 = grep(/$tla/, ('GBC', 'GIC', 'GNC', 'CBC')); $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 $ownerKey = $developer . $tla; if (!exists($recorded{$ownerKey})) { $recorded{$ownerKey} = 1; my $dhash = $self->[BRANCH_OWNERS]->{$developer}; $dhash->{$tla} = [] unless exists($dhash->{$tla}); push(@{$dhash->{$tla}}, $line); } } } } sub _countMcdcTlaData { my ($self, $line, $lineData, $fileSummary) = @_; my $differentialData = $lineData->differential_mcdc(); my %foundMcdcTlas; my ($src_age, $developer, $srcLine); my $lineTla = $lineData->tla(); $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. $src_age = $srcLine->age(); $developer = $srcLine->owner(); if (defined($developer)) { my $shash = $self->[BRANCH_OWNERS]; if (!exists($shash->{$developer})) { $shash->{$developer} = {}; $shash->{$developer}->{lines} = []; } push(@{$shash->{$developer}->{lines}}, $line); } } } my %recorded; while (my ($groupSize, $group) = each(%{$differentialData->groups()})) { foreach my $expr (@$group) { foreach my $sense (0, 1) { my $count = $expr->count($sense); my $tla = $count->[0]; # LCOV_EXCL_START unless (defined($tla)) { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef TLA for sense $sense of MC/DC " . $expr->index() . ' of ' . $self->path() . ":$line - lineTLA:$lineTla taken:" . $count->[1]); next; } # LCOV_EXCL_STOP $fileSummary->[SummaryInfo::MCDC_DATA]->[SummaryInfo::DATA] ->{$tla} += 1; # keep track of all the branch TLAs found on this line... if (!exists($foundMcdcTlas{$tla})) { $foundMcdcTlas{$tla} = 1; $self->[MCDC_CATEGORIES]->{$tla} = [] unless exists($self->[MCDC_CATEGORIES]->{$tla}); } push(@{$self->[MCDC_CATEGORIES]->{$tla}}, $line); 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)) { # just count totals $fileSummary->mcdcCovCount($tla, 'noGroup', undef, 1); next; } # increment count of branches of this age we found for this TLA $fileSummary->mcdcCovCount($tla, "age", $src_age, 1); my $hit = grep(/$tla/, ('GBC', 'GIC', 'GNC', 'CBC')); $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 branchTLA to the owner... my $ownerKey = $developer . $tla; if (!exists($recorded{$ownerKey})) { $recorded{$ownerKey} = 1; my $dhash = $self->[MCDC_OWNERS]->{$developer}; $dhash->{$tla} = [] unless exists($dhash->{$tla}); push(@{$dhash->{$tla}}, $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 #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); 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) = @_; # LCOV_EXCL_START unless (defined($tla)) { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef function TLA for age '$src_age' of " . $fileSummary->name()); return 1; # error } # LCOV_EXCL_STOP $fileSummary->[SummaryInfo::FUNCTION_DATA]->[SummaryInfo::DATA]->{$tla} += 1; if (defined($src_age)) { $fileSummary->functionCovCount($tla, 'age', $src_age, 1); my $hit = grep(/$tla/, ('GBC', 'GIC', 'GNC', 'CBC')); $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]; # LCOV_EXCL_START if (!defined($mergedTla)) { 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_EXCL_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 { my $aliases = $func->aliases(); foreach my $alias (keys %$aliases) { my $data = $aliases->{$alias}; my $tla = $data->[1]; # LCOV_EXCL_START if (!defined($tla)) { lcovutil::ignorable_error($lcovutil::ERROR_INTERNAL, "undef TLA for alias:'$alias' hit:" . $data->[0] . " of function '" . $func->name() . "' hit " . $h->[0] . " at line " . $line . " (" . $lineData->tla() . ' ' . $lineData->curr_count() . ")"); # die("this should not happen either"); $tla = $data->[0] == 0 ? 'UNC' : 'GNC'; $data->[1] = $tla; } # LCOV_EXCL_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 line { my $self = shift; my $i = shift; die("bad line index '$i'") unless ($i =~ /^[0-9]+$/); return $self->[LINES]->[$i - 1]; } # how old is the oldest (or youngest) line in this file? sub age { my ($self, $youngest) = @_; return undef unless $self->isProjectFile(); my $age = $self->line(1)->age(); foreach my $line (@{$self->lines()}) { my $a = $line->age(); if (defined($youngest)) { $age = $a if ($a < $age); } else { $age = $a if ($a > $age); } } 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]; if ($v > $after) { $max = $mid; } elsif ($v < $after) { $min = $mid; } else { return $mid; } my $diff = $max - $min; if ($diff <= 1) { $mid = $min; $mid = $max if $list->[$min] < $after; last; } } 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 = nexTlaGroup('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 the 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 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 the 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 $t = $self->line($line)->tla(); my $a = $self->line($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 = $l->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; } exists($self->[LINE_OWNERS]->{$owner}) && exists($self->[LINE_OWNERS]->{$owner}->{$tla}) or die("$owner not responsible for any $tla lines in" . $self->path()); # note the 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 = $l->owner(); if (!defined($o)) { lcovutil::ignorable_error($lcovutil::ERROR_UNMAPPED_LINE, "no owber 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 the the "$self->line(...)" argument is 1-based (not zero-base) $after = defined($after) ? $after + 1 : 1; exists($self->[$category]->{$tla}) or die("no $tla ${type}es in " . $self->path()); 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 = $l->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 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 the 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 _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 my $data = exists($ENV{SOURCE_DATE_EPOCH}) ? "'SOURCE_DATE_EPOCH=$ENV{SOURCE_DATE_EPOCH}'" : "'now'"; lcovutil::ignorable_error($lcovutil::ERROR_INCONSISTENT_DATA, "File \"$path\": $data ($now) 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 $lineNum = 0; 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 foreach my $line (@$lines) { my ($text, $abbrev, $full, $when, $commit) = @$line; ++$lineNum; 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 $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 behavior 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 CounntData 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 $id ($branchEntry->blocks()) { my $block = $branchEntry->getBlock($id); foreach my $data (@$block) { my ($br, $tla) = @$data; my $count = $br->count(); die("unexpected branch TLA $tla for count $count at " . $fileDetails->path() . ":$line") unless (($count != 0) == exists($tlaData{$tla})); next if (0 == $count); $tlaData{$tla} += 1; } } } } elsif (SummaryInfo::MCDC_DATA == $covtype) { foreach my $line ($testcaseCounts->keylist()) { my $lineData = $fileDetails->line($line); # "SourceLine" structure my $mcdcEntry = $lineData->mcdcElem(); while (my ($groupSize, $group) = each(%{$mcdcEntry->groups()})) { foreach my $expr (@$group) { foreach my $sense (0, 1) { # use 'current' count... my ($tla, $b_count, $c_count) = @{$expr->count($sense)}; die("unexpected branch TLA $tla for count $c_count at " . $fileDetails->path() . ":$line") unless ( ($c_count != 0) == exists($tlaData{$tla})); next if (0 == $c_count); $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 @data; if ($main::merge_function_aliases) { push(@data, $differential->hit()); } else { my $aliases = $differential->aliases(); foreach my $alias (keys %$aliases) { push(@data, $aliases->{$alias}); } } foreach my $d (@data) { my ($count, $tla) = @$d; die("unexpected branch TLA $tla for count $count") unless (($count != 0) == exists($tlaData{$tla})); #next if (0 == $count); $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, CHILD_DATA => 4, RETRY_COUNT => 5, TMPDIR => 6, DELAY_TIMER => 7, CURRENT_PARALLEL => 8, }; 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 {}, # childData - for callback {}, # jobID -> fail count for this job File::Temp->newdir("genhtml_XXXX", DIR => $lcovutil::tmp_dir, CLEANUP => 1), 0, # delay timer 0 # current count of parallel jobs ]; 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] ], {} ]; # sort the worklist so segment will tend to contain files from the same directory foreach my $f (sort $current_data->files()) { 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); $short_name =~ s|^$lcovutil::dirseparator||; 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, 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 my @fileData = (SummaryInfo->new("file", $filename), [{}, {}, {}], $f); push(@$worklist, ['file', \@fileData, \@short_path, \@path, $mergeInto]); } $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) { my $base_name = File::Basename::basename($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 _waitChild { my ($self, $waitForPending, $noHang) = @_; my $currentSize = 0; my $consumption = 0; if (0 != $lcovutil::maxMemory) { $currentSize = lcovutil::current_process_size(); $consumption = ($self->[CURRENT_PARALLEL] + 1) * $currentSize; } my $reaped = 0; if ($waitForPending || $self->[CURRENT_PARALLEL] >= $lcovutil::maxParallelism || ($self->[CURRENT_PARALLEL] > 1 && $consumption > $lcovutil::maxMemory) ) { my $children = $self->[CHILD_DATA]; # wait here for child: # - while we are oversubscribed # - too many parallel jobs or # - consuming too much memory), OR # - if the worklist is empty but there are pending jobs # - the pending jobs will be put onto the worklist when # their dependency finishes my $worklist = $self->[WORKLIST]; my $pending = $self->[PENDING]; lcovutil::info(1, "memory constraint ($consumption > $lcovutil::maxMemory violated: waiting." . (scalar(@$worklist) + scalar(keys(%$pending))) . " remaining\n") if ($consumption > $lcovutil::maxMemory); my $start = Time::HiRes::gettimeofday(); my $child = wait(); my $end = Time::HiRes::gettimeofday(); $self->[DELAY_TIMER] += $end - $start; return 0 unless $child > 0; my $childstatus = $?; unless (exists($children->{$child})) { lcovutil::report_unknown_child($child); return $reaped; } $self->merge_child($child, $childstatus); $reaped += 1; } if ($noHang) { while (1) { my $start = Time::HiRes::gettimeofday(); my $child = waitpid(-1, POSIX::WNOHANG); if (0 < $child) { my $end = Time::HiRes::gettimeofday(); $self->[DELAY_TIMER] += $end - $start; my $childstatus = $?; my $children = $self->[CHILD_DATA]; unless (exists($children->{$child})) { lcovutil::report_unknown_child($child); next; } $self->merge_child($child, $childstatus); $reaped += 1; } else { last; } } } print("reaped $reaped current:", $self->[CURRENT_PARALLEL], " jobs:", scalar(@{$self->[JOBS]}), " work:", scalar(@{$self->[WORKLIST]}), " pending:", scalar(%{$self->[PENDING]}), "\n") if ($main::debugScheduler && $reaped); return $reaped; } 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->[CURRENT_PARALLEL], " jobs: ", scalar(@{$self->[JOBS]}), "\n") if $main::debugScheduler; } sub _report_fail_and_reschedule { my ($self, $jobId, $jobs, $childPid, $reason) = @_; my $counts = $self->[RETRY_COUNT]; if (exists($counts->{$jobId})) { $counts->{$jobId} += 1; } else { $counts->{$jobId} = 1; } lcovutil::report_fork_failure("compute job $jobId (child $childPid)", $reason, 0, $counts->{$jobId}); $self->_reschedule($jobId, $jobs); } sub _process_child { my ($self, $jobs, $jobId, $startTime) = @_; # clear the profile data - we want just my contribution my $childStart = Time::HiRes::gettimeofday(); my $tmp = '' . $self->[TMPDIR]; my $stdout_file = File::Spec->catfile($tmp, "genhtml_$$.log"); my $stderr_file = File::Spec->catfile($tmp, "genhtml_$$.err"); my $currentState = lcovutil::initial_state(); # clear counters so we store count only what we saw in the child $SourceFile::annotatedFiles = 0; $SourceFile::totalFiles = 0; my $status = 0; my @rtnData; my ($stdout, $stderr, $code) = Capture::Tiny::capture { 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 = $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 }; # end capture # print stdout and stderr ... foreach my $d ([$stdout_file, $stdout], [$stderr_file, $stderr]) { next unless ($d->[1]); # only print if there is something to print my $f = InOutFile->out($d->[0]); my $h = $f->hdl(); print($h $d->[1]); } my $file = File::Spec->catfile($tmp, "dumper_$$"); my $childEnd = Time::HiRes::gettimeofday(); $lcovutil::profileData{child}{$jobId} = $childEnd - $childStart; my $data; eval { $data = Storable::store([\@rtnData, [$SourceFile::annotatedFiles, $SourceFile::totalFiles ], lcovutil::compute_update($currentState), [$childStart, $childEnd] ], $file); my $done = Time::HiRes::gettimeofday(); printf(" %d: dump %d %d jobs %0.3fs %0.3fMb %s\n", $jobId, $$, scalar(@$jobs), $done - $childEnd, (-f $file ? (-s $file) : 0) / (1024 * 1024), DateTime->now()) if ($main::debugScheduler); }; if ($@ || !defined($data)) { lcovutil::ignorable_error($lcovutil::ERROR_PARALLEL, "Job $jobId: child $$ serialize failed" . ($@ ? ": $@" : '')); $status = 1; } return $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->[CURRENT_PARALLEL]; # 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) { my @jobs; push(@{$self->[JOBS]}, [\@jobs, $segmentID++]); foreach my $task (splice(@$worklist, 0, $tasksPerCore)) { 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 $children = $self->[CHILD_DATA]; my $failedAttempts = 0; my $segmentID = 0; WORK: while ($self->[CURRENT_PARALLEL] || @$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:", $self->[CURRENT_PARALLEL], " pending:", scalar(%$pending), "\n") if ($main::debugScheduler > 1 || ($main::debugScheduler && 0 == scalar(@$worklist) && 0 == scalar(@$joblist))); if ($self->_waitChild( 0 == scalar(@$worklist) && 0 == scalar(@$joblist), 1 )) { # reaped at least one child process next WORK; } } #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) my $reaped = $self->_waitChild(0); die("unexpected reap count $reaped") if $reaped > 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 && ($self->[CURRENT_PARALLEL] > 0 || 0 != scalar(@$joblist) || scalar(@$jobs) > 1) ) { $lcovutil::profileData{nJobs}{$jobId} = scalar(@$jobs); $lcovutil::deferWarnings = 1; my $pid = fork(); if (!defined($pid)) { # fork failed ++$failedAttempts; # report_fork_failure sleeps a bit if it doesn't error out lcovutil::report_fork_failure("process segment $jobId", $!, $failedAttempts); # put job back into schedule $self->_reschedule($jobId, $jobs); next; } # fork succeeded - so reset 'consecutive fails' counter $failedAttempts = 0; if (0 == $pid) { # I'm the child my $status = $self->_process_child($jobs, $jobId, $start); exit($status); } else { $children->{$pid} = [$jobs, $jobId, $start]; ++$self->[CURRENT_PARALLEL]; print("forked $jobId current: ", $self->[CURRENT_PARALLEL], " jobs:", scalar(@$joblist), "\n") if $main::debugScheduler; } } 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) } sub merge_child($$$) { my $mergeStart = Time::HiRes::gettimeofday(); my ($self, $childPid, $childstatus) = @_; my $children = $self->[CHILD_DATA]; --$self->[CURRENT_PARALLEL]; my ($jobs, $jobId, $start) = @{$children->{$childPid}}; delete($children->{$childPid}); my $tmp = '' . $self->[TMPDIR]; my $dumpfile = File::Spec->catfile($tmp, "dumper_$childPid"); my $childLog = File::Spec->catfile($tmp, "genhtml_$childPid.log"); my $childErr = File::Spec->catfile($tmp, "genhtml_$childPid.err"); foreach my $f ($childLog, $childErr) { if (!-f $f) { $f = ''; # there was no output next; } if (open(RESTORE, "<", $f)) { # slurp into a string and eval.. my $str = do { local $/; }; # slurp whole thing close(RESTORE) or die("unable to close $f: $!\n"); unlink $f; $f = $str; } else { $f = "unable to open $f: $!"; if (0 == $childstatus) { lcovutil::report_parallel_error('genhtml', $lcovutil::ERROR_PARALLEL, $childPid, 0, $f, keys(%$children)); } } } my $signal = $childstatus & 0xFF; # non-parallel execution prints stuff to stdout - so we should too print(STDOUT $childLog) if ((0 != $childstatus && $signal != POSIX::SIGKILL && $lcovutil::max_fork_fails != 0) || $lcovutil::verbose >= 0); print(STDERR $childErr); # now undump the data ... my $dStart = Time::HiRes::gettimeofday(); my $dumped = Storable::retrieve($dumpfile) if (-f $dumpfile && 0 == $childstatus); my $dEnd = Time::HiRes::gettimeofday(); printf(" %d restore %d: %0.3fs %s (parallel %d)\n", $jobId, $childPid, $dEnd - $dStart, DateTime->now(), $self->[CURRENT_PARALLEL]) if $main::debugScheduler; if (defined($dumped)) { eval { my ($jobData, $countData, $update, $otherData) = @$dumped; die("error processing child") if (scalar(@$jobData) != scalar(@$jobs)); $SourceFile::annotatedFiles += $countData->[0]; $SourceFile::totalFiles += $countData->[1]; lcovutil::update_state(@$update); $lcovutil::profileData{startDelay}{$jobId} = $otherData->[0] - $start; $lcovutil::profileData{mergeDelay}{$jobId} = $mergeStart - $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)); $CoverageCriteria::coverageCriteriaStatus = ($v != 0 || 0 != scalar(@{$criteria->[2]})) ? $v : 0; } } # foreach job }; if ($@) { $childstatus = 1 << 8 unless $childstatus; print(STDERR $@); lcovutil::report_parallel_error('genhtml', $lcovutil::ERROR_PARALLEL, $childPid, $childstatus, "unable to deserialize $dumpfile: $@", keys(%$children)); } } if (!-f $dumpfile) { $self->_report_fail_and_reschedule($jobId, $jobs, $childPid, "serialized data $dumpfile not found"); } elsif (!defined($dumped) || $childstatus != 0) { my $signal = $childstatus & 0xFF; if (POSIX::SIGKILL == $signal) { $self->_report_fail_and_reschedule($jobId, $jobs, $childPid, "killed by OS - possibly due to out-of-memory"); } else { #print("stdout: $childLog\nstderr: $childErr\n"); my $msg = "compute job $jobId"; $msg .= ": unable to deserialize $dumpfile" unless defined($dumped); lcovutil::report_parallel_error('genhtml', $lcovutil::ERROR_CHILD, $childPid, $childstatus, $msg, keys(%$children)); } } my $end = Time::HiRes::gettimeofday(); $lcovutil::profileData{segment}{$jobId} = $end - $start; $lcovutil::profileData{merge_segment}{$jobId} = $end - $mergeStart; unlink $dumpfile if -f $dumpfile; } package main; # Global variables & initialization lcovutil::save_cmd_line(\@ARGV, "$FindBin::RealBin"); # 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; # 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; 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 = 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 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 = 8; # Number of spaces to use in place of 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 = "html"; # Extension for generated 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'); our @fileview_prefixes = (""); our @funcview_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 our $deprecated_highlight; # ignored former option our $cwd = cwd(); # Current working directory # 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); 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_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, "genhtml_charset" => \$charset, "genhtml_desc_html" => \$rc_desc_html, 'merge_function_aliases' => \$merge_function_aliases, 'suppress_function_aliases' => \$suppress_function_aliases, "genhtml_missed" => \$opt_missed, "genhtml_dark_mode" => \$dark_mode, "genhtml_hierarchical" => \$hierarchical, "genhtml_flat_view" => \$flat, "genhtml_show_havigation" => \$show_tla, "genhtml_show_noncode_owners" => \$show_nonCodeOwners, "genhtml_show_function_proportion" => \$show_functionProportions, 'genhtml_show_owner_table' => \$show_ownerBins, "genhtml_demangle_cpp" => \@lcovutil::cpp_demangle, "genhtml_demangle_cpp_tool" => \$lcovutil::cpp_demangle_tool, "genhtml_demangle_cpp_params" => \$lcovutil::cpp_demangle_params, 'genhtml_annotate_script' => \@rc_annotate_script, 'genhtml_annotate_tooltip' => \$SourceFile::annotateTooltip, "select_script" => \@rc_select_script, '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, '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" => \@selectCallbackScript, "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, "highlight" => \$deprecated_highlight, "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" => \$sort, "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,); # 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); } $merge_function_aliases = 1 if ($suppress_function_aliases || defined($lcovutil::cov_filter[$lcovutil::FILTER_FUNCTION_ALIAS])); lcovutil::ignorable_error($lcovutil::ERROR_DEPRECATED, "option '--highlight' has been removed.") if ($deprecated_highlight); $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)); $frames = undef unless (defined($frames) && $frames); foreach my $rc ([\@datebins, \@rc_date_bins], [\@SummaryInfo::ageGroupHeader, \@rc_date_labels], [\@SourceFile::annotateScript, \@rc_annotate_script], [\@selectCallbackScript, \@rc_select_script] ) { @{$rc->[0]} = @{$rc->[1]} unless (@{$rc->[0]}); } foreach my $cb ([\$SourceFile::annotateCallback, \@SourceFile::annotateScript], [\$selectCallback, \@selectCallbackScript]) { lcovutil::configure_callback($cb->[0], @{$cb->[1]}) if scalar(@{$cb->[1]}); } 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 = 0 if ($no_sort); die( "unsupported use of mutually exclusive '--flat' and '--hierachical' 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_char, join($lcovutil::split_char, @datebins)); @datebins = sort(keys %uniqify); } SummaryInfo::setAgeGroups(@datebins); if (defined($show_ownerBins)) { @truncateOwnerTableLevels = split($lcovutil::split_char, 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 integraion" ) if (0 != scalar(@datebins)); } 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; } foreach my $tla (@SummaryInfo::tlaPriorityOrder) { # 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_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); } } # Make sure tab_size is within valid range if ($tab_size < 1) { print(STDERR "ERROR: invalid number of spaces specified: $tab_size!\n"); exit(1); } # 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); if ($sort) { 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(@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); } } 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(); # 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 foreach my $d (['baseline_', @base_filenames], ['', $diff_filename], ['current_', @info_filenames] ) { my $prefix = shift @$d; $prefix = '' unless @base_filenames; foreach my $from (@$d) { next unless defined($from); my $to = File::Spec->catfile($output_directory, $prefix . File::Basename::basename($from)); File::Copy::copy($from, $to) unless -f $to; } } } # 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{overall} = $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()); exit($exit_status); # # print_usage(handle) # # Print usage information. # sub print_usage(*) { local *HANDLE = $_[0]; print(HANDLE <files())); my $width = length("source files"); my $indent = ' '; for my $type (@types) { my $name = SummaryInfo::type2str($type); my $plural = "ch" eq substr($name, -2, 2) ? "es" : "s"; my $label = "$name$plural"; my $fill = '.' x ($width - length($label)); info(-1, " $name$plural$fill: %s\n", get_overall_line($summary->get("found", $type), $summary->get("hit", $type), $name)); if ($main::show_tla) { for my $tla (@SummaryInfo::tlaPriorityOrder) { my $v = $summary->get($tla, $type); next if $v == 0; my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacySrcLabel{$tla} : $tla; my $f = '.' x ($width - (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 # 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())); $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 dont 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+$//; $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(); } 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::br_coverage, $lcovutil::mcdc_coverage, $genhtml->top()); return $genhtml->top(); } # # html_create(handle, filename) # sub html_create($$) { my $handle = $_[0]; my $filename = File::Spec->catfile($output_directory, $_[1]); $filename = lc($filename) if $lcovutil::case_insensitive; if ($html_gzip) { open($handle, "|-", "gzip -c > $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 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 ) { my $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()); my @dirPageCalls; foreach my $sort_type (@main::fileview_sortlist) { 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()); } 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] 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 number of un-exercised lines $filename = File::Spec->catfile($rel_dir, "$base_name.func-l.$html_ext"); } elsif ($sort_type == $main::SORT_MISSING_MCDC) { # by number of un-exercised MC/DC expressions return unless %{$fileCovInfo->[3]}; $filename = File::Spec->catfile($rel_dir, "$base_name.func-m.$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 declaration line number $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 [4, 'name', $sort_type,], File::Spec->catfile($trunc_dir, $base_name), File::Spec->catfile($rel_dir, $base_name), $summary, undef, $funcdata->[0]); 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"); } # # process_file(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)' : '') . "\n"); my $base_name = basename($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()); $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); # 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 = $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(); $lcovutil::profileData{categorize}{$filename} = $then - $now; 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; info($r . "\n"); 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 $fineNotInBaseline 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; 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] ) { 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 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; } } 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', [2, 'name', 0], File::Spec->catfile($trunc_dir, $base_name), File::Spec->catfile($rel_dir, $base_name), $fileSummary, $srcfile, $differentialFunctionMap); @source = write_source(*HTML_HANDLE, $srcfile, $sumcount, $checkdata, $fileCovInfo, $funcdata, $sumbrcount, $mcdc_summary); 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; if ($main::show_functionProportions) { while (my ($name, $funcEntry) = each(%$differentialFunctionMap)) { 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]; } 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]; } } 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]; } } } } if (%$differentialFunctionMap) { foreach my $sort_type (@funcview_sortlist) { write_function_page([$differentialFunctionMap, \%lineCov, \%branchCov, \%mcdcCov, ], $base_dir, $rel_dir, $trunc_dir, $base_name, $test_title, $sumcount, $funcdata, $testfncdata, $sumbrcount, $testbrdata, $mcdc_summary, $testcase_mcdc, $sort_type, $fileSummary); } } } # Additional files are needed in case of frame output if ($frames) { # Create overview png file my $simplified = defined($main::show_simplifiedColors) && $main::show_simplifiedColors; my $png = File::Spec->catfile($rel_dir, "$base_name.gcov.png"); $png = lc($png) if $lcovutil::case_insensitive; 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, "$base_name.gcov.frameset.$html_ext" )); write_frameset(*HTML_HANDLE, $base_dir, $base_name, $pagetitle); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); # Write overview frame html_create(*HTML_HANDLE, File::Spec->catfile( $rel_dir, "$base_name.gcov.overview.$html_ext" )); write_overview(*HTML_HANDLE, $base_dir, $base_name, $pagetitle, scalar(@source)); close(*HTML_HANDLE) or die("unable to close HTML handle: $!\n"); } } my $to = Time::HiRes::gettimeofday(); $lcovutil::profileData{html}{$filename} = $to - $from; 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 my $current = 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 if (!$_[0]) { return (""); } # Count number of /s in path my $index = ($_[0] =~ s/$lcovutil::dirseparator/$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 $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 if ($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. # sub escape_html($) { my $string = $_[0]; if (!$string) { return ""; } $string =~ s/&/&/g; # & -> & $string =~ s/ < $string =~ s/>/>/g; # > -> > $string =~ s/\"/"/g; # " -> " while ($string =~ /^([^\t]*)(\t)/) { my $replacement = " " x ($tab_size - (length($1) % $tab_size)); $string =~ s/^([^\t]*)(\t)/$1$replacement/; } $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 (!$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) { $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 system("cp", $css_filename, $f) and die("cannot copy file $css_filename: $!\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 ow 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; } /* Source code view: source code format */ pre.source { font-family: monospace; white-space: pre; margin-top: 2px; } /* 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 ; foreach my $tla (@SummaryInfo::tlaPriorityOrder) { my $title = $SummaryInfo::tlaToTitle{$tla}; my $color = $lcovutil::tlaColor{$tla}; 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; } 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; } 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; } # # 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 $html_code =~ s/^\t//gm; 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 = 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_link; 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 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 = '$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) { 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 && grep(/^$key$/, ("UNC", "LBC", "UIC"))); 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) && !grep(/^$key$/, ('DUB', 'DCB')) && grep(/^$key$/, @SummaryInfo::tlaPriorityOrder)) { 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::type2str($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; } 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; } 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; } 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 write_html($handle, < END_OF_HTML # Test data foreach my $entry (@entries) { my ($found, $hit, $covtype, $callback) = @{$entry}; my $rate = rate($hit, $found, " %"); if (SummaryInfo::LINE_DATA == $covtype && defined($showBinDetail)) { 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. # 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)
#
# 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($)
{
    my $data = shift;
    return () unless defined($data);

    my @blocks;

    if ('BranchEntry' eq ref($data)) {

        my $block = [];
        my $last_block_num;

        # Group branches
        foreach my $block_num (sort $data->blocks()) {
            my $blockData = $data->getBlock($block_num);
            my $branch    = 0;
            foreach my $br (@$blockData) {

                if (defined($last_block_num) && $block_num != $last_block_num) {
                    push(@blocks, $block);
                    $block = [];
                }
                my $br = [$block_num, $branch, $br, 3, 0, 0];
                push(@{$block}, $br);
                $last_block_num = $block_num;
                ++$branch;
            }
        }
        push(@blocks, $block) if (scalar(@{$block}) > 0);
    } else {
        die('unexpected ' . ref($data)) unless 'MCDC_Block' eq ref($data);
        # Group MC/DC groups
        foreach my $groupSize (sort keys %{$data->groups()}) {
            my $exprs = $data->groups()->{$groupSize};
            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 =
                        [$groupSize, $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;
        }
        # Split the block into several lines
        foreach my $branch (@{$block}) {
            if ($line_len + $branch->[$BR_LEN] >= $field_width) {
                # 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     = [];
            }
            push(@{$line}, $branch);
            $line_len += $branch->[$BR_LEN];
        }
    }
    push(@lines, $line);

    return @lines;
}

#
# 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();
    if (defined($main::show_tla)) {
        my $lineNo   = $cbdata->lineNo();
        my $lineData = $cbdata->lineData()->line($lineNo);
        $differentialBranch = $lineData->differential_branch()
            if defined($lineData);
    }
    # build the 'blocks' array from differential data if we have it..
    my @blocks = get_block_list(
                  defined($differentialBranch) ? $differentialBranch : $brdata);

    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;
            my $tla;
            my $base_count;
            if ('ARRAY' ne ref($br)) {
                # vanilla case - no differential coverage info
                die("differential branch coverage but no TLA")
                    if defined($differentialBranch);
                if ($br->data() eq '-') {
                    $class = "tlaUNC";
                } elsif ($br->data() == 0) {
                    $class = "tlaUNC";
                } else {
                    $class = 'tlaGBC';
                }
                $prefix = '';
            } else {
                die("differential branch coverage but no TLA")
                    unless defined($differentialBranch);
                $tla        = $br->[1];
                $base_count = $br->[2]->[0];
                $br         = $br->[0];
                $class      = "tla$tla";
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$tla} :
                    $tla;
                $prefix = $label . ": ";
            }
            my ($char, $title);

            my $br_name =
                defined($br->expr()) ? '"' . $br->expr() . '"' : $br_num;
            my $taken = $br->data();
            if ($taken eq '-') {
                $char  = "#";
                $title = "${prefix}Branch $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}Branch $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}Branch $br_name was taken $taken time" .
                    (($taken > 1) ? "s" : "");
            }
            $title = escape_html($title) if defined($br->expr());
            $current .= "[" if ($open);

            if (!$main::no_sourceview &&
                defined($differentialBranch)) {
                my $href;
                if (exists($tlaLinks{$tla})) {
                    $href = $tlaLinks{$tla};
                } else {
                    my $line = $differentialBranch->line();
                    my $next = $fileDetail->nextBranchTlaGroup($tla, $line);
                    $href =
                        "$char";
                    $tlaLinks{$tla} = $href;
                }
                $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();
    if (defined($main::show_tla)) {
        my $lineNo   = $cbdata->lineNo();
        my $lineData = $cbdata->lineData()->line($lineNo);
        $differentialMcdc = $lineData->differential_mcdc()
            if defined($lineData);
    }
    # build the 'blocks' array from differential data if we have it..
    my @blocks = get_block_list(
                   defined($differentialMcdc) ? $differentialMcdc : $mcdc_data);

    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';

            my $count = $expr->count($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 = "tla$tla";
                my $label =
                    $main::use_legacyLabels ?
                    $SummaryInfo::tlaToLegacySrcLabel{$tla} :
                    $tla;
                $prefix = $label . ": ";
            }
            my ($char, $title);

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

            if (!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 &&
                defined($differentialMcdc)) {
                my $href;
                if (exists($tlaLinks{$tla})) {
                    $href = $tlaLinks{$tla};
                } else {
                    my $line = $differentialMcdc->line();
                    my $next = $fileDetail->nextMcdcTlaGroup($tla, $line);
                    $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 = get_branch_html($brdata, $cbdata) if ($showBranches);

    my @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
        info(2,
             "    $bucket: line=$line " .
                 (defined($count) ? "count= $count " : "") . ' curr=' .
                 (defined($curr_count) ? $curr_count : '-') . ' base=' .
                 (defined($base_count) ? $base_count : '-') . "\n");
    }
    $result .= ":" . $source;

    my $tooltip =
        @SourceFile::annotateScript ? $SourceFile::annotateTooltip : '';
    if ($tooltip ne '') {
        my $lineData = $sourceDetail->line($line);
        my $commitId = $lineData->commit();
        if (defined($commitId) &&
            $commitId ne 'NONE') {
            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', $line],
            ) {
                $tooltip =~ s/$p->[0]/$p->[1]/g;
            }
            $tooltip = " title=\"$tooltip\"";
        } else {
            $tooltip = '';
        }
    }
    # 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) ||
                    ($bucket ne 'EUB' &&
                        $bucket ne 'ECB'))
            ) {
                # 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) &&
                        $bucket ne "EUB" &&
                        $bucket ne "ECB");

                #$html .= "";
                if ("" ne $source_format) {
                    OWNER: {
                        if (!defined($src_owner) || !defined($src_age)) {
                            # maybe this should be a different error type?
                            lcovutil::ignorable_eror(
                                      $lcovutil::ERROR_UNMAPPED_LINE,
                                      "undefined owner/age for $bucket $line " .
                                          $sourceDetail->path());
                            last OWNER;
                        }
                        # add a 'title="tooltip"' popup - to give owner, date, etc
                        my $title = "span title=\"$src_owner $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.
            #  - is 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 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 {
                $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]->lineNo('base');
        my $last  = $deletedLines->[-1]->lineNo('base');
        die("invalid deleted lines array") unless $first <= $last;
        $lineNumTitle = ' title="'
            .
            (($first != $last) ? "removed lines [$first:$last]" :
                 "removed line $first") .
            ' from baseline version"';
        $lineNumTag = 'lineNumWithDelete';
    }
    my $lineNumSpan = "";

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

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

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

    # '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 wheatever
        #   (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 : ";
    }

    $html .= escape_html($source);
    $html .= "" if ($source_format);
    $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) =~ s/(<\/span>|)//g;
            $br .= ' ' x ($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) # # 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) # # 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); } } # ************************************************************* my $f = $lcovutil::case_insensitive ? lc($_[2]) : $_[2]; write_html($_[0], <
Top

Overview
END_OF_HTML # ************************************************************* } sub max($$) { my ($a, $b) = @_; return $a if ($a > $b); return $b; } sub buildDateSummaryTable($$$$$$$$$) { my ($summary, $covType, $covCountCallback, $fileDetail, $nextLocationCallback, $title, $detailLink, $numRows, $activeTlaList) = @_; $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); if ($covType == SummaryInfo::LINE_DATA) { $lim_med = $ln_med_limit; $lim_high = $ln_hi_limit; } elsif ($covType == SummaryInfo::BRANCH_DATA) { $lim_med = $br_med_limit; $lim_high = $br_hi_limit; } elsif ($covType == SummaryInfo::MCDC_DATA) { $lim_med = $mcdc_med_limit; $lim_high = $mcdc_hi_limit; } elsif ($covType == SummaryInfo::FUNCTION_DATA) { $lim_med = $fn_med_limit; $lim_high = $fn_hi_limit; } else { die("unexpected cover type $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 && grep(/^$tla$/, ("UNC", "LBC", "UIC")) ? "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) && !grep(/^$tla$/, ('DUB', 'DCB'))) { # 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) 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::type2str($covType) . " in “$dateBin” bin\""; $value = "$value"; } 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) = @_; $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); if ($covType == SummaryInfo::LINE_DATA) { $lim_med = $ln_med_limit; $lim_high = $ln_hi_limit; } elsif ($covType eq SummaryInfo::BRANCH_DATA) { $lim_med = $br_med_limit; $lim_high = $br_hi_limit; } elsif ($covType eq SummaryInfo::MCDC_DATA) { $lim_med = $mcdc_med_limit; $lim_high = $mcdc_hi_limit; } elsif ($covType eq SummaryInfo::FUNCTION_DATA) { $lim_med = $fn_med_limit; $lim_high = $fn_hi_limit; } else { die("unexpected cover type $covType"); } my $first = 1; # owners are sorted from most uncovered lines to least foreach my $od (@$ownerList) { my ($name, $lineData, $branchData) = @$od; my $d = ($covType == SummaryInfo::LINE_DATA) ? $lineData : $branchData; my ($missed, $found) = @$d; # 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 && grep(/^$tla$/, ("UNC", "LBC", "UIC")) ? "tla$tla" : "ownerTla"; if ("0" eq $value) { $value = ""; } elsif (!$main::no_sourceview && defined($fileDetail) && defined($nextLocationCallback)) { my $firstAppearance = &$nextLocationCallback($fileDetail, $name, $tla); defined($firstAppearance) 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::type2str($covType) . " in “$name” bin\""; $value = "$value"; } 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 && grep(/^$tla$/, ("UNC", "LBC", "UIC")) ? "tla$tla" : "headerCovTableEntry"; if ("0" eq $value) { # suppress zeros - make table less busy/easier to read $value = ""; } elsif (!$main::no_sourceview && defined($fileDetail) && !($tla eq "DCB" || $tla eq "DUB")) { # deleted lines don't appear.. my $firstAppearance = &$nextLocationCallback($fileDetail, $tla); defined($firstAppearance) 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::type2str($covType) . "\""; $value = "$value"; } 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($$$$$) { my ($path, $key, $bin_type, $isFile, $isAbsolute) = @_; $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; 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)) # ctrl = (type, primary_key, sort_type, bin_type) # # Write a complete standard page header. TYPE may be (0, 1, 2, 3, 4) # corresponding to (directory view header, file view header, source view # header, test case description header, function view header) # # 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 correspondign 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; my ($callback_type, $ctrl, $trunc_name, $rel_filename, $summary, $fileDetail, $differentialFunctionMap) = @_; 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); $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)) ); my $html_path = build_html_path($trunc_name, $key, $bin_type, $summary->type() eq 'file', $isAbsolutePath); # 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) { # 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 - "; if ($main::flat) { # for flat view, point only to the top-levevl and show path # to this file. No other links $view .= File::Spec->catfile($esc_dir_name, $esc_base_name); } else { if ($main::hierarchical) { $html_path =~ s/$esc_dir_name"; } $view .= " - $esc_base_name"; } # Add function suffix if ($lcovutil::func_coverage && defined($differentialFunctionMap) && %$differentialFunctionMap) { $view .= ""; if ($type == $HDR_SOURCE) { my $suffix = $sort ? '-c' : ''; $view .= " (source / functions)"; } elsif ($type == $HDR_FUNC) { $view .= " (source / functions)"; } $view .= ""; } } 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)) { # 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(); # 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 && (defined($main::show_ownerBins) || @SourceFile::annotateScript) ) { # we are going to have 3 versions of 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)) { # 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 } 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; foreach my $covtype (@visit) { if ($main::show_zeroTlaColumns) { $activeTlaColsForType{$covtype} = \@SummaryInfo::tlaPriorityOrder; } else { my @found; for my $tla (@SummaryInfo::tlaPriorityOrder) { push(@found, $tla) if (0 != $summary->get($tla, $covtype)); } $activeTlaColsForType{$covtype} = \@found; } } foreach my $tla (@SummaryInfo::tlaPriorityOrder) { # check to see that there are non-zero entries 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::tlaPriorityOrder : \@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, $br_med_limit, $br_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::nextBranchTlaGroup, $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 = "index-date.$html_ext" if ($type != $HDR_SOURCE && $type != $HDR_FUNC); 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); } } # 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 = "index-owner.$html_ext" if ($type != $HDR_SOURCE && $type != $HDR_FUNC); 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::nextBranchInOwnerBin, "MC/DC coverage ownership bins", $ownerBinDetailPage, scalar(@dataRow), $tlaSummaryTypes); push(@row_right, @$mcdc_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 = "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_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 drectory) # 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(); my $includeFunctionColumns = $lcovutil::func_coverage && 0 != $dirSummary->function_found() && 'owner' ne $primary_key; # 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 && 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 @head_columns; my @lineCovCols = (["Rate", 2], "Total"); my @mcdcCovCols = ("Rate", "Total"); my @branchCovCols = ("Rate", "Total"); my @functionCovCols = ("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); } 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; 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} ]); } } } push(@head_columns, [$line_code, $#lineCovCols + 2, \@lineCovCols]); push(@head_columns, [$mcdc_code, $#mcdcCovCols + 1, \@mcdcCovCols]) if $hasMcdcData; push(@head_columns, [$br_code, $#branchCovCols + 1, \@branchCovCols]) if $hasBranchData; push(@head_columns, [$func_code, $#functionCovCols + 1, \@functionCovCols]) if $includeFunctionColumns; 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 @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; my %owners; foreach my $d ([1, SummaryInfo::LINE_DATA, 0], [$lcovutil::mcdc_coverage, SummaryInfo::MCDC_DATA, 1], [$lcovutil::br_coverage, SummaryInfo::BRANCH_DATA, 2], ) { my ($enable, $dataType, $idx) = @$d; next unless $enable; foreach my $owner ($dirSummary->owners($all, $dataType)) { # (line count, branch count, mcdc count, function count) $owners{$owner} = [[0, 0], [0, 0], [0, 0], [0, 0]] unless exists($owners{$owner}); $owners{$owner}->[$idx] = [ $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 ([1, $SORT_LINE, 0], [$lcovutil::mcdc_coverage, $SORT_MCDC, 1], [$lcovutil::br_coverage, $SORT_BRANCH, 2], ) { my ($enable, $s, $idx) = @$d; if ($sort_type eq $s) { # sort by number of missed lines/branches/MCDC expressions @sorted = sort({ my $la = $owners{$a}->[$idx]; my $lb = $owners{$b}->[$idx]; ($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($ownderTableElements) && # $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); my $lines = $dirSummary->lineCovCount('found', 'age', $ageval); if (0 != $lines || ($lcovutil::br_coverage && 0 != $dirSummary->branchCovCount('found', 'age', $ageval)) || ($lcovutil::mcdc_coverage && 0 != $dirSummary->mcdcCovCount('found', 'age', $ageval)) || ($lcovutil::func_coverage && 0 != $dirSummary->functionCovCount('found', 'age', $ageval)) ) { push(@tableRows, FileOrDirectoryDateCallback->new($bin, $dirSummary)); } } } else { die("unsupported primary key '$primary_key'"); } 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); my $lineCb = $primaryCb->dateDetailCallback($ageval, SummaryInfo::LINE_DATA); my $lineTotal = $lineCb->get('found'); my $hit = $lineCb->get('hit'); my $lineMissed = $lineTotal - $hit; my $branchCb = $primaryCb->dateDetailCallback($ageval, SummaryInfo::BRANCH_DATA); my $branchTotal = $lcovutil::br_coverage ? $branchCb->get('found') : 0; $hit = $lcovutil::br_coverage ? $branchCb->get('hit') : 0; my $branchMissed = $branchTotal - $hit; my $mcdcCb = $primaryCb->dateDetailCallback($ageval, SummaryInfo::MCDC_DATA); my $mcdcTotal = $lcovutil::mcdc_coverage ? $mcdcCb->get('found') : 0; $hit = $lcovutil::mcdc_coverage ? $mcdcCb->get('hit') : 0; my $mcdcMissed = $mcdcTotal - $hit; my $functionCb = $primaryCb->dateDetailCallback($ageval, SummaryInfo::FUNCTION_DATA); my $functionTotal = $lcovutil::func_coverage ? $functionCb->get('found') : 0; $hit = $lcovutil::func_coverage ? $functionCb->get('hit') : 0; my $functionMissed = $functionTotal - $hit; next if 0 == $lineTotal && 0 == $branchTotal && 0 == $mcdcTotal && 0 == $functionTotal; push(@secondaryRows, [$SummaryInfo::ageGroupHeader[$bin], [$lineMissed, $lineTotal, $lineCb], [$mcdcMissed, $mcdcTotal, $mcdcCb], [$branchMissed, $branchTotal, $branchCb], [$functionMissed, $functionTotal, $functionCb] ]); } } } } # if showBinDetail elsif ($primary_key ne 'name') { ($skippedSecondaryRows, @secondaryRows) = @{$primaryCb->findFileList($all)}; } my ($found, $hit, $fn_found, $fn_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 ]); } 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; } 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 { my $sortElem; if ($sort_type == $SORT_LINE) { $sortElem = 1; } elsif ($sort_type == $SORT_BRANCH) { $sortElem = 2; } elsif ($sort_type == $SORT_MCDC) { $sortElem = 3; } elsif ($sort_type == $SORT_FUNC) { $sortElem = 4; } @secondaryRows = sort({ my $ca = $a->[$sortElem]; my $cb = $b->[$sortElem]; # sort based on 'missed' $cb->[0] <=> $ca->[0]; } @secondaryRows); } foreach my $secondary (@secondaryRows) { my ($name, $line, $branch, $mcdc, $func) = @$secondary; my ($lineMissed, $lineTotal, $lineCb) = @$line; 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 ]); if ($hasMcdcData) { my ($mcdcMissed, $mcdcTotal, $mcdcCallback) = @$mcdc; # need to compute the totals... push(@ownerColData, [$mcdcTotal, $mcdcTotal - $mcdcMissed, $mcdc_med_limit, $mcdc_hi_limit, 0, $mcdcCallback, SummaryInfo::MCDC_DATA ]); } if ($hasBranchData) { my ($branchMissed, $branchTotal, $brCallback) = @$branch; # need to compute the totals... push(@ownerColData, [$branchTotal, $branchTotal - $branchMissed, $br_med_limit, $br_hi_limit, 0, $brCallback, SummaryInfo::BRANCH_DATA ]); } if ($includeFunctionColumns) { my ($funcMissed, $funcTotal, $funcCallback) = @$func; # need to compute the totals... push(@ownerColData, [$funcTotal, $funcTotal - $funcMissed, $fn_med_limit, $fn_hi_limit, 0, $funcCallback, SummaryInfo::FUNCTION_DATA ]); } 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); 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; } foreach my $testname (keys(%affecting_tests)) { ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, $mcdc_found, $mcdc_hit) = @{$affecting_tests{$testname}}; 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"; } 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.' ], [ $lcovutil::func_coverage && !$includeFunctionColumns, "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 $testfnccount = $testfncdata->value($testname); my $testbrcount = $testbrdata->value($testname); my $testmcdccount = $testmcdcdata->value($testname); # Calculate sum my ($found, $hit) = $testcount->get_found_and_hit(); # 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 ] if ($hit > 0); } return (\%result); } # # write_source(filehandle, source_filename, count_data, checksum_data, # converted_data, func_data, sumbrcount, mcdc_summary) # # 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) = @_; my @result; # 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; my $cbdata = PrintCallback->new($srcfile, $fileCovInfo); my ($region, $empty); if ($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; } 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/; push(@result, $l); 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())) ); push(@result, write_source_line(HTML_HANDLE, $srcline, $count_data->value($line_number), $showBranches, $sumbrcount->value($line_number), $showMcdc, $mcdc_summary->value($line_number), $cbdata)); } 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); 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 && $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 && $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 && $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 && $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); } else { die("unexpected sort $col") unless ($col eq 'missed_branch'); if ($sort && $sort_type != $SORT_MISSING_BRANCH) { $link = "$name.func-b.$html_ext"; } return "Branches" . get_sort_code($link, "unexercised branches in function", $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) or by the associated call count (type 1). # sub funcview_get_sorted($$$) { my ($funcData, $type, $merged) = @_; my ($differential_func, $lineFuncData, $branchFuncData, $mcdcFuncData) = @$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_MISSING_LINE) { @rtn = sort({ my $linea = exists($lineFuncData->{$a}) ? $lineFuncData->{$a} : undef; my $lineb = exists($lineFuncData->{$b}) ? $lineFuncData->{$b} : undef; my $missedA = defined($linea) ? $linea->[0] - $linea->[1] : -1; my $missedB = defined($lineb) ? $lineb->[0] - $lineb->[1] : -1; # highest to lowest 'missed lines', then by # function name if count matches $missedB <=> $missedA or $a cmp $b } @rtn); } elsif ($type == $SORT_MISSING_BRANCH) { @rtn = sort({ my $bra = exists($branchFuncData->{$a}) ? $branchFuncData->{$a} : undef; my $brb = exists($branchFuncData->{$b}) ? $branchFuncData->{$b} : undef; my $missedA = defined($bra) ? $bra->[0] - $bra->[1] : -1; my $missedB = defined($brb) ? $brb->[0] - $brb->[1] : -1; # highest to lowest 'missed branches', then by # function name if count matches $missedB <=> $missedA or $a cmp $b } @rtn); } elsif ($type == $SORT_MISSING_MCDC) { @rtn = sort({ my $bra = exists($mcdcFuncData->{$a}) ? $mcdcFuncData->{$a} : undef; my $brb = exists($mcdcFuncData->{$b}) ? $mcdcFuncData->{$b} : undef; my $missedA = defined($bra) ? $bra->[0] - $bra->[1] : -1; my $missedB = defined($brb) ? $brb->[0] - $brb->[1] : -1; # highest to lowest 'missed expressions', then by # function name if count matches $missedB <=> $missedA or $a cmp $b } @rtn); } elsif (!defined($main::no_sort)) { # sort alphabetically by function name @rtn = sort(@rtn); } return @rtn; } # # write_function_table(filehandle, funcData, source_file, sumcount, funcdata, # testfncdata, sumbrcount, testbrdata, mcdc_summary, testase_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) = @$funcData; # 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 $showTlas = $main::show_tla && 0 != scalar(keys %$differentialMap); my $tlaRow = ""; my $lineProportionRow = ''; my $branchProportionRow = ''; my $mcdcProportionRow = ''; my $countWidth = 20; if ($showTlas) { my $label = $main::use_legacyLabels ? 'Hit?' : 'TLA'; $tlaRow = "$label"; $countWidth = 10; } 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; } die("no functions in file") unless (%$differentialMap); write_html(*HTML_HANDLE, < $tlaRow $lineProportionRow $branchProportionRow $mcdcProportionRow END_OF_HTML foreach my $func ( funcview_get_sorted($funcData, $type, $main::merge_function_aliases)) { my $funcEntry = $differentialMap->{$func}; my ($count, $tla) = @{$funcEntry->hit()}; next if grep(/^$tla$/, ('DUB', 'DCB')); # don't display deleted functions my $startline = $funcEntry->line() - $func_offset; my $name = $func; my $countstyle; # Escape special characters $name = escape_html($name); if ($startline < 1) { $startline = 1; } if ($count == 0) { $countstyle = "coverFnLo"; } else { $countstyle = "coverFnHi"; } my $tlaRow = ""; my $lineProportionRow = ''; my $branchProportionRow = ''; my $mcdcProportionRow = ''; if ($showTlas) { my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} : $tla; $tlaRow = ""; } if ($main::show_functionProportions) { if (exists($funcLineCovMap->{$func})) { my ($found, $hit) = @{$funcLineCovMap->{$func}}; # colorize based on hit proportion my $style = $rate_name[classify_rate($found, $hit, $ln_med_limit, $ln_hi_limit)]; my $rate = rate($hit, $found, " %"); $lineProportionRow = ""; } else { $lineProportionRow = "" if %$funcLineCovMap; } if ($lcovutil::br_coverage && %$funcBranchCovMap) { if (exists($funcBranchCovMap->{$func})) { my ($found, $hit) = @{$funcBranchCovMap->{$func}}; # colorize based on hit proportion my $style = $rate_name[ classify_rate($found, $hit, $br_med_limit, $br_hi_limit) ]; my $rate = rate($hit, $found, " %"); $branchProportionRow = ""; } else { $branchProportionRow = ""; } } if ($lcovutil::mcdc_coverage && %$funcMcdcCovMap) { if (exists($funcMcdcCovMap->{$func})) { my ($found, $hit) = @{$funcMcdcCovMap->{$func}}; # colorize based on hit proportion my $style = $rate_name[ classify_rate($found, $hit, $mcdc_med_limit, $mcdc_hi_limit) ]; my $rate = rate($hit, $found, " %"); $mcdcProportionRow = ""; } else { $mcdcProportionRow = ""; } } } write_html(*HTML_HANDLE, < $tlaRow $lineProportionRow $branchProportionRow $mcdcProportionRow END_OF_HTML if ((!defined($main::suppress_function_aliases) || 0 == $main::suppress_function_aliases) && $funcEntry->numAliases() > 1 ) { my $aliases = $funcEntry->aliases(); my @aliasList = keys(%$aliases); if ($main::show_functionProportions) { $lineProportionRow = "" if %$funcLineCovMap; $branchProportionRow = $lineProportionRow if $lcovutil::br_coverage && %$funcBranchCovMap; $mcdcProportionRow = $mcdcProportionRow if $lcovutil::mcdc_coverage && %$funcMcdcCovMap; } if (0 != $type) { @aliasList = sort({ my $da = $aliases->{$a}; my $db = $aliases->{$b}; $da->[0] <=> $db->[0] or $a cmp $b } @aliasList); } else { @aliasList = sort(@aliasList); } foreach my $alias (@aliasList) { my ($hit, $tla) = @{$aliases->{$alias}}; # don't display deleted functions next if grep(/^$tla$/, ('DUB', 'DCB')); # my $style = "coverFnAlias" . ($hit == 0 ? "Lo" : "Hi"); $tlaRow = ""; if ($showTlas) { my $label = $main::use_legacyLabels ? $SummaryInfo::tlaToLegacy{$tla} : $tla; $tlaRow = ""; } # Escape special characters $alias = escape_html($alias); write_html(*HTML_HANDLE, < $tlaRow $lineProportionRow $branchProportionRow $mcdcProportionRow 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 (!defined($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\E$lcovutil::dirseparator(.*)$/) { 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); foreach my $item (@opt_dir_prefix) { if ($item =~ /$lcovutil::split_char/) { # Split and add comma-separated parameters push(@dir_prefix, split($lcovutil::split_char, $item)); } else { # Add single parameter push(@dir_prefix, $item); } } }

$func_code$count_code
$label$rate ($hit / $found)$rate ($hit / $found)$rate ($hit / $found)$name$count
$label$alias$hit