diff --git a/utils/lcov/genhtml b/utils/lcov/genhtml index 9502a27c1..d74063a4f 100755 --- a/utils/lcov/genhtml +++ b/utils/lcov/genhtml @@ -1,6 +1,6 @@ #!/usr/bin/perl -w # -# Copyright (c) International Business Machines Corp., 2002 +# Copyright (c) International Business Machines Corp., 2002,2010 # # 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 @@ -48,6 +48,20 @@ # 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) # use strict; @@ -57,16 +71,27 @@ use Digest::MD5 qw(md5_base64); # Global constants -our $title = "LTP GCOV extension - code coverage report"; -our $lcov_version = "LTP GCOV extension version 1.4"; +our $title = "LCOV - code coverage report"; +our $lcov_version = 'LCOV version 1.9'; our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; +our $tool_name = basename($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 -our $hi_limit = 50; -our $med_limit = 15; + +# For line coverage/all coverage types if not specified +our $hi_limit = 90; +our $med_limit = 75; + +# For function coverage +our $fn_hi_limit; +our $fn_med_limit; + +# For branch coverage +our $br_hi_limit; +our $br_med_limit; # Width of overview image our $overview_width = 80; @@ -81,31 +106,78 @@ our $nav_resolution = 4; # line in the window. This number specifies that offset in lines. our $nav_offset = 10; -our $overview_title = "directory"; +# 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; + +# 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; + +# Fileview heading types +our $HEAD_NO_DETAIL = 1; +our $HEAD_DETAIL_HIDDEN = 2; +our $HEAD_DETAIL_SHOWN = 3; + +# Offsets for storing branch coverage data in vectors +our $BR_BLOCK = 0; +our $BR_BRANCH = 1; +our $BR_TAKEN = 2; +our $BR_VEC_ENTRIES = 3; +our $BR_VEC_WIDTH = 32; + +# Additional offsets used when converting branch coverage data to HTML +our $BR_LEN = 3; +our $BR_OPEN = 4; +our $BR_CLOSE = 5; + +# Branch data combination types +our $BR_SUB = 0; +our $BR_ADD = 1; # Data related prototypes sub print_usage(*); sub gen_html(); +sub html_create($$); sub process_dir($); sub process_file($$$); sub info(@); sub read_info_file($); sub get_info_entry($); -sub set_info_entry($$$$$;$$); +sub set_info_entry($$$$$$$$$;$$$$$$); sub get_prefix(@); sub shorten_prefix($); sub get_dir_list(@); sub get_relative_base_path($); sub read_testfile($); sub get_date_string(); -sub split_filename($); sub create_sub_dir($); sub subtract_counts($$); sub add_counts($$); sub apply_baseline($$); sub remove_unused_descriptions(); sub get_found_and_hit($); -sub get_affecting_tests($); +sub get_affecting_tests($$$); sub combine_info_files($$); sub merge_checksums($$$); sub combine_info_entries($$$); @@ -113,6 +185,19 @@ sub apply_prefix($$); sub system_no_output($@); sub read_config($); sub apply_config($); +sub get_html_prolog($); +sub get_html_epilog($); +sub write_dir_page($$$$$$$$$$$$$$$$$); +sub classify_rate($$$$); +sub br_taken_add($$); +sub br_taken_sub($$); +sub br_ivec_len($); +sub br_ivec_get($$); +sub br_ivec_push($$$$); +sub combine_brcount($$$); +sub get_br_found_and_hit($); +sub warn_handler($); +sub die_handler($); # HTML related prototypes @@ -120,32 +205,33 @@ sub escape_html($); sub get_bar_graph_code($$$); sub write_png_files(); +sub write_htaccess_file(); sub write_css_file(); -sub write_description_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(*$$$$$$$$$$); sub write_header_prolog(*$); -sub write_header_line(*$$;$$); +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_heading(*$$); -sub write_file_table_detail_entry(*$$$); +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($$$$$$$); sub write_source_prolog(*); -sub write_source_line(*$$$$); +sub write_source_line(*$$$$$$); sub write_source_epilog(*); sub write_frameset(*$$$); @@ -173,12 +259,31 @@ our $help; # Help option flag our $version; # Version option flag our $show_details; # If set, generate detailed directory view our $no_prefix; # If set, do not remove filename prefix +our $func_coverage = 1; # If set, generate function coverage statistics +our $no_func_coverage; # Disable func_coverage +our $br_coverage = 1; # If set, generate branch coverage statistics +our $no_br_coverage; # Disable br_coverage +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 $no_sourceview; # If set, do not create a source code view for each file our $highlight; # If set, highlight lines covered by converted data only +our $legend; # If set, include legend in output our $tab_size = 8; # Number of spaces to use in place of tab our $config; # Configuration file contents +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 $demangle_cpp = 0; # Demangle C++ function names +our @fileview_sortlist; +our @fileview_sortname = ("", "-sort-l", "-sort-f", "-sort-b"); +our @funcview_sortlist; +our @rate_name = ("Lo", "Med", "Hi"); +our @rate_png = ("ruby.png", "amber.png", "emerald.png"); our $cwd = `pwd`; # Current working directory chomp($cwd); @@ -189,6 +294,12 @@ our $tool_dir = dirname($0); # Directory where genhtml tool is installed # Code entry point # +$SIG{__WARN__} = \&warn_handler; +$SIG{__DIE__} = \&die_handler; + +# Prettify version string +$lcov_version =~ s/\$\s*Revision\s*:?\s*(\S+)\s*\$/$1/; + # Add current working directory if $tool_dir is not already an absolute path if (! ($tool_dir =~ /^\/(.*)$/)) { @@ -196,7 +307,7 @@ if (! ($tool_dir =~ /^\/(.*)$/)) } # Read configuration file if available -if (-r $ENV{"HOME"}."/.lcovrc") +if (defined($ENV{"HOME"}) && (-r $ENV{"HOME"}."/.lcovrc")) { $config = read_config($ENV{"HOME"}."/.lcovrc"); } @@ -212,6 +323,7 @@ if ($config) "genhtml_css_file" => \$css_filename, "genhtml_hi_limit" => \$hi_limit, "genhtml_med_limit" => \$med_limit, + "genhtml_line_field_width" => \$line_field_width, "genhtml_overview_width" => \$overview_width, "genhtml_nav_resolution" => \$nav_resolution, "genhtml_nav_offset" => \$nav_offset, @@ -219,30 +331,75 @@ if ($config) "genhtml_no_prefix" => \$no_prefix, "genhtml_no_source" => \$no_sourceview, "genhtml_num_spaces" => \$tab_size, - "genhtml_highlight" => \$highlight}); + "genhtml_highlight" => \$highlight, + "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_function_hi_limit" => \$fn_hi_limit, + "genhtml_function_med_limit" => \$fn_med_limit, + "genhtml_function_coverage" => \$func_coverage, + "genhtml_branch_hi_limit" => \$br_hi_limit, + "genhtml_branch_med_limit" => \$br_med_limit, + "genhtml_branch_coverage" => \$br_coverage, + "genhtml_branch_field_width" => \$br_field_width, + "genhtml_sort" => \$sort, + }); } +# Copy limit values if not specified +$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)); + # Parse command line options -if (!GetOptions("output-directory=s" => \$output_directory, - "title=s" => \$test_title, - "description-file=s" => \$desc_filename, - "keep-descriptions" => \$keep_descriptions, - "css-file=s" => \$css_filename, - "baseline-file=s" => \$base_filename, - "prefix=s" => \$dir_prefix, +if (!GetOptions("output-directory|o=s" => \$output_directory, + "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_filename, + "prefix|p=s" => \$dir_prefix, "num-spaces=i" => \$tab_size, "no-prefix" => \$no_prefix, "no-sourceview" => \$no_sourceview, - "show-details" => \$show_details, - "frames" => \$frames, + "show-details|s" => \$show_details, + "frames|f" => \$frames, "highlight" => \$highlight, - "quiet" => \$quiet, - "help|h" => \$help, - "version" => \$version + "legend" => \$legend, + "quiet|q" => \$quiet, + "help|h|?" => \$help, + "version|v" => \$version, + "html-prolog=s" => \$html_prolog_file, + "html-epilog=s" => \$html_epilog_file, + "html-extension=s" => \$html_ext, + "html-gzip" => \$html_gzip, + "function-coverage" => \$func_coverage, + "no-function-coverage" => \$no_func_coverage, + "branch-coverage" => \$br_coverage, + "no-branch-coverage" => \$no_br_coverage, + "sort" => \$sort, + "no-sort" => \$no_sort, + "demangle-cpp" => \$demangle_cpp, )) { - print_usage(*STDERR); + print(STDERR "Use $tool_name --help to get usage information\n"); exit(1); +} else { + # Merge options + if ($no_func_coverage) { + $func_coverage = 0; + } + if ($no_br_coverage) { + $br_coverage = 0; + } + + # Merge sort options + if ($no_sort) { + $sort = 0; + } } @info_filenames = @ARGV; @@ -257,16 +414,15 @@ if ($help) # Check for version option if ($version) { - print($lcov_version."\n"); + print("$tool_name: $lcov_version\n"); exit(0); } # Check for info filename if (!@info_filenames) { - print(STDERR "No filename specified\n"); - print_usage(*STDERR); - exit(1); + die("No filename specified\n". + "Use $tool_name --help to get usage information\n"); } # Generate a title if none is specified @@ -302,20 +458,51 @@ if ($tab_size < 1) 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 ($no_sourceview && $frames) +if ($no_sourceview && defined($frames)) { warn("WARNING: option --frames disabled because --no-sourceview ". "was specified!\n"); $frames = undef; } +# Issue a warning if --no-prefix is enabled together with --prefix +if ($no_prefix && defined($dir_prefix)) +{ + warn("WARNING: option --prefix disabled because --no-prefix was ". + "specified!\n"); + $dir_prefix = undef; +} + +@fileview_sortlist = ($SORT_FILE); +@funcview_sortlist = ($SORT_FILE); + +if ($sort) { + push(@fileview_sortlist, $SORT_LINE); + push(@fileview_sortlist, $SORT_FUNC) if ($func_coverage); + push(@fileview_sortlist, $SORT_BRANCH) if ($br_coverage); + push(@funcview_sortlist, $SORT_LINE); +} + if ($frames) { # Include genpng code needed for overview image generation do("$tool_dir/genpng"); } +# Ensure that the c++filt tool is available when using --demangle-cpp +if ($demangle_cpp) +{ + if (system_no_output(3, "c++filt", "--version")) { + die("ERROR: could not find c++filt tool needed for ". + "--demangle-cpp\n"); + } +} + # Make sure output_directory exists, create it if necessary if ($output_directory) { @@ -323,12 +510,10 @@ if ($output_directory) if (! -e _) { - system("mkdir", "-p", $output_directory) - and die("ERROR: cannot create directory $_!\n"); + create_sub_dir($output_directory); } } - # Do something gen_html(); @@ -345,37 +530,113 @@ exit(0); sub print_usage(*) { local *HANDLE = $_[0]; - my $executable_name = basename($0); print(HANDLE <index.html") - or die("ERROR: cannot open index.html for writing!\n"); - write_html_prolog(*HTML_HANDLE, "", "LCOV - $test_title"); - write_header(*HTML_HANDLE, 0, "", "", $overall_found, $overall_hit); - write_file_table(*HTML_HANDLE, "", \%overview, {}, 0); - write_html_epilog(*HTML_HANDLE, ""); - close(*HTML_HANDLE); + + # Create sorted pages + foreach (@fileview_sortlist) { + write_dir_page($fileview_sortname[$_], ".", "", $test_title, + undef, $overall_found, $overall_hit, + $total_fn_found, $total_fn_hit, $total_br_found, + $total_br_hit, \%overview, {}, {}, {}, 0, $_); + } # 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, - $overall_found, $overall_hit); + $overall_found, $overall_hit, + $total_fn_found, $total_fn_hit, + $total_br_found, $total_br_hit); } - if ($overall_found == 0) + print_overall_rate(1, $overall_found, $overall_hit, + $func_coverage, $total_fn_found, $total_fn_hit, + $br_coverage, $total_br_found, $total_br_hit); + + chdir($cwd); +} + +# +# html_create(handle, filename) +# + +sub html_create($$) +{ + my $handle = $_[0]; + my $filename = $_[1]; + + if ($html_gzip) { - info("Warning: No lines found!\n"); + open($handle, "|gzip -c >$filename") + or die("ERROR: cannot open $filename for writing ". + "(gzip)!\n"); } else { - info("Overall coverage rate: %d of %d lines (%.1f%%)\n", - $overall_hit, $overall_found, - $overall_hit*100/$overall_found); + open($handle, ">$filename") + or die("ERROR: cannot open $filename for writing!\n"); } +} - chdir($cwd); +sub write_dir_page($$$$$$$$$$$$$$$$$) +{ + my ($name, $rel_dir, $base_dir, $title, $trunc_dir, $overall_found, + $overall_hit, $total_fn_found, $total_fn_hit, $total_br_found, + $total_br_hit, $overview, $testhash, $testfnchash, $testbrhash, + $view_type, $sort_type) = @_; + + # Generate directory overview page including details + html_create(*HTML_HANDLE, "$rel_dir/index$name.$html_ext"); + if (!defined($trunc_dir)) { + $trunc_dir = ""; + } + write_html_prolog(*HTML_HANDLE, $base_dir, "LCOV - $title$trunc_dir"); + write_header(*HTML_HANDLE, $view_type, $trunc_dir, $rel_dir, + $overall_found, $overall_hit, $total_fn_found, + $total_fn_hit, $total_br_found, $total_br_hit, $sort_type); + write_file_table(*HTML_HANDLE, $base_dir, $overview, $testhash, + $testfnchash, $testbrhash, $view_type, $sort_type); + write_html_epilog(*HTML_HANDLE, $base_dir); + close(*HTML_HANDLE); } @@ -556,12 +880,25 @@ sub process_dir($) my %overview; my $lines_found; my $lines_hit; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; my $overall_found=0; my $overall_hit=0; + my $total_fn_found=0; + my $total_fn_hit=0; + my $total_br_found = 0; + my $total_br_hit = 0; my $base_name; my $extension; my $testdata; my %testhash; + my $testfncdata; + my %testfnchash; + my $testbrdata; + my %testbrhash; + my @sort_list; local *HTML_HANDLE; # Remove prefix if applicable @@ -587,70 +924,68 @@ sub process_dir($) # sub-directories foreach $filename (grep(/^\Q$abs_dir\E\/[^\/]*$/,keys(%info_data))) { - ($lines_found, $lines_hit, $testdata) = + my $page_link; + my $func_link; + + ($lines_found, $lines_hit, $fn_found, $fn_hit, $br_found, + $br_hit, $testdata, $testfncdata, $testbrdata) = process_file($trunc_dir, $rel_dir, $filename); $base_name = basename($filename); - if ($no_sourceview) - { - # User asked as not to create source code view, do not - # provide a page link - $overview{$base_name} = - "$lines_found,$lines_hit"; - } - elsif ($frames) - { + if ($no_sourceview) { + $page_link = ""; + } elsif ($frames) { # Link to frameset page - $overview{$base_name} = - "$lines_found,$lines_hit,". - "$base_name.gcov.frameset.html"; - } - else - { + $page_link = "$base_name.gcov.frameset.$html_ext"; + } else { # Link directory to source code view page - $overview{$base_name} = - "$lines_found,$lines_hit,". - "$base_name.gcov.html"; + $page_link = "$base_name.gcov.$html_ext"; } + $overview{$base_name} = [$lines_found, $lines_hit, $fn_found, + $fn_hit, $br_found, $br_hit, + $page_link, + get_rate($lines_found, $lines_hit), + get_rate($fn_found, $fn_hit), + get_rate($br_found, $br_hit)]; $testhash{$base_name} = $testdata; + $testfnchash{$base_name} = $testfncdata; + $testbrhash{$base_name} = $testbrdata; $overall_found += $lines_found; $overall_hit += $lines_hit; + + $total_fn_found += $fn_found; + $total_fn_hit += $fn_hit; + + $total_br_found += $br_found; + $total_br_hit += $br_hit; } - # Generate directory overview page (without details) - open(*HTML_HANDLE, ">$rel_dir/index.html") - or die("ERROR: cannot open $rel_dir/index.html ". - "for writing!\n"); - write_html_prolog(*HTML_HANDLE, $base_dir, - "LCOV - $test_title - $trunc_dir"); - write_header(*HTML_HANDLE, 1, $trunc_dir, $rel_dir, $overall_found, - $overall_hit); - write_file_table(*HTML_HANDLE, $base_dir, \%overview, {}, 1); - write_html_epilog(*HTML_HANDLE, $base_dir); - close(*HTML_HANDLE); - - if ($show_details) - { + # Create sorted pages + foreach (@fileview_sortlist) { + # Generate directory overview page (without details) + write_dir_page($fileview_sortname[$_], $rel_dir, $base_dir, + $test_title, $trunc_dir, $overall_found, + $overall_hit, $total_fn_found, $total_fn_hit, + $total_br_found, $total_br_hit, \%overview, {}, + {}, {}, 1, $_); + if (!$show_details) { + next; + } # Generate directory overview page including details - open(*HTML_HANDLE, ">$rel_dir/index-detail.html") - or die("ERROR: cannot open $rel_dir/". - "index-detail.html for writing!\n"); - write_html_prolog(*HTML_HANDLE, $base_dir, - "LCOV - $test_title - $trunc_dir"); - write_header(*HTML_HANDLE, 1, $trunc_dir, $rel_dir, - $overall_found, - $overall_hit); - write_file_table(*HTML_HANDLE, $base_dir, \%overview, - \%testhash, 1); - write_html_epilog(*HTML_HANDLE, $base_dir); - close(*HTML_HANDLE); + write_dir_page("-detail".$fileview_sortname[$_], $rel_dir, + $base_dir, $test_title, $trunc_dir, + $overall_found, $overall_hit, $total_fn_found, + $total_fn_hit, $total_br_found, $total_br_hit, + \%overview, \%testhash, \%testfnchash, + \%testbrhash, 1, $_); } # Calculate resulting line counts - return ($overall_found, $overall_hit); + return ($overall_found, $overall_hit, $total_fn_found, $total_fn_hit, + $total_br_found, $total_br_hit); } @@ -711,6 +1046,37 @@ sub get_converted_lines($) } +sub write_function_page($$$$$$$$$$$$$$$$$$) +{ + my ($base_dir, $rel_dir, $trunc_dir, $base_name, $title, + $lines_found, $lines_hit, $fn_found, $fn_hit, $br_found, $br_hit, + $sumcount, $funcdata, $sumfnccount, $testfncdata, $sumbrcount, + $testbrdata, $sort_type) = @_; + my $pagetitle; + my $filename; + + # Generate function table for this file + if ($sort_type == 0) { + $filename = "$rel_dir/$base_name.func.$html_ext"; + } else { + $filename = "$rel_dir/$base_name.func-sort-c.$html_ext"; + } + html_create(*HTML_HANDLE, $filename); + $pagetitle = "LCOV - $title - $trunc_dir/$base_name - functions"; + write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle); + write_header(*HTML_HANDLE, 4, "$trunc_dir/$base_name", + "$rel_dir/$base_name", $lines_found, $lines_hit, + $fn_found, $fn_hit, $br_found, $br_hit, $sort_type); + write_function_table(*HTML_HANDLE, "$base_name.gcov.$html_ext", + $sumcount, $funcdata, + $sumfnccount, $testfncdata, $sumbrcount, + $testbrdata, $base_name, + $base_dir, $sort_type); + write_html_epilog(*HTML_HANDLE, $base_dir, 1); + close(*HTML_HANDLE); +} + + # # process_file(trunc_dir, rel_dir, filename) # @@ -729,42 +1095,69 @@ sub process_file($$$) my $sumcount; my $funcdata; my $checkdata; + my $testfncdata; + my $sumfnccount; + my $testbrdata; + my $sumbrcount; my $lines_found; my $lines_hit; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; my $converted; my @source; my $pagetitle; local *HTML_HANDLE; - ($testdata, $sumcount, $funcdata, $checkdata, $lines_found, - $lines_hit) = get_info_entry($info_data{$filename}); + ($testdata, $sumcount, $funcdata, $checkdata, $testfncdata, + $sumfnccount, $testbrdata, $sumbrcount, $lines_found, $lines_hit, + $fn_found, $fn_hit, $br_found, $br_hit) + = get_info_entry($info_data{$filename}); # Return after this point in case user asked us not to generate # source code view if ($no_sourceview) { - return ($lines_found, $lines_hit, $testdata); + return ($lines_found, $lines_hit, $fn_found, $fn_hit, + $br_found, $br_hit, $testdata, $testfncdata, + $testbrdata); } $converted = get_converted_lines($testdata); # Generate source code view for this file - open(*HTML_HANDLE, ">$rel_dir/$base_name.gcov.html") - or die("ERROR: cannot open $rel_dir/$base_name.gcov.html ". - "for writing!\n"); + html_create(*HTML_HANDLE, "$rel_dir/$base_name.gcov.$html_ext"); $pagetitle = "LCOV - $test_title - $trunc_dir/$base_name"; write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle); write_header(*HTML_HANDLE, 2, "$trunc_dir/$base_name", - "$rel_dir/$base_name", $lines_found, $lines_hit); + "$rel_dir/$base_name", $lines_found, $lines_hit, + $fn_found, $fn_hit, $br_found, $br_hit, 0); @source = write_source(*HTML_HANDLE, $filename, $sumcount, $checkdata, - $converted); + $converted, $funcdata, $sumbrcount); write_html_epilog(*HTML_HANDLE, $base_dir, 1); close(*HTML_HANDLE); + if ($func_coverage) { + # Create function tables + foreach (@funcview_sortlist) { + write_function_page($base_dir, $rel_dir, $trunc_dir, + $base_name, $test_title, + $lines_found, $lines_hit, + $fn_found, $fn_hit, $br_found, + $br_hit, $sumcount, + $funcdata, $sumfnccount, + $testfncdata, $sumbrcount, + $testbrdata, $_); + } + } + # Additional files are needed in case of frame output if (!$frames) { - return ($lines_found, $lines_hit, $testdata); + return ($lines_found, $lines_hit, $fn_found, $fn_hit, + $br_found, $br_hit, $testdata, $testfncdata, + $testbrdata); } # Create overview png file @@ -772,23 +1165,20 @@ sub process_file($$$) @source); # Create frameset page - open(*HTML_HANDLE, ">$rel_dir/$base_name.gcov.frameset.html") - or die("ERROR: cannot open ". - "$rel_dir/$base_name.gcov.frameset.html". - " for writing!\n"); + html_create(*HTML_HANDLE, + "$rel_dir/$base_name.gcov.frameset.$html_ext"); write_frameset(*HTML_HANDLE, $base_dir, $base_name, $pagetitle); close(*HTML_HANDLE); # Write overview frame - open(*HTML_HANDLE, ">$rel_dir/$base_name.gcov.overview.html") - or die("ERROR: cannot open ". - "$rel_dir/$base_name.gcov.overview.html". - " for writing!\n"); + html_create(*HTML_HANDLE, + "$rel_dir/$base_name.gcov.overview.$html_ext"); write_overview(*HTML_HANDLE, $base_dir, $base_name, $pagetitle, scalar(@source)); close(*HTML_HANDLE); - return ($lines_found, $lines_hit, $testdata); + return ($lines_found, $lines_hit, $fn_found, $fn_hit, $br_found, + $br_hit, $testdata, $testfncdata, $testbrdata); } @@ -806,13 +1196,24 @@ sub process_file($$$) # "found" -> $lines_found (number of instrumented lines found in file) # "hit" -> $lines_hit (number of executed lines in file) # "check" -> \%checkdata +# "testfnc" -> \%testfncdata +# "sumfnc" -> \%sumfnccount +# "testbr" -> \%testbrdata +# "sumbr" -> \%sumbrcount # -# %testdata: name of test affecting this file -> \%testcount +# %testdata : name of test affecting this file -> \%testcount +# %testfncdata: name of test affecting this file -> \%testfnccount +# %testbrdata: name of test affecting this file -> \%testbrcount # -# %testcount: line number -> execution count for a single test -# %sumcount : line number -> execution count for all tests -# %funcdata : line number -> name of function beginning at that line -# %checkdata: line number -> checksum of source code line +# %testcount : line number -> execution count for a single test +# %testfnccount: function name -> execution count for a single test +# %testbrcount : line number -> branch coverage data for a single test +# %sumcount : line number -> execution count for all tests +# %sumfnccount : function name -> execution count for all tests +# %sumbrcount : line number -> branch coverage data for all tests +# %funcdata : function name -> line number +# %checkdata : line number -> checksum of source code line +# $brdata : vector of items: block, branch, taken # # Note that .info file sections referring to the same file and test name # will automatically be combined by adding all execution counts. @@ -834,15 +1235,26 @@ sub read_info_file($) my $sumcount; # " " my $funcdata; # " " my $checkdata; # " " + my $testfncdata; + my $testfnccount; + my $sumfnccount; + my $testbrdata; + my $testbrcount; + my $sumbrcount; my $line; # Current line read from .info file my $testname; # Current test name my $filename; # Current filename my $hitcount; # Count for lines hit my $count; # Execution count of current line my $negative; # If set, warn about negative counts - my $checksum; # Checksum of current line + my $changed_testname; # If set, warn about changed testname + my $line_checksum; # Checksum of current line + my $br_found; + my $br_hit; local *INFO_HANDLE; # Filehandle for .info file + info("Reading data file $tracefile\n"); + # Check if file exists and is readable stat($_[0]); if (!(-r _)) @@ -860,7 +1272,7 @@ sub read_info_file($) if ($_[0] =~ /\.gz$/) { # Check for availability of GZIP tool - system_no_output(1, "gunzip", "-h") + system_no_output(1, "gunzip" ,"-h") and die("ERROR: gunzip command not available!\n"); # Check integrity of compressed file @@ -870,12 +1282,12 @@ sub read_info_file($) # Open compressed file open(INFO_HANDLE, "gunzip -c $_[0]|") - or die("ERROR: cannot start gunzip to uncompress ". + or die("ERROR: cannot start gunzip to decompress ". "file $_[0]!\n"); } else { - # Open uncompressed file + # Open decompressed file open(INFO_HANDLE, $_[0]) or die("ERROR: cannot read file $_[0]!\n"); } @@ -889,10 +1301,15 @@ sub read_info_file($) # Switch statement foreach ($line) { - /^TN:(\w*(,\w+)?)/ && do + /^TN:([^,]*)(,diff)?/ && do { # Test name information found $testname = defined($1) ? $1 : ""; + if ($testname =~ s/\W/_/g) + { + $changed_testname = 1; + } + $testname .= $2 if (defined($2)); last; }; @@ -903,17 +1320,22 @@ sub read_info_file($) $filename = $1; $data = $result{$filename}; - ($testdata, $sumcount, $funcdata, $checkdata) = + ($testdata, $sumcount, $funcdata, $checkdata, + $testfncdata, $sumfnccount, $testbrdata, + $sumbrcount) = get_info_entry($data); if (defined($testname)) { $testcount = $testdata->{$testname}; + $testfnccount = $testfncdata->{$testname}; + $testbrcount = $testbrdata->{$testname}; } else { - my %new_hash; - $testcount = \%new_hash; + $testcount = {}; + $testfnccount = {}; + $testbrcount = {}; } last; }; @@ -939,17 +1361,18 @@ sub read_info_file($) # Store line checksum if available if (defined($3)) { - $checksum = substr($3, 1); + $line_checksum = substr($3, 1); # Does it match a previous definition if (defined($checkdata->{$1}) && - ($checkdata->{$1} ne $checksum)) + ($checkdata->{$1} ne + $line_checksum)) { die("ERROR: checksum mismatch ". "at $filename:$1\n"); } - $checkdata->{$1} = $checksum; + $checkdata->{$1} = $line_checksum; } last; }; @@ -957,7 +1380,52 @@ sub read_info_file($) /^FN:(\d+),([^,]+)/ && do { # Function data found, add to structure - $funcdata->{$1} = $2; + $funcdata->{$2} = $1; + + # Also initialize function call data + if (!defined($sumfnccount->{$2})) { + $sumfnccount->{$2} = 0; + } + if (defined($testname)) + { + if (!defined($testfnccount->{$2})) { + $testfnccount->{$2} = 0; + } + } + last; + }; + + /^FNDA:(\d+),([^,]+)/ && do + { + # Function call count found, add to structure + # Add summary counts + $sumfnccount->{$2} += $1; + + # Add test-specific counts + if (defined($testname)) + { + $testfnccount->{$2} += $1; + } + last; + }; + + /^BRDA:(\d+),(\d+),(\d+),(\d+|-)/ && do { + # Branch coverage data found + my ($line, $block, $branch, $taken) = + ($1, $2, $3, $4); + + $sumbrcount->{$line} = + br_ivec_push($sumbrcount->{$line}, + $block, $branch, $taken); + + # Add test-specific counts + if (defined($testname)) { + $testbrcount->{$line} = + br_ivec_push( + $testbrcount->{$line}, + $block, $branch, + $taken); + } last; }; @@ -971,13 +1439,21 @@ sub read_info_file($) { $testdata->{$testname} = $testcount; - } + $testfncdata->{$testname} = + $testfnccount; + $testbrdata->{$testname} = + $testbrcount; + } + set_info_entry($data, $testdata, $sumcount, $funcdata, - $checkdata); + $checkdata, $testfncdata, + $sumfnccount, + $testbrdata, + $sumbrcount); $result{$filename} = $data; + last; } - }; # default @@ -991,7 +1467,26 @@ sub read_info_file($) { $data = $result{$filename}; - ($testdata, $sumcount, $funcdata) = get_info_entry($data); + ($testdata, $sumcount, undef, undef, $testfncdata, + $sumfnccount, $testbrdata, $sumbrcount) = + get_info_entry($data); + + # Filter out empty files + if (scalar(keys(%{$sumcount})) == 0) + { + delete($result{$filename}); + next; + } + # Filter out empty test cases + foreach $testname (keys(%{$testdata})) + { + if (!defined($testdata->{$testname}) || + scalar(keys(%{$testdata->{$testname}})) == 0) + { + delete($testdata->{$testname}); + delete($testfncdata->{$testname}); + } + } $data->{"found"} = scalar(keys(%{$sumcount})); $hitcount = 0; @@ -1003,7 +1498,22 @@ sub read_info_file($) $data->{"hit"} = $hitcount; - $result{$filename} = $data; + # Get found/hit values for function call data + $data->{"f_found"} = scalar(keys(%{$sumfnccount})); + $hitcount = 0; + + foreach (keys(%{$sumfnccount})) { + if ($sumfnccount->{$_} > 0) { + $hitcount++; + } + } + $data->{"f_hit"} = $hitcount; + + # Get found/hit values for branch data + ($br_found, $br_hit) = get_br_found_and_hit($sumbrcount); + + $data->{"b_found"} = $br_found; + $data->{"b_hit"} = $br_hit; } if (scalar(keys(%result)) == 0) @@ -1015,6 +1525,11 @@ sub read_info_file($) warn("WARNING: negative counts found in tracefile ". "$tracefile\n"); } + if ($changed_testname) + { + warn("WARNING: invalid characters removed from testname in ". + "tracefile $tracefile\n"); + } return(\%result); } @@ -1026,7 +1541,8 @@ sub read_info_file($) # Retrieve data from an entry of the structure generated by read_info_file(). # Return a list of references to hashes: # (test data hash ref, sum count hash ref, funcdata hash ref, checkdata hash -# ref, lines found, lines hit) +# ref, testfncdata hash ref, sumfnccount hash ref, lines found, lines hit, +# functions found, functions hit) # sub get_info_entry($) @@ -1035,22 +1551,34 @@ sub get_info_entry($) my $sumcount_ref = $_[0]->{"sum"}; my $funcdata_ref = $_[0]->{"func"}; my $checkdata_ref = $_[0]->{"check"}; + my $testfncdata = $_[0]->{"testfnc"}; + my $sumfnccount = $_[0]->{"sumfnc"}; + my $testbrdata = $_[0]->{"testbr"}; + my $sumbrcount = $_[0]->{"sumbr"}; my $lines_found = $_[0]->{"found"}; my $lines_hit = $_[0]->{"hit"}; + my $fn_found = $_[0]->{"f_found"}; + my $fn_hit = $_[0]->{"f_hit"}; + my $br_found = $_[0]->{"b_found"}; + my $br_hit = $_[0]->{"b_hit"}; return ($testdata_ref, $sumcount_ref, $funcdata_ref, $checkdata_ref, - $lines_found, $lines_hit); + $testfncdata, $sumfnccount, $testbrdata, $sumbrcount, + $lines_found, $lines_hit, $fn_found, $fn_hit, + $br_found, $br_hit); } # # set_info_entry(hash_ref, testdata_ref, sumcount_ref, funcdata_ref, -# checkdata_ref[,lines_found, lines_hit]) +# checkdata_ref, testfncdata_ref, sumfcncount_ref, +# testbrdata_ref, sumbrcount_ref[,lines_found, +# lines_hit, f_found, f_hit, $b_found, $b_hit]) # # Update the hash referenced by HASH_REF with the provided data references. # -sub set_info_entry($$$$$;$$) +sub set_info_entry($$$$$$$$$;$$$$$$) { my $data_ref = $_[0]; @@ -1058,9 +1586,526 @@ sub set_info_entry($$$$$;$$) $data_ref->{"sum"} = $_[2]; $data_ref->{"func"} = $_[3]; $data_ref->{"check"} = $_[4]; + $data_ref->{"testfnc"} = $_[5]; + $data_ref->{"sumfnc"} = $_[6]; + $data_ref->{"testbr"} = $_[7]; + $data_ref->{"sumbr"} = $_[8]; - if (defined($_[5])) { $data_ref->{"found"} = $_[5]; } - if (defined($_[6])) { $data_ref->{"hit"} = $_[6]; } + if (defined($_[9])) { $data_ref->{"found"} = $_[9]; } + if (defined($_[10])) { $data_ref->{"hit"} = $_[10]; } + if (defined($_[11])) { $data_ref->{"f_found"} = $_[11]; } + if (defined($_[12])) { $data_ref->{"f_hit"} = $_[12]; } + if (defined($_[13])) { $data_ref->{"b_found"} = $_[13]; } + if (defined($_[14])) { $data_ref->{"b_hit"} = $_[14]; } +} + + +# +# add_counts(data1_ref, data2_ref) +# +# DATA1_REF and DATA2_REF are references to hashes containing a mapping +# +# line number -> execution count +# +# Return a list (RESULT_REF, LINES_FOUND, LINES_HIT) where RESULT_REF +# is a reference to a hash containing the combined mapping in which +# execution counts are added. +# + +sub add_counts($$) +{ + my %data1 = %{$_[0]}; # Hash 1 + my %data2 = %{$_[1]}; # Hash 2 + my %result; # Resulting hash + my $line; # Current line iteration scalar + my $data1_count; # Count of line in hash1 + my $data2_count; # Count of line in hash2 + my $found = 0; # Total number of lines found + my $hit = 0; # Number of lines with a count > 0 + + foreach $line (keys(%data1)) + { + $data1_count = $data1{$line}; + $data2_count = $data2{$line}; + + # Add counts if present in both hashes + if (defined($data2_count)) { $data1_count += $data2_count; } + + # Store sum in %result + $result{$line} = $data1_count; + + $found++; + if ($data1_count > 0) { $hit++; } + } + + # Add lines unique to data2 + foreach $line (keys(%data2)) + { + # Skip lines already in data1 + if (defined($data1{$line})) { next; } + + # Copy count from data2 + $result{$line} = $data2{$line}; + + $found++; + if ($result{$line} > 0) { $hit++; } + } + + return (\%result, $found, $hit); +} + + +# +# merge_checksums(ref1, ref2, filename) +# +# REF1 and REF2 are references to hashes containing a mapping +# +# line number -> checksum +# +# Merge checksum lists defined in REF1 and REF2 and return reference to +# resulting hash. Die if a checksum for a line is defined in both hashes +# but does not match. +# + +sub merge_checksums($$$) +{ + my $ref1 = $_[0]; + my $ref2 = $_[1]; + my $filename = $_[2]; + my %result; + my $line; + + foreach $line (keys(%{$ref1})) + { + if (defined($ref2->{$line}) && + ($ref1->{$line} ne $ref2->{$line})) + { + die("ERROR: checksum mismatch at $filename:$line\n"); + } + $result{$line} = $ref1->{$line}; + } + + foreach $line (keys(%{$ref2})) + { + $result{$line} = $ref2->{$line}; + } + + return \%result; +} + + +# +# merge_func_data(funcdata1, funcdata2, filename) +# + +sub merge_func_data($$$) +{ + my ($funcdata1, $funcdata2, $filename) = @_; + my %result; + my $func; + + if (defined($funcdata1)) { + %result = %{$funcdata1}; + } + + foreach $func (keys(%{$funcdata2})) { + my $line1 = $result{$func}; + my $line2 = $funcdata2->{$func}; + + if (defined($line1) && ($line1 != $line2)) { + warn("WARNING: function data mismatch at ". + "$filename:$line2\n"); + next; + } + $result{$func} = $line2; + } + + return \%result; +} + + +# +# add_fnccount(fnccount1, fnccount2) +# +# Add function call count data. Return list (fnccount_added, f_found, f_hit) +# + +sub add_fnccount($$) +{ + my ($fnccount1, $fnccount2) = @_; + my %result; + my $fn_found; + my $fn_hit; + my $function; + + if (defined($fnccount1)) { + %result = %{$fnccount1}; + } + foreach $function (keys(%{$fnccount2})) { + $result{$function} += $fnccount2->{$function}; + } + $fn_found = scalar(keys(%result)); + $fn_hit = 0; + foreach $function (keys(%result)) { + if ($result{$function} > 0) { + $fn_hit++; + } + } + + return (\%result, $fn_found, $fn_hit); +} + +# +# add_testfncdata(testfncdata1, testfncdata2) +# +# Add function call count data for several tests. Return reference to +# added_testfncdata. +# + +sub add_testfncdata($$) +{ + my ($testfncdata1, $testfncdata2) = @_; + my %result; + my $testname; + + foreach $testname (keys(%{$testfncdata1})) { + if (defined($testfncdata2->{$testname})) { + my $fnccount; + + # Function call count data for this testname exists + # in both data sets: add + ($fnccount) = add_fnccount( + $testfncdata1->{$testname}, + $testfncdata2->{$testname}); + $result{$testname} = $fnccount; + next; + } + # Function call count data for this testname is unique to + # data set 1: copy + $result{$testname} = $testfncdata1->{$testname}; + } + + # Add count data for testnames unique to data set 2 + foreach $testname (keys(%{$testfncdata2})) { + if (!defined($result{$testname})) { + $result{$testname} = $testfncdata2->{$testname}; + } + } + return \%result; +} + + +# +# brcount_to_db(brcount) +# +# Convert brcount data to the following format: +# +# db: line number -> block hash +# block hash: block number -> branch hash +# branch hash: branch number -> taken value +# + +sub brcount_to_db($) +{ + my ($brcount) = @_; + my $line; + my $db; + + # Add branches from first count to database + foreach $line (keys(%{$brcount})) { + my $brdata = $brcount->{$line}; + my $i; + my $num = br_ivec_len($brdata); + + for ($i = 0; $i < $num; $i++) { + my ($block, $branch, $taken) = br_ivec_get($brdata, $i); + + $db->{$line}->{$block}->{$branch} = $taken; + } + } + + return $db; +} + + +# +# db_to_brcount(db) +# +# Convert branch coverage data back to brcount format. +# + +sub db_to_brcount($) +{ + my ($db) = @_; + my $line; + my $brcount = {}; + my $br_found = 0; + my $br_hit = 0; + + # Convert database back to brcount format + foreach $line (sort({$a <=> $b} keys(%{$db}))) { + my $ldata = $db->{$line}; + my $brdata; + my $block; + + foreach $block (sort({$a <=> $b} keys(%{$ldata}))) { + my $bdata = $ldata->{$block}; + my $branch; + + foreach $branch (sort({$a <=> $b} keys(%{$bdata}))) { + my $taken = $bdata->{$branch}; + + $br_found++; + $br_hit++ if ($taken ne "-" && $taken > 0); + $brdata = br_ivec_push($brdata, $block, + $branch, $taken); + } + } + $brcount->{$line} = $brdata; + } + + return ($brcount, $br_found, $br_hit); +} + + +# +# combine_brcount(brcount1, brcount2, type) +# +# If add is BR_ADD, add branch coverage data and return list (brcount_added, +# br_found, br_hit). If add is BR_SUB, subtract the taken values of brcount2 +# from brcount1 and return (brcount_sub, br_found, br_hit). +# + +sub combine_brcount($$$) +{ + my ($brcount1, $brcount2, $type) = @_; + my $line; + my $block; + my $branch; + my $taken; + my $db; + my $br_found = 0; + my $br_hit = 0; + my $result; + + # Convert branches from first count to database + $db = brcount_to_db($brcount1); + # Combine values from database and second count + foreach $line (keys(%{$brcount2})) { + my $brdata = $brcount2->{$line}; + my $num = br_ivec_len($brdata); + my $i; + + for ($i = 0; $i < $num; $i++) { + ($block, $branch, $taken) = br_ivec_get($brdata, $i); + my $new_taken = $db->{$line}->{$block}->{$branch}; + + if ($type == $BR_ADD) { + $new_taken = br_taken_add($new_taken, $taken); + } elsif ($type == $BR_SUB) { + $new_taken = br_taken_sub($new_taken, $taken); + } + $db->{$line}->{$block}->{$branch} = $new_taken + if (defined($new_taken)); + } + } + # Convert database back to brcount format + ($result, $br_found, $br_hit) = db_to_brcount($db); + + return ($result, $br_found, $br_hit); +} + + +# +# add_testbrdata(testbrdata1, testbrdata2) +# +# Add branch coverage data for several tests. Return reference to +# added_testbrdata. +# + +sub add_testbrdata($$) +{ + my ($testbrdata1, $testbrdata2) = @_; + my %result; + my $testname; + + foreach $testname (keys(%{$testbrdata1})) { + if (defined($testbrdata2->{$testname})) { + my $brcount; + + # Branch coverage data for this testname exists + # in both data sets: add + ($brcount) = combine_brcount($testbrdata1->{$testname}, + $testbrdata2->{$testname}, $BR_ADD); + $result{$testname} = $brcount; + next; + } + # Branch coverage data for this testname is unique to + # data set 1: copy + $result{$testname} = $testbrdata1->{$testname}; + } + + # Add count data for testnames unique to data set 2 + foreach $testname (keys(%{$testbrdata2})) { + if (!defined($result{$testname})) { + $result{$testname} = $testbrdata2->{$testname}; + } + } + return \%result; +} + + +# +# combine_info_entries(entry_ref1, entry_ref2, filename) +# +# Combine .info data entry hashes referenced by ENTRY_REF1 and ENTRY_REF2. +# Return reference to resulting hash. +# + +sub combine_info_entries($$$) +{ + my $entry1 = $_[0]; # Reference to hash containing first entry + my $testdata1; + my $sumcount1; + my $funcdata1; + my $checkdata1; + my $testfncdata1; + my $sumfnccount1; + my $testbrdata1; + my $sumbrcount1; + + my $entry2 = $_[1]; # Reference to hash containing second entry + my $testdata2; + my $sumcount2; + my $funcdata2; + my $checkdata2; + my $testfncdata2; + my $sumfnccount2; + my $testbrdata2; + my $sumbrcount2; + + my %result; # Hash containing combined entry + my %result_testdata; + my $result_sumcount = {}; + my $result_funcdata; + my $result_testfncdata; + my $result_sumfnccount; + my $result_testbrdata; + my $result_sumbrcount; + my $lines_found; + my $lines_hit; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; + + my $testname; + my $filename = $_[2]; + + # Retrieve data + ($testdata1, $sumcount1, $funcdata1, $checkdata1, $testfncdata1, + $sumfnccount1, $testbrdata1, $sumbrcount1) = get_info_entry($entry1); + ($testdata2, $sumcount2, $funcdata2, $checkdata2, $testfncdata2, + $sumfnccount2, $testbrdata2, $sumbrcount2) = get_info_entry($entry2); + + # Merge checksums + $checkdata1 = merge_checksums($checkdata1, $checkdata2, $filename); + + # Combine funcdata + $result_funcdata = merge_func_data($funcdata1, $funcdata2, $filename); + + # Combine function call count data + $result_testfncdata = add_testfncdata($testfncdata1, $testfncdata2); + ($result_sumfnccount, $fn_found, $fn_hit) = + add_fnccount($sumfnccount1, $sumfnccount2); + + # Combine branch coverage data + $result_testbrdata = add_testbrdata($testbrdata1, $testbrdata2); + ($result_sumbrcount, $br_found, $br_hit) = + combine_brcount($sumbrcount1, $sumbrcount2, $BR_ADD); + + # Combine testdata + foreach $testname (keys(%{$testdata1})) + { + if (defined($testdata2->{$testname})) + { + # testname is present in both entries, requires + # combination + ($result_testdata{$testname}) = + add_counts($testdata1->{$testname}, + $testdata2->{$testname}); + } + else + { + # testname only present in entry1, add to result + $result_testdata{$testname} = $testdata1->{$testname}; + } + + # update sum count hash + ($result_sumcount, $lines_found, $lines_hit) = + add_counts($result_sumcount, + $result_testdata{$testname}); + } + + foreach $testname (keys(%{$testdata2})) + { + # Skip testnames already covered by previous iteration + if (defined($testdata1->{$testname})) { next; } + + # testname only present in entry2, add to result hash + $result_testdata{$testname} = $testdata2->{$testname}; + + # update sum count hash + ($result_sumcount, $lines_found, $lines_hit) = + add_counts($result_sumcount, + $result_testdata{$testname}); + } + + # Calculate resulting sumcount + + # Store result + set_info_entry(\%result, \%result_testdata, $result_sumcount, + $result_funcdata, $checkdata1, $result_testfncdata, + $result_sumfnccount, $result_testbrdata, + $result_sumbrcount, $lines_found, $lines_hit, + $fn_found, $fn_hit, $br_found, $br_hit); + + return(\%result); +} + + +# +# combine_info_files(info_ref1, info_ref2) +# +# Combine .info data in hashes referenced by INFO_REF1 and INFO_REF2. Return +# reference to resulting hash. +# + +sub combine_info_files($$) +{ + my %hash1 = %{$_[0]}; + my %hash2 = %{$_[1]}; + my $filename; + + foreach $filename (keys(%hash2)) + { + if ($hash1{$filename}) + { + # Entry already exists in hash1, combine them + $hash1{$filename} = + combine_info_entries($hash1{$filename}, + $hash2{$filename}, + $filename); + } + else + { + # Entry is unique in both hashes, simply add to + # resulting hash + $hash1{$filename} = $hash2{$filename}; + } + } + + return(\%hash1); } @@ -1212,6 +2257,7 @@ sub read_testfile($) { my %result; my $test_name; + my $changed_testname; local *TEST_HANDLE; open(TEST_HANDLE, "<".$_[0]) @@ -1226,6 +2272,10 @@ sub read_testfile($) { # Store name for later use $test_name = $1; + if ($test_name =~ s/\W/_/g) + { + $changed_testname = 1; + } } # Match lines beginning with TD: @@ -1247,6 +2297,12 @@ sub read_testfile($) close(TEST_HANDLE); + if ($changed_testname) + { + warn("WARNING: invalid characters removed from testname in ". + "descriptions file $_[0]\n"); + } + return \%result; } @@ -1310,13 +2366,17 @@ sub get_date_string() sub create_sub_dir($) { - system("mkdir", "-p" ,$_[0]) - and die("ERROR: cannot create directory $_!\n"); + my ($dir) = @_; + + system("mkdir", "-p" ,$dir) + and die("ERROR: cannot create directory $dir!\n"); } # -# write_description_file(descriptions, overall_found, overall_hit) +# 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 @@ -1326,19 +2386,22 @@ sub create_sub_dir($) # Die on error. # -sub write_description_file($$$) +sub write_description_file($$$$$$$) { my %description = %{$_[0]}; my $found = $_[1]; my $hit = $_[2]; + my $fn_found = $_[3]; + my $fn_hit = $_[4]; + my $br_found = $_[5]; + my $br_hit = $_[6]; my $test_name; local *HTML_HANDLE; - open(HTML_HANDLE, ">descriptions.html") - or die("ERROR: cannot open descriptions.html for writing!\n"); - + html_create(*HTML_HANDLE,"descriptions.$html_ext"); write_html_prolog(*HTML_HANDLE, "", "LCOV - test case descriptions"); - write_header(*HTML_HANDLE, 3, "", "", $found, $hit); + write_header(*HTML_HANDLE, 3, "", "", $found, $hit, $fn_found, + $fn_hit, $br_found, $br_hit, 0); write_test_table_prolog(*HTML_HANDLE, "Test case descriptions - alphabetical list"); @@ -1352,7 +2415,7 @@ sub write_description_file($$$) write_test_table_epilog(*HTML_HANDLE); write_html_epilog(*HTML_HANDLE, ""); - close(HTML_HANDLE); + close(*HTML_HANDLE); } @@ -1401,8 +2464,8 @@ sub write_png_files() 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]; + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, + 0x82]; $data{"emerald.png"} = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, @@ -1453,7 +2516,19 @@ sub write_png_files() 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]; - + $data{"updown.png"} = + [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] if ($sort); foreach (keys(%data)) { open(PNG_HANDLE, ">".$_) @@ -1465,6 +2540,28 @@ sub write_png_files() } +# +# write_htaccess_file() +# + +sub write_htaccess_file() +{ + local *HTACCESS_HANDLE; + my $htaccess_data; + + open(*HTACCESS_HANDLE, ">.htaccess") + or die("ERROR: cannot open .htaccess for writing!\n"); + + $htaccess_data = (<<"END_OF_HTACCESS") +AddEncoding x-gzip .html +END_OF_HTACCESS + ; + + print(HTACCESS_HANDLE $htaccess_data); + close(*HTACCESS_HANDLE); +} + + # # write_css_file() # @@ -1495,318 +2592,522 @@ sub write_css_file() /* All views: initial background and text color */ body { - color: #000000; + color: #000000; background-color: #FFFFFF; } - - + /* All views: standard link format*/ a:link { - color: #284FA8; + color: #284FA8; text-decoration: underline; } - - + /* All views: standard link - visited format */ a:visited { - color: #00CB40; + color: #00CB40; text-decoration: underline; } - - + /* All views: standard link - activated format */ a:active { - color: #FF0040; + color: #FF0040; text-decoration: underline; } - - + /* All views: main title format */ td.title { - text-align: center; + text-align: center; padding-bottom: 10px; - font-family: sans-serif; - font-size: 20pt; - font-style: italic; - font-weight: bold; + font-family: sans-serif; + font-size: 20pt; + font-style: italic; + font-weight: bold; } - - + /* All views: header item format */ td.headerItem { - text-align: right; + text-align: right; padding-right: 6px; - font-family: sans-serif; - font-weight: bold; + 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: #284FA8; + text-align: left; + color: #284FA8; 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; + font-size: 80%; + white-space: nowrap; + } + + /* All views: header item coverage table entry */ + td.headerCovTableEntry + { + text-align: right; + color: #284FA8; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #DAE7FE; + } + + /* All views: header item coverage table entry for high coverage rate */ + td.headerCovTableEntryHi + { + text-align: right; + color: #000000; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #A7FC9D; + } + + /* All views: header item coverage table entry for medium coverage rate */ + td.headerCovTableEntryMed + { + text-align: right; + color: #000000; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #FFEA20; + } + + /* All views: header item coverage table entry for ow coverage rate */ + td.headerCovTableEntryLo + { + text-align: right; + color: #000000; + font-family: sans-serif; + font-weight: bold; + white-space: nowrap; + padding-left: 12px; + padding-right: 4px; + background-color: #FF0000; + } + + /* All views: header legend value for legend entry */ + td.headerValueLeg + { + text-align: left; + color: #000000; + font-family: sans-serif; + font-size: 80%; + white-space: nowrap; + padding-top: 4px; + } + /* All views: color of horizontal ruler */ td.ruler { background-color: #6688D4; } - - + /* All views: version string format */ td.versionInfo { - text-align: center; - padding-top: 2px; - font-family: sans-serif; - font-style: italic; + 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: #FFFFFF; + text-align: center; + color: #FFFFFF; background-color: #6688D4; - font-family: sans-serif; - font-size: 120%; - font-weight: bold; + 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: #284FA8; + text-align: left; + padding-left: 10px; + padding-right: 20px; + color: #284FA8; background-color: #DAE7FE; - font-family: monospace; + font-family: monospace; } - - + /* Directory view/File view (all): bar-graph entry format*/ td.coverBar { - padding-left: 10px; - padding-right: 10px; + padding-left: 10px; + padding-right: 10px; background-color: #DAE7FE; } - - + /* Directory view/File view (all): bar-graph outline color */ td.coverBarOutline { background-color: #000000; } - - + /* 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: #DAE7FE; - font-weight: bold; + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #A7FC9D; + font-weight: bold; + font-family: sans-serif; } - - + /* 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: #DAE7FE; + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #A7FC9D; + 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; + text-align: right; + padding-left: 10px; + padding-right: 10px; background-color: #FFEA20; - font-weight: bold; + 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; + text-align: right; + padding-left: 10px; + padding-right: 10px; background-color: #FFEA20; + 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; + text-align: right; + padding-left: 10px; + padding-right: 10px; background-color: #FF0000; - font-weight: bold; + 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; + text-align: right; + padding-left: 10px; + padding-right: 10px; background-color: #FF0000; + white-space: nowrap; + font-family: sans-serif; } - - + /* File view (all): "show/hide details" link format */ a.detail:link { color: #B8D0FF; + font-size:80%; } - - + /* File view (all): "show/hide details" link - visited format */ a.detail:visited { color: #B8D0FF; + font-size:80%; } - - + /* File view (all): "show/hide details" link - activated format */ a.detail:active { color: #FFFFFF; + font-size:80%; } - - - /* File view (detail): test name table headline format */ - td.testNameHead - { - text-align: right; - padding-right: 10px; - background-color: #DAE7FE; - font-family: sans-serif; - font-weight: bold; - } - - - /* File view (detail): test lines table headline format */ - td.testLinesHead - { - text-align: center; - background-color: #DAE7FE; - font-family: sans-serif; - font-weight: bold; - } - - + /* File view (detail): test name entry */ td.testName { - text-align: right; - padding-right: 10px; + text-align: right; + padding-right: 10px; background-color: #DAE7FE; + font-family: sans-serif; } - - + /* File view (detail): test percentage entry */ td.testPer { - text-align: right; - padding-left: 10px; - padding-right: 10px; + text-align: right; + padding-left: 10px; + padding-right: 10px; background-color: #DAE7FE; + font-family: sans-serif; } - - + /* File view (detail): test lines count entry */ td.testNum { - text-align: right; - padding-left: 10px; - padding-right: 10px; + text-align: right; + padding-left: 10px; + padding-right: 10px; background-color: #DAE7FE; + 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; + padding-top: 10px; + padding-left: 30px; + padding-bottom: 10px; + padding-right: 30px; background-color: #DAE7FE; } + + /* Source code view: function entry */ + td.coverFn + { + text-align: left; + padding-left: 10px; + padding-right: 20px; + color: #284FA8; + background-color: #DAE7FE; + font-family: monospace; + } + /* Source code view: function entry zero count*/ + td.coverFnLo + { + text-align: right; + padding-left: 10px; + padding-right: 10px; + background-color: #FF0000; + 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: #DAE7FE; + font-weight: bold; + font-family: sans-serif; + } /* Source code view: source code format */ pre.source { font-family: monospace; white-space: pre; + margin-top: 2px; } - + /* Source code view: line number format */ span.lineNum { background-color: #EFE383; } - - + /* Source code view: format for lines which were executed */ + td.lineCov, span.lineCov { background-color: #CAD7FE; } - - + + /* Source code view: format for Cov legend */ + span.coverLegendCov + { + padding-left: 10px; + padding-right: 10px; + padding-bottom: 2px; + background-color: #CAD7FE; + } + /* Source code view: format for lines which were not executed */ + td.lineNoCov, span.lineNoCov { background-color: #FF6230; } - - + + /* Source code view: format for NoCov legend */ + span.coverLegendNoCov + { + padding-left: 10px; + padding-right: 10px; + padding-bottom: 2px; + background-color: #FF6230; + } + + /* Source code view (function table): standard link - visited format */ + td.lineNoCov > a:visited, + td.lineCov > a:visited + { + color: black; + text-decoration: underline; + } + /* Source code view: format for lines which were executed only in a previous version */ span.lineDiffCov { background-color: #B5F7AF; } + + /* Source code view: format for branches which were executed + * and taken */ + span.branchCov + { + background-color: #CAD7FE; + } + + /* Source code view: format for branches which were executed + * but not taken */ + span.branchNoCov + { + background-color: #FF6230; + } + + /* Source code view: format for branches which were not executed */ + span.branchNoExec + { + background-color: #FF6230; + } + + /* 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: #FF0000; + 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: #FFEA20; + 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: #A7FC9D; + 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: #FF0000; + } + + /* All views except source code view: legend format for med coverage */ + span.coverLegendCovMed + { + padding-left: 10px; + padding-right: 10px; + padding-top: 2px; + background-color: #FFEA20; + } + + /* All views except source code view: legend format for hi coverage */ + span.coverLegendCovHi + { + padding-left: 10px; + padding-right: 10px; + padding-top: 2px; + background-color: #A7FC9D; + } END_OF_CSS ; @@ -1847,9 +3148,8 @@ sub get_bar_graph_code($$$) $remainder = sprintf("%d", 100-$width); # Decide which .png file to use - if ($rate < $med_limit) { $png_name = "ruby.png"; } - elsif ($rate < $hi_limit) { $png_name = "amber.png"; } - else { $png_name = "emerald.png"; } + $png_name = $rate_png[classify_rate($_[1], $_[2], $med_limit, + $hi_limit)]; if ($width == 0) { @@ -1883,6 +3183,29 @@ END_OF_HTML 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) = @_; + my $rate; + + if ($found == 0) { + return 2; + } + $rate = $hit * 100 / $found; + if ($rate < $med) { + return 0; + } elsif ($rate < $hi) { + return 1; + } + return 2; +} + # # write_html(filehandle, html_code) @@ -1914,28 +3237,15 @@ sub write_html(*$) 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], < - - - - - - $pagetitle - - - - - -END_OF_HTML - ; - - # ************************************************************* + write_html($_[0], $prolog); } @@ -1965,54 +3275,40 @@ END_OF_HTML # -# write_header_line(filehandle, item1, value1, [item2, value2]) +# write_header_line(handle, content) # -# Write a header line, containing of either one or two pairs "header item" -# and "header value". +# Write a header line with the specified table contents. # -sub write_header_line(*$$;$$) +sub write_header_line(*@) { - my $item1 = $_[1]; - my $value1 = $_[2]; + my ($handle, @content) = @_; + my $entry; - # Use GOTO to prevent indenting HTML with more than one tabs - if (scalar(@_) > 3) { goto two_items } + write_html($handle, " \n"); + foreach $entry (@content) { + my ($width, $class, $text, $colspan) = @{$entry}; - # ************************************************************* - - write_html($_[0], < - $item1: - $value1 - -END_OF_HTML - ; - - return(); - - # ************************************************************* - - -two_items: - my $item2 = $_[3]; - my $value2 = $_[4]; - - - # ************************************************************* - - write_html($_[0], < - $item1: - $value1 - - $item2: - $value2 - -END_OF_HTML - ; - - # ************************************************************* + if (defined($width)) { + $width = " width=\"$width\""; + } else { + $width = ""; + } + if (defined($class)) { + $class = " class=\"$class\""; + } else { + $class = ""; + } + if (defined($colspan)) { + $colspan = " colspan=\"$colspan\""; + } else { + $colspan = ""; + } + $text = "" if (!defined($text)); + write_html($handle, + " $text\n"); + } + write_html($handle, " \n"); } @@ -2027,9 +3323,11 @@ sub write_header_epilog(*$) # ************************************************************* write_html($_[0], < + @@ -2041,148 +3339,172 @@ END_OF_HTML # -# write_file_table_prolog(filehandle, left_heading, right_heading) +# write_file_table_prolog(handle, file_heading, ([heading, num_cols], ...)) # # Write heading for file table. # -sub write_file_table_prolog(*$$) +sub write_file_table_prolog(*$@) { - # ************************************************************* + my ($handle, $file_heading, @columns) = @_; + my $num_columns = 0; + my $file_width; + my $col; + my $width; - write_html($_[0], < 2); + + foreach $col (@columns) { + my ($heading, $cols) = @{$col}; + + $num_columns += $cols; + } + $file_width = 100 - $num_columns * $width; + + # Table definition + 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 + write_html($handle, < + + + +END_OF_HTML + # Heading row + foreach $col (@columns) { + my ($heading, $cols) = @{$col}; + my $colspan = ""; + + $colspan = " colspan=$cols" if ($cols > 1); + write_html($handle, <$heading +END_OF_HTML + } + write_html($handle, < +END_OF_HTML } -# -# write_file_table_entry(filehandle, cover_filename, cover_bar_graph, -# cover_found, cover_hit) +# write_file_table_entry(handle, base_dir, filename, page_link, +# ([ found, hit, med_limit, hi_limit, graph ], ..) # # Write an entry of the file table. # -sub write_file_table_entry(*$$$$) +sub write_file_table_entry(*$$$@) { - my $rate; - my $rate_string; - my $classification = "Lo"; + my ($handle, $base_dir, $filename, $page_link, @entries) = @_; + my $file_code; + my $entry; - if ($_[3]>0) - { - $rate = $_[4] * 100 / $_[3]; - $rate_string = sprintf("%.1f", $rate)." %"; - - if ($rate < $med_limit) { $classification = "Lo"; } - elsif ($rate < $hi_limit) { $classification = "Med"; } - else { $classification = "Hi"; } - } - else - { - $rate_string = "undefined"; + # Add link to source if provided + if (defined($page_link) && $page_link ne "") { + $file_code = "$filename"; + } else { + $file_code = $filename; } - # ************************************************************* - - write_html($_[0], < - + +END_OF_HTML + # Columns as defined + foreach $entry (@entries) { + my ($found, $hit, $med, $hi, $graph) = @{$entry}; + my $bar_graph; + my $class; + my $rate; + + # Generate bar graph if requested + if ($graph) { + $bar_graph = get_bar_graph_code($base_dir, $found, + $hit); + write_html($handle, < - $_[2] + $bar_graph - - - - END_OF_HTML - ; - - # ************************************************************* + } + # Get rate color and text + if ($found == 0) { + $rate = "-"; + $class = "Hi"; + } else { + $rate = sprintf("%.1f %%", $hit * 100 / $found); + $class = $rate_name[classify_rate($found, $hit, + $med, $hi)]; + } + write_html($handle, <$rate + +END_OF_HTML + } + # End of row + write_html($handle, < +END_OF_HTML } # -# write_file_table_detail_heading(filehandle, left_heading, right_heading) -# -# Write heading for detail section in file table. -# - -sub write_file_table_detail_heading(*$$) -{ - # ************************************************************* - - write_html($_[0], < - - - - -END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_file_table_detail_entry(filehandle, test_name, cover_found, cover_hit) +# write_file_table_detail_entry(filehandle, test_name, ([found, hit], ...)) # # Write entry for detail section in file table. # -sub write_file_table_detail_entry(*$$$) +sub write_file_table_detail_entry(*$@) { - my $rate; - my $name = $_[1]; - - if ($_[2]>0) - { - $rate = sprintf("%.1f", $_[3]*100/$_[2])." %"; - } - else - { - $rate = "undefined"; - } + my ($handle, $test, @entries) = @_; + my $entry; - if ($name =~ /^(.*),diff$/) - { - $name = $1." (converted)"; + if ($test eq "") { + $test = "<unnamed>"; + } elsif ($test =~ /^(.*),diff$/) { + $test = $1." (converted)"; } - - if ($name eq "") - { - $name = "<unnamed>"; - } - - # ************************************************************* - - write_html($_[0], < - + +END_OF_HTML + # Test data + foreach $entry (@entries) { + my ($found, $hit) = @{$entry}; + my $rate = "-"; + + if ($found > 0) { + $rate = sprintf("%.1f %%", $hit * 100 / $found); + } + write_html($handle, <$rate - + +END_OF_HTML + } + + write_html($handle, < END_OF_HTML - ; # ************************************************************* } @@ -2287,6 +3609,17 @@ END_OF_HTML } +sub fmt_centered($$) +{ + my ($width, $text) = @_; + my $w0 = length($text); + my $w1 = int(($width - $w0) / 2); + my $w2 = $width - $w0 - $w1; + + return (" "x$w1).$text.(" "x$w2); +} + + # # write_source_prolog(filehandle) # @@ -2295,6 +3628,15 @@ END_OF_HTML sub write_source_prolog(*) { + my $lineno_heading = " "; + my $branch_heading = ""; + my $line_heading = fmt_centered($line_field_width, "Line data"); + my $source_heading = " Source code"; + + if ($br_coverage) { + $branch_heading = fmt_centered($br_field_width, "Branch data"). + " "; + } # ************************************************************* write_html($_[0], <
- - + +

$_[1]$_[2]

$file_heading$_[1]$file_code$rate_string$_[4] / $_[3] lines
$hit / $found$_[1]$_[2]
$name$test$_[3] / $_[2] lines$hit / $found
+	      
+
${lineno_heading}${branch_heading}${line_heading} ${source_heading}
+
 END_OF_HTML
 	;
 
@@ -2312,50 +3656,251 @@ END_OF_HTML
 
 
 #
-# write_source_line(filehandle, line_num, source, hit_count, converted)
+# get_branch_blocks(brdata)
+#
+# Group branches that belong to the same basic block.
+#
+# Returns: [block1, block2, ...]
+# block:   [branch1, branch2, ...]
+# branch:  [block_num, branch_num, taken_count, text_length, open, close]
+#
+
+sub get_branch_blocks($)
+{
+	my ($brdata) = @_;
+	my $last_block_num;
+	my $block = [];
+	my @blocks;
+	my $i;
+	my $num = br_ivec_len($brdata);
+
+	# Group branches
+	for ($i = 0; $i < $num; $i++) {
+		my ($block_num, $branch, $taken) = br_ivec_get($brdata, $i);
+		my $br;
+
+		if (defined($last_block_num) && $block_num != $last_block_num) {
+			push(@blocks, $block);
+			$block = [];
+		}
+		$br = [$block_num, $branch, $taken, 3, 0, 0];
+		push(@{$block}, $br);
+		$last_block_num = $block_num;
+	}
+	push(@blocks, $block) if (scalar(@{$block}) > 0);
+
+	# Add braces to first and last branch in group
+	foreach $block (@blocks) {
+		$block->[0]->[$BR_OPEN] = 1;
+		$block->[0]->[$BR_LEN]++;
+		$block->[scalar(@{$block}) - 1]->[$BR_CLOSE] = 1;
+		$block->[scalar(@{$block}) - 1]->[$BR_LEN]++;
+	}
+
+	return @blocks;
+}
+
+#
+# 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;
+	my $branch;
+
+	foreach $branch (@{$block}) {
+		$len += $branch->[$BR_LEN];
+	}
+
+	return $len;
+}
+
+
+#
+# get_branch_html(brdata)
+#
+# Return a list of HTML lines which represent the specified branch coverage
+# data in source code view.
+#
+
+sub get_branch_html($)
+{
+	my ($brdata) = @_;
+	my @blocks = get_branch_blocks($brdata);
+	my $block;
+	my $branch;
+	my $line_len = 0;
+	my $line = [];	# [branch2|" ", branch|" ", ...]
+	my @lines;	# [line1, line2, ...]
+	my @result;
+
+	# Distribute blocks to lines
+	foreach $block (@blocks) {
+		my $block_len = get_block_len($block);
+
+		# Does this block fit into the current line?
+		if ($line_len + $block_len <= $br_field_width) {
+			# Add it
+			$line_len += $block_len;
+			push(@{$line}, @{$block});
+			next;
+		} elsif ($block_len <= $br_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 $branch (@{$block}) {
+			if ($line_len + $branch->[$BR_LEN] >= $br_field_width) {
+				# Start a new line
+				if (($line_len + 1 <= $br_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);
+
+	# Convert to HTML
+	foreach $line (@lines) {
+		my $current = "";
+		my $current_len = 0;
+
+		foreach $branch (@$line) {
+			# Skip alignment space
+			if ($branch eq " ") {
+				$current .= " ";
+				$current_len++;
+				next;
+			}
+
+			my ($block_num, $br_num, $taken, $len, $open, $close) =
+			   @{$branch};
+			my $class;
+			my $title;
+			my $text;
+
+			if ($taken eq '-') {
+				$class	= "branchNoExec";
+				$text	= " # ";
+				$title	= "Branch $br_num was not executed";
+			} elsif ($taken == 0) {
+				$class	= "branchNoCov";
+				$text	= " - ";
+				$title	= "Branch $br_num was not taken";
+			} else {
+				$class	= "branchCov";
+				$text	= " + ";
+				$title	= "Branch $br_num was taken $taken ".
+					  "time";
+				$title .= "s" if ($taken > 1);
+			}
+			$current .= "[" if ($open);
+			$current .= "";
+			$current .= $text."";
+			$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;
+}
+
+
+#
+# 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;
+
+	$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");
+	}
+	return $result;
+}
+
+#
+# write_source_line(filehandle, line_num, source, hit_count, converted,
+#                   brdata, add_anchor)
 #
 # Write formatted source code line. Return a line in a format as needed
 # by gen_png()
 #
 
-sub write_source_line(*$$$$)
+sub write_source_line(*$$$$$$)
 {
+	my ($handle, $line, $source, $count, $converted, $brdata,
+	    $add_anchor) = @_;
 	my $source_format;
-	my $count;
+	my $count_format;
 	my $result;
 	my $anchor_start = "";
 	my $anchor_end = "";
+	my $count_field_width = $line_field_width - 1;
+	my @br_html;
+	my $html;
 
-	if (!(defined$_[3]))
-	{
+	# Get branch HTML data for this line
+	@br_html = get_branch_html($brdata) if ($br_coverage);
+
+	if (!defined($count)) {
 		$result		= "";
 		$source_format	= "";
-		$count		= " "x15;
+		$count_format	= " "x$count_field_width;
 	}
-	elsif ($_[3] == 0)
-	{
-		$result		= $_[3];
+	elsif ($count == 0) {
+		$result		= $count;
 		$source_format	= '';
-		$count		= sprintf("%15d", $_[3]);
+		$count_format	= format_count($count, $count_field_width);
 	}
-	elsif ($_[4] && defined($highlight))
-	{
-		$result		= "*".$_[3];
+	elsif ($converted && defined($highlight)) {
+		$result		= "*".$count;
 		$source_format	= '';
-		$count		= sprintf("%15d", $_[3]);
+		$count_format	= format_count($count, $count_field_width);
 	}
-	else
-	{
-		$result		= $_[3];
+	else {
+		$result		= $count;
 		$source_format	= '';
-		$count		= sprintf("%15d", $_[3]);
+		$count_format	= format_count($count, $count_field_width);
 	}
-
-	$result .= ":".$_[2];
+	$result .= ":".$source;
 
 	# Write out a line number navigation anchor every $nav_resolution
 	# lines if necessary
-	if ($frames && (($_[1] - 1) % $nav_resolution == 0))
+	if ($add_anchor)
 	{
 		$anchor_start	= "";
 		$anchor_end	= "";
@@ -2364,13 +3909,23 @@ sub write_source_line(*$$$$)
 
 	# *************************************************************
 
-	write_html($_[0],
-		   $anchor_start.
-		   ''.sprintf("%8d", $_[1]).
-		   " $source_format$count : ".
-		   escape_html($_[2]).($source_format?"":"").
-		   $anchor_end."\n");
+	$html = $anchor_start;
+	$html .= "".sprintf("%8d", $line)." ";
+	$html .= shift(@br_html).":" if ($br_coverage);
+	$html .= "$source_format$count_format : ";
+	$html .= escape_html($source);
+	$html .= "" if ($source_format);
+	$html .= $anchor_end."\n";
 
+	write_html($handle, $html);
+
+	if ($br_coverage) {
+		# Add lines for overlong branch information
+		foreach (@br_html) {
+			write_html($handle, "".
+				   "         $_\n");
+		}
+	}
 	# *************************************************************
 
 	return($result);
@@ -2411,7 +3966,9 @@ END_OF_HTML
 
 sub write_html_epilog(*$;$)
 {
+	my $basedir = $_[1];
 	my $break_code = "";
+	my $epilog;
 
 	if (defined($_[2]))
 	{
@@ -2422,17 +3979,17 @@ sub write_html_epilog(*$;$)
 
 	write_html($_[0], <
-	  
Generated by: $lcov_version
Generated by: $lcov_version

- - - END_OF_HTML ; - # ************************************************************* + $epilog = $html_epilog; + $epilog =~ s/\@basedir\@/$basedir/g; + + write_html($_[0], $epilog); } @@ -2459,8 +4016,8 @@ sub write_frameset(*$$$) - - + + <center>Frames not supported by your browser!<br></center> @@ -2488,7 +4045,7 @@ sub write_overview_line(*$$$) # ************************************************************* write_html($_[0], < + overview END_OF_HTML ; @@ -2551,7 +4108,7 @@ END_OF_HTML
- Top

+ Top

Overview
@@ -2563,16 +4120,36 @@ END_OF_HTML } +# format_rate(found, hit) # -# write_header(filehandle, type, trunc_file_name, rel_file_name, lines_found, -# lines_hit) -# -# Write a complete standard page header. TYPE may be (0, 1, 2, 3) -# corresponding to (directory view header, file view header, source view -# header, test case description header) +# Return formatted percent string for coverage rate. # -sub write_header(*$$$$$) +sub format_rate($$) +{ + return $_[0] == 0 ? "-" : sprintf("%.1f", $_[1] * 100 / $_[0])." %"; +} + + +sub max($$) +{ + my ($a, $b) = @_; + + return $a if ($a > $b); + return $b; +} + + +# +# write_header(filehandle, type, trunc_file_name, rel_file_name, lines_found, +# lines_hit, funcs_found, funcs_hit, sort_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) +# + +sub write_header(*$$$$$$$$$$) { local *HTML_HANDLE = $_[0]; my $type = $_[1]; @@ -2580,68 +4157,77 @@ sub write_header(*$$$$$) my $rel_filename = $_[3]; my $lines_found = $_[4]; my $lines_hit = $_[5]; + my $fn_found = $_[6]; + my $fn_hit = $_[7]; + my $br_found = $_[8]; + my $br_hit = $_[9]; + my $sort_type = $_[10]; my $base_dir; my $view; my $test; - my $rate; my $base_name; - - # Calculate coverage rate - if ($lines_found>0) - { - $rate = sprintf("%.1f", $lines_hit * 100 / $lines_found)." %"; - } - else - { - $rate = "-"; - } + my $style; + my $rate; + my @row_left; + my @row_right; + my $num_rows; + my $i; $base_name = basename($rel_filename); # Prepare text for "current view" field - if ($type == 0) + if ($type == $HDR_DIR) { # Main overview $base_dir = ""; $view = $overview_title; } - elsif ($type == 1) + elsif ($type == $HDR_FILE) { # Directory overview $base_dir = get_relative_base_path($rel_filename); - $view = "". + $view = "". "$overview_title - $trunc_name"; } - elsif ($type == 2) + elsif ($type == $HDR_SOURCE || $type == $HDR_FUNC) { # File view my $dir_name = dirname($rel_filename); - - $base_dir = get_relative_base_path($dir_name); if ($frames) { # Need to break frameset when clicking any of these # links - $view = "$overview_title - ". - "". + "". "$dir_name - $base_name"; } else { - $view = "". + $view = "". "$overview_title - ". - "". + "". "$dir_name - $base_name"; } + + # Add function suffix + if ($func_coverage) { + $view .= ""; + if ($type == $HDR_SOURCE) { + $view .= " (source / functions)"; + } elsif ($type == $HDR_FUNC) { + $view .= " (source / functions)"; + } + $view .= ""; + } } - elsif ($type == 3) + elsif ($type == $HDR_TESTDESC) { # Test description header $base_dir = ""; - $view = "". + $view = "". "$overview_title - test case descriptions"; } @@ -2649,60 +4235,265 @@ sub write_header(*$$$$$) $test = escape_html($test_title); # Append link to test description page if available - if (%test_description && ($type != 3)) + if (%test_description && ($type != $HDR_TESTDESC)) { - if ($frames && ($type == 2)) + if ($frames && ($type == $HDR_SOURCE || $type == $HDR_FUNC)) { # Need to break frameset when clicking this link - $test .= " ( ". - "view test case descriptions )"; + $test .= " ( ". + "". + "view descriptions )"; } else { - $test .= " ( ". - "view test case descriptions )"; + $test .= " ( ". + "". + "view descriptions )"; } } # Write header write_header_prolog(*HTML_HANDLE, $base_dir); - write_header_line(*HTML_HANDLE, "Current view", $view); - write_header_line(*HTML_HANDLE, "Test", $test); - write_header_line(*HTML_HANDLE, "Date", $date, - "Instrumented lines", $lines_found); - write_header_line(*HTML_HANDLE, "Code covered", $rate, - "Executed lines", $lines_hit); + + # Left row + push(@row_left, [[ "10%", "headerItem", "Current view:" ], + [ "35%", "headerValue", $view ]]); + push(@row_left, [[undef, "headerItem", "Test:"], + [undef, "headerValue", $test]]); + push(@row_left, [[undef, "headerItem", "Date:"], + [undef, "headerValue", $date]]); + + # Right row + if ($legend && ($type == $HDR_SOURCE || $type == $HDR_FUNC)) { + my $text = <hit + not hit +END_OF_HTML + if ($br_coverage) { + $text .= <+ taken + - not taken + # not executed +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]]); + } + if ($type == $HDR_TESTDESC) { + push(@row_right, [[ "55%" ]]); + } else { + push(@row_right, [["15%", undef, undef ], + ["10%", "headerCovTableHead", "Hit" ], + ["10%", "headerCovTableHead", "Total" ], + ["15%", "headerCovTableHead", "Coverage"]]); + } + # Line coverage + $style = $rate_name[classify_rate($lines_found, $lines_hit, + $med_limit, $hi_limit)]; + $rate = format_rate($lines_found, $lines_hit); + push(@row_right, [[undef, "headerItem", "Lines:"], + [undef, "headerCovTableEntry", $lines_hit], + [undef, "headerCovTableEntry", $lines_found], + [undef, "headerCovTableEntry$style", $rate]]) + if ($type != $HDR_TESTDESC); + # Function coverage + if ($func_coverage) { + $style = $rate_name[classify_rate($fn_found, $fn_hit, + $fn_med_limit, $fn_hi_limit)]; + $rate = format_rate($fn_found, $fn_hit); + push(@row_right, [[undef, "headerItem", "Functions:"], + [undef, "headerCovTableEntry", $fn_hit], + [undef, "headerCovTableEntry", $fn_found], + [undef, "headerCovTableEntry$style", $rate]]) + if ($type != $HDR_TESTDESC); + } + # Branch coverage + if ($br_coverage) { + $style = $rate_name[classify_rate($br_found, $br_hit, + $br_med_limit, $br_hi_limit)]; + $rate = format_rate($br_found, $br_hit); + push(@row_right, [[undef, "headerItem", "Branches:"], + [undef, "headerCovTableEntry", $br_hit], + [undef, "headerCovTableEntry", $br_found], + [undef, "headerCovTableEntry$style", $rate]]) + if ($type != $HDR_TESTDESC); + } + + # Print rows + $num_rows = max(scalar(@row_left), scalar(@row_right)); + for ($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); } # -# split_filename(filename) -# -# Return (path, filename, extension) for a given FILENAME. +# get_sorted_keys(hash_ref, sort_type) # -sub split_filename($) +sub get_sorted_keys($$) { - if (!$_[0]) { return(); } - my @path_components = split('/', $_[0]); - my @file_components = split('\.', pop(@path_components)); - my $extension = pop(@file_components); + my ($hash, $type) = @_; - return (join("/",@path_components), join(".",@file_components), - $extension); + if ($type == $SORT_FILE) { + # Sort by name + return sort(keys(%{$hash})); + } elsif ($type == $SORT_LINE) { + # Sort by line coverage + return sort({$hash->{$a}[7] <=> $hash->{$b}[7]} keys(%{$hash})); + } elsif ($type == $SORT_FUNC) { + # Sort by function coverage; + return sort({$hash->{$a}[8] <=> $hash->{$b}[8]} keys(%{$hash})); + } elsif ($type == $SORT_BRANCH) { + # Sort by br coverage; + return sort({$hash->{$a}[9] <=> $hash->{$b}[9]} keys(%{$hash})); + } } +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 = ""; + } + + return ' '.$link_start. + ''.$link_end.''; +} + +sub get_file_code($$$$) +{ + my ($type, $text, $sort_button, $base) = @_; + my $result = $text; + my $link; + + if ($sort_button) { + if ($type == $HEAD_NO_DETAIL) { + $link = "index.$html_ext"; + } else { + $link = "index-detail.$html_ext"; + } + } + $result .= get_sort_code($link, "Sort by name", $base); + + return $result; +} + +sub get_line_code($$$$$) +{ + my ($type, $sort_type, $text, $sort_button, $base) = @_; + my $result = $text; + my $sort_link; + + if ($type == $HEAD_NO_DETAIL) { + # Just text + if ($sort_button) { + $sort_link = "index-sort-l.$html_ext"; + } + } elsif ($type == $HEAD_DETAIL_HIDDEN) { + # Text + link to detail view + $result .= ' ( show details )'; + if ($sort_button) { + $sort_link = "index-sort-l.$html_ext"; + } + } else { + # Text + link to standard view + $result .= ' ( hide details )'; + if ($sort_button) { + $sort_link = "index-detail-sort-l.$html_ext"; + } + } + # Add sort button + $result .= get_sort_code($sort_link, "Sort by line coverage", $base); + + return $result; +} + +sub get_func_code($$$$) +{ + my ($type, $text, $sort_button, $base) = @_; + my $result = $text; + my $link; + + if ($sort_button) { + if ($type == $HEAD_NO_DETAIL) { + $link = "index-sort-f.$html_ext"; + } else { + $link = "index-detail-sort-f.$html_ext"; + } + } + $result .= get_sort_code($link, "Sort by function coverage", $base); + return $result; +} + +sub get_br_code($$$$) +{ + my ($type, $text, $sort_button, $base) = @_; + my $result = $text; + my $link; + + if ($sort_button) { + if ($type == $HEAD_NO_DETAIL) { + $link = "index-sort-b.$html_ext"; + } else { + $link = "index-detail-sort-b.$html_ext"; + } + } + $result .= get_sort_code($link, "Sort by branch coverage", $base); + return $result; +} # -# write_file_table(filehandle, base_dir, overview, testhash, fileview) +# write_file_table(filehandle, base_dir, overview, testhash, testfnchash, +# testbrhash, fileview, sort_type) # # Write a complete file table. OVERVIEW is a reference to a hash containing # the following mapping: # -# filename -> "lines_found,lines_hit,page_link" +# filename -> "lines_found,lines_hit,funcs_found,funcs_hit,page_link, +# func_link" # # TESTHASH is a reference to the following hash: # @@ -2714,95 +4505,138 @@ sub split_filename($) # otherwise. # -sub write_file_table(*$$$$) +sub write_file_table(*$$$$$$$) { local *HTML_HANDLE = $_[0]; my $base_dir = $_[1]; - my %overview = %{$_[2]}; - my %testhash = %{$_[3]}; - my $fileview = $_[4]; + my $overview = $_[2]; + my $testhash = $_[3]; + my $testfnchash = $_[4]; + my $testbrhash = $_[5]; + my $fileview = $_[6]; + my $sort_type = $_[7]; my $filename; my $bar_graph; my $hit; my $found; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; my $page_link; my $testname; my $testdata; - my $testcount; + my $testfncdata; + my $testbrdata; my %affecting_tests; - my $coverage_heading = "Coverage"; + my $line_code = ""; + my $func_code; + my $br_code; + my $file_code; + my @head_columns; - # Provide a link to details/non-detail list if this is directory - # overview and we are supposed to create a detail view + # Determine HTML code for column headings if (($base_dir ne "") && $show_details) { - if (%testhash) - { - # This is the detail list, provide link to standard - # list - $coverage_heading .= " ( hide ". - "details )"; - } - else - { - # This is the standard list, provide link to detail - # list - $coverage_heading .= " ( show ". - "details )"; - } + my $detailed = keys(%{$testhash}); + + $file_code = get_file_code($detailed ? $HEAD_DETAIL_HIDDEN : + $HEAD_NO_DETAIL, + $fileview ? "Filename" : "Directory", + $sort && $sort_type != $SORT_FILE, + $base_dir); + $line_code = get_line_code($detailed ? $HEAD_DETAIL_SHOWN : + $HEAD_DETAIL_HIDDEN, + $sort_type, + "Line Coverage", + $sort && $sort_type != $SORT_LINE, + $base_dir); + $func_code = get_func_code($detailed ? $HEAD_DETAIL_HIDDEN : + $HEAD_NO_DETAIL, + "Functions", + $sort && $sort_type != $SORT_FUNC, + $base_dir); + $br_code = get_br_code($detailed ? $HEAD_DETAIL_HIDDEN : + $HEAD_NO_DETAIL, + "Branches", + $sort && $sort_type != $SORT_BRANCH, + $base_dir); + } else { + $file_code = get_file_code($HEAD_NO_DETAIL, + $fileview ? "Filename" : "Directory", + $sort && $sort_type != $SORT_FILE, + $base_dir); + $line_code = get_line_code($HEAD_NO_DETAIL, $sort_type, "Line Coverage", + $sort && $sort_type != $SORT_LINE, + $base_dir); + $func_code = get_func_code($HEAD_NO_DETAIL, "Functions", + $sort && $sort_type != $SORT_FUNC, + $base_dir); + $br_code = get_br_code($HEAD_NO_DETAIL, "Branches", + $sort && $sort_type != $SORT_BRANCH, + $base_dir); } + push(@head_columns, [ $line_code, 3 ]); + push(@head_columns, [ $func_code, 2]) if ($func_coverage); + push(@head_columns, [ $br_code, 2]) if ($br_coverage); - write_file_table_prolog(*HTML_HANDLE, - $fileview ? "Filename" : "Directory name", - $coverage_heading); + write_file_table_prolog(*HTML_HANDLE, $file_code, @head_columns); - foreach $filename (sort(keys(%overview))) + foreach $filename (get_sorted_keys($overview, $sort_type)) { - ($found, $hit, $page_link) = split(",", $overview{$filename}); - $bar_graph = get_bar_graph_code($base_dir, $found, $hit); + my @columns; + ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, + $page_link) = @{$overview->{$filename}}; - $testdata = $testhash{$filename}; - - # Add anchor tag in case a page link is provided - if ($page_link) - { - $filename = "$filename"; + # Line coverage + push(@columns, [$found, $hit, $med_limit, $hi_limit, 1]); + # Function coverage + if ($func_coverage) { + push(@columns, [$fn_found, $fn_hit, $fn_med_limit, + $fn_hi_limit, 0]); } + # Branch coverage + if ($br_coverage) { + push(@columns, [$br_found, $br_hit, $br_med_limit, + $br_hi_limit, 0]); + } + write_file_table_entry(*HTML_HANDLE, $base_dir, $filename, + $page_link, @columns); - write_file_table_entry(*HTML_HANDLE, $filename, $bar_graph, - $found, $hit); + $testdata = $testhash->{$filename}; + $testfncdata = $testfnchash->{$filename}; + $testbrdata = $testbrhash->{$filename}; # Check whether we should write test specific coverage # as well if (!($show_details && $testdata)) { next; } # Filter out those tests that actually affect this file - %affecting_tests = %{ get_affecting_tests($testdata) }; + %affecting_tests = %{ get_affecting_tests($testdata, + $testfncdata, $testbrdata) }; # Does any of the tests affect this file at all? if (!%affecting_tests) { next; } - # Write test details for this entry - write_file_table_detail_heading(*HTML_HANDLE, "Test name", - "Lines hit"); - foreach $testname (keys(%affecting_tests)) { - ($found, $hit) = + my @results; + ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit) = split(",", $affecting_tests{$testname}); # Insert link to description of available if ($test_description{$testname}) { $testname = "". + "descriptions.$html_ext#$testname\">". "$testname"; } + push(@results, [$found, $hit]); + push(@results, [$fn_found, $fn_hit]) if ($func_coverage); + push(@results, [$br_found, $br_hit]) if ($br_coverage); write_file_table_detail_entry(*HTML_HANDLE, $testname, - $found, $hit); + @results); } } @@ -2839,33 +4673,248 @@ sub get_found_and_hit($) # -# get_affecting_tests(hashref) +# get_func_found_and_hit(sumfnccount) +# +# Return (f_found, f_hit) for sumfnccount +# + +sub get_func_found_and_hit($) +{ + my ($sumfnccount) = @_; + my $function; + my $fn_found; + my $fn_hit; + + $fn_found = scalar(keys(%{$sumfnccount})); + $fn_hit = 0; + foreach $function (keys(%{$sumfnccount})) { + if ($sumfnccount->{$function} > 0) { + $fn_hit++; + } + } + return ($fn_found, $fn_hit); +} + + +# +# br_taken_to_num(taken) +# +# Convert a branch taken value .info format to number format. +# + +sub br_taken_to_num($) +{ + my ($taken) = @_; + + return 0 if ($taken eq '-'); + return $taken + 1; +} + + +# +# br_num_to_taken(taken) +# +# Convert a branch taken value in number format to .info format. +# + +sub br_num_to_taken($) +{ + my ($taken) = @_; + + return '-' if ($taken == 0); + return $taken - 1; +} + + +# +# br_taken_add(taken1, taken2) +# +# Return the result of taken1 + taken2 for 'branch taken' values. +# + +sub br_taken_add($$) +{ + my ($t1, $t2) = @_; + + return $t1 if (!defined($t2)); + return $t2 if (!defined($t1)); + return $t1 if ($t2 eq '-'); + return $t2 if ($t1 eq '-'); + return $t1 + $t2; +} + + +# +# br_taken_sub(taken1, taken2) +# +# Return the result of taken1 - taken2 for 'branch taken' values. Return 0 +# if the result would become negative. +# + +sub br_taken_sub($$) +{ + my ($t1, $t2) = @_; + + return $t1 if (!defined($t2)); + return undef if (!defined($t1)); + return $t1 if ($t1 eq '-'); + return $t1 if ($t2 eq '-'); + return 0 if $t2 > $t1; + return $t1 - $t2; +} + + +# +# br_ivec_len(vector) +# +# Return the number of entries in the branch coverage vector. +# + +sub br_ivec_len($) +{ + my ($vec) = @_; + + return 0 if (!defined($vec)); + return (length($vec) * 8 / $BR_VEC_WIDTH) / $BR_VEC_ENTRIES; +} + + +# +# br_ivec_get(vector, number) +# +# Return an entry from the branch coverage vector. +# + +sub br_ivec_get($$) +{ + my ($vec, $num) = @_; + my $block; + my $branch; + my $taken; + my $offset = $num * $BR_VEC_ENTRIES; + + # Retrieve data from vector + $block = vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH); + $branch = vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH); + $taken = vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH); + + # Decode taken value from an integer + $taken = br_num_to_taken($taken); + + return ($block, $branch, $taken); +} + + +# +# br_ivec_push(vector, block, branch, taken) +# +# Add an entry to the branch coverage vector. If an entry with the same +# branch ID already exists, add the corresponding taken values. +# + +sub br_ivec_push($$$$) +{ + my ($vec, $block, $branch, $taken) = @_; + my $offset; + my $num = br_ivec_len($vec); + my $i; + + $vec = "" if (!defined($vec)); + + # Check if branch already exists in vector + for ($i = 0; $i < $num; $i++) { + my ($v_block, $v_branch, $v_taken) = br_ivec_get($vec, $i); + + next if ($v_block != $block || $v_branch != $branch); + + # Add taken counts + $taken = br_taken_add($taken, $v_taken); + last; + } + + $offset = $i * $BR_VEC_ENTRIES; + $taken = br_taken_to_num($taken); + + # Add to vector + vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH) = $block; + vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH) = $branch; + vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH) = $taken; + + return $vec; +} + + +# +# get_br_found_and_hit(sumbrcount) +# +# Return (br_found, br_hit) for sumbrcount +# + +sub get_br_found_and_hit($) +{ + my ($sumbrcount) = @_; + my $line; + my $br_found = 0; + my $br_hit = 0; + + foreach $line (keys(%{$sumbrcount})) { + my $brdata = $sumbrcount->{$line}; + my $i; + my $num = br_ivec_len($brdata); + + for ($i = 0; $i < $num; $i++) { + my $taken; + + (undef, undef, $taken) = br_ivec_get($brdata, $i); + + $br_found++; + $br_hit++ if ($taken ne "-" && $taken > 0); + } + } + + return ($br_found, $br_hit); +} + + +# +# get_affecting_tests(testdata, testfncdata, testbrdata) # # 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($) +sub get_affecting_tests($$$) { - my %hash = %{$_[0]}; + my ($testdata, $testfncdata, $testbrdata) = @_; my $testname; my $testcount; + my $testfnccount; + my $testbrcount; my %result; my $found; my $hit; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; - foreach $testname (keys(%hash)) + foreach $testname (keys(%{$testdata})) { # Get (line number -> count) hash for this test case - $testcount = $hash{$testname}; + $testcount = $testdata->{$testname}; + $testfnccount = $testfncdata->{$testname}; + $testbrcount = $testbrdata->{$testname}; # Calculate sum ($found, $hit) = get_found_and_hit($testcount); + ($fn_found, $fn_hit) = get_func_found_and_hit($testfnccount); + ($br_found, $br_hit) = get_br_found_and_hit($testbrcount); if ($hit>0) { - $result{$testname} = "$found,$hit"; + $result{$testname} = "$found,$hit,$fn_found,$fn_hit,". + "$br_found,$br_hit"; } } @@ -2873,9 +4922,21 @@ sub get_affecting_tests($) } +sub get_hash_reverse($) +{ + my ($hash) = @_; + my %result; + + foreach (keys(%{$hash})) { + $result{$hash->{$_}} = $_; + } + + return \%result; +} + # # write_source(filehandle, source_filename, count_data, checksum_data, -# converted_data) +# converted_data, func_data, sumbrcount) # # Write an HTML view of a source code file. Returns a list containing # data as needed by gen_png(). @@ -2883,7 +4944,7 @@ sub get_affecting_tests($) # Die on error. # -sub write_source($$$$$) +sub write_source($$$$$$$) { local *HTML_HANDLE = $_[0]; local *SOURCE_HANDLE; @@ -2893,6 +4954,10 @@ sub write_source($$$$$) my @result; my $checkdata = $_[3]; my $converted = $_[4]; + my $funcdata = $_[5]; + my $sumbrcount = $_[6]; + my $datafunc = get_hash_reverse($funcdata); + my $add_anchor; if ($_[2]) { @@ -2908,6 +4973,9 @@ sub write_source($$$$$) { chomp($_); + # Also remove CR from line-end + s/\015$//; + # Source code matches coverage data? if (defined($checkdata->{$line_number}) && ($checkdata->{$line_number} ne md5_base64($_))) @@ -2916,10 +4984,25 @@ sub write_source($$$$$) "$line_number\n"); } + $add_anchor = 0; + if ($frames) { + if (($line_number - 1) % $nav_resolution == 0) { + $add_anchor = 1; + } + } + if ($func_coverage) { + if ($line_number == 1) { + $add_anchor = 1; + } elsif (defined($datafunc->{$line_number + + $func_offset})) { + $add_anchor = 1; + } + } push (@result, write_source_line(HTML_HANDLE, $line_number, $_, $count_data{$line_number}, - $converted->{$line_number})); + $converted->{$line_number}, + $sumbrcount->{$line_number}, $add_anchor)); } close(SOURCE_HANDLE); @@ -2928,6 +5011,141 @@ sub write_source($$$$$) } +sub funcview_get_func_code($$$) +{ + my ($name, $base, $type) = @_; + my $result; + my $link; + + if ($sort && $type == 1) { + $link = "$name.func.$html_ext"; + } + $result = "Function Name"; + $result .= get_sort_code($link, "Sort by function name", $base); + + return $result; +} + +sub funcview_get_count_code($$$) +{ + my ($name, $base, $type) = @_; + my $result; + my $link; + + if ($sort && $type == 0) { + $link = "$name.func-sort-c.$html_ext"; + } + $result = "Hit count"; + $result .= get_sort_code($link, "Sort by hit count", $base); + + return $result; +} + +# +# funcview_get_sorted(funcdata, sumfncdata, sort_type) +# +# 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, $sumfncdata, $type) = @_; + + if ($type == 0) { + return sort(keys(%{$funcdata})); + } + return sort({$sumfncdata->{$b} <=> $sumfncdata->{$a}} + keys(%{$sumfncdata})); +} + +# +# write_function_table(filehandle, source_file, sumcount, funcdata, +# sumfnccount, testfncdata, sumbrcount, testbrdata, +# 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 = $_[0]; + my $source = $_[1]; + my $sumcount = $_[2]; + my $funcdata = $_[3]; + my $sumfncdata = $_[4]; + my $testfncdata = $_[5]; + my $sumbrcount = $_[6]; + my $testbrdata = $_[7]; + my $name = $_[8]; + my $base = $_[9]; + my $type = $_[10]; + my $func; + my $func_code; + my $count_code; + + # Get HTML code for headings + $func_code = funcview_get_func_code($name, $base, $type); + $count_code = funcview_get_count_code($name, $base, $type); + write_html(*HTML_HANDLE, < + + + + + + +END_OF_HTML + ; + + # Get a sorted table + foreach $func (funcview_get_sorted($funcdata, $sumfncdata, $type)) { + if (!defined($funcdata->{$func})) + { + next; + } + + my $startline = $funcdata->{$func} - $func_offset; + my $name = $func; + my $count = $sumfncdata->{$name}; + my $countstyle; + + # Demangle C++ function names if requested + if ($demangle_cpp) { + $name = `c++filt "$name"`; + chomp($name); + } + # Escape any remaining special characters + $name = escape_html($name); + if ($startline < 1) { + $startline = 1; + } + if ($count == 0) { + $countstyle = "coverFnLo"; + } else { + $countstyle = "coverFnHi"; + } + + write_html(*HTML_HANDLE, < + + + +END_OF_HTML + ; + } + write_html(*HTML_HANDLE, < +
+ +END_OF_HTML + ; +} + + # # info(printf_parameter) # @@ -2982,57 +5200,45 @@ sub subtract_counts($$) # -# add_counts(data1_ref, data2_ref) +# subtract_fnccounts(data, base) # -# DATA1_REF and DATA2_REF are references to hashes containing a mapping -# -# line number -> execution count -# -# Return a list (RESULT_REF, LINES_FOUND, LINES_HIT) where RESULT_REF -# is a reference to a hash containing the combined mapping in which -# execution counts are added. +# Subtract function call counts found in base from those in data. +# Return (data, f_found, f_hit). # -sub add_counts($$) +sub subtract_fnccounts($$) { - my %data1 = %{$_[0]}; # Hash 1 - my %data2 = %{$_[1]}; # Hash 2 - my %result; # Resulting hash - my $line; # Current line iteration scalar - my $data1_count; # Count of line in hash1 - my $data2_count; # Count of line in hash2 - my $found = 0; # Total number of lines found - my $hit = 0; # Number of lines with a count > 0 + my %data; + my %base; + my $func; + my $data_count; + my $base_count; + my $fn_hit = 0; + my $fn_found = 0; - foreach $line (keys(%data1)) - { - $data1_count = $data1{$line}; - $data2_count = $data2{$line}; + %data = %{$_[0]} if (defined($_[0])); + %base = %{$_[1]} if (defined($_[1])); + foreach $func (keys(%data)) { + $fn_found++; + $data_count = $data{$func}; + $base_count = $base{$func}; - # Add counts if present in both hashes - if (defined($data2_count)) { $data1_count += $data2_count; } + if (defined($base_count)) { + $data_count -= $base_count; - # Store sum in %result - $result{$line} = $data1_count; + # Make sure we don't get negative numbers + if ($data_count < 0) { + $data_count = 0; + } + } - $found++; - if ($data1_count > 0) { $hit++; } + $data{$func} = $data_count; + if ($data_count > 0) { + $fn_hit++; + } } - # Add lines unique to data2 - foreach $line (keys(%data2)) - { - # Skip lines already in data1 - if (defined($data1{$line})) { next; } - - # Copy count from data2 - $result{$line} = $data2{$line}; - - $found++; - if ($result{$line} > 0) { $hit++; } - } - - return (\%result, $found, $hit); + return (\%data, $fn_found, $fn_hit); } @@ -3053,14 +5259,25 @@ sub apply_baseline($$) my $data_testdata; my $data_funcdata; my $data_checkdata; + my $data_testfncdata; + my $data_testbrdata; my $data_count; + my $data_testfnccount; + my $data_testbrcount; my $base; - my $base_testdata; my $base_checkdata; + my $base_sumfnccount; + my $base_sumbrcount; my $base_count; my $sumcount; + my $sumfnccount; + my $sumbrcount; my $found; my $hit; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; foreach $filename (keys(%data_hash)) { @@ -3068,50 +5285,80 @@ sub apply_baseline($$) $data = $data_hash{$filename}; $base = $base_hash{$filename}; + # Skip data entries for which no base entry exists + if (!defined($base)) + { + next; + } + # Get set entries for data and baseline - ($data_testdata, undef, $data_funcdata, $data_checkdata) = + ($data_testdata, undef, $data_funcdata, $data_checkdata, + $data_testfncdata, undef, $data_testbrdata) = get_info_entry($data); - ($base_testdata, $base_count, undef, $base_checkdata) = + (undef, $base_count, undef, $base_checkdata, undef, + $base_sumfnccount, undef, $base_sumbrcount) = get_info_entry($base); # Check for compatible checksums merge_checksums($data_checkdata, $base_checkdata, $filename); - # Sumcount has to be calculated anew + # sumcount has to be calculated anew $sumcount = {}; + $sumfnccount = {}; + $sumbrcount = {}; # For each test case, subtract test specific counts foreach $testname (keys(%{$data_testdata})) { # Get counts of both data and baseline $data_count = $data_testdata->{$testname}; - - $hit = 0; + $data_testfnccount = $data_testfncdata->{$testname}; + $data_testbrcount = $data_testbrdata->{$testname}; ($data_count, undef, $hit) = subtract_counts($data_count, $base_count); + ($data_testfnccount) = + subtract_fnccounts($data_testfnccount, + $base_sumfnccount); + ($data_testbrcount) = + combine_brcount($data_testbrcount, + $base_sumbrcount, $BR_SUB); + # Check whether this test case did hit any line at all if ($hit > 0) { # Write back resulting hash $data_testdata->{$testname} = $data_count; + $data_testfncdata->{$testname} = + $data_testfnccount; + $data_testbrdata->{$testname} = + $data_testbrcount; } else { # Delete test case which did not impact this # file delete($data_testdata->{$testname}); + delete($data_testfncdata->{$testname}); + delete($data_testbrdata->{$testname}); } # Add counts to sum of counts ($sumcount, $found, $hit) = add_counts($sumcount, $data_count); + ($sumfnccount, $fn_found, $fn_hit) = + add_fnccount($sumfnccount, $data_testfnccount); + ($sumbrcount, $br_found, $br_hit) = + combine_brcount($sumbrcount, $data_testbrcount, + $BR_ADD); } # Write back resulting entry - set_info_entry($data, $data_testdata, $sumcount, - $data_funcdata, $data_checkdata, $found, $hit); + set_info_entry($data, $data_testdata, $sumcount, $data_funcdata, + $data_checkdata, $data_testfncdata, $sumfnccount, + $data_testbrdata, $sumbrcount, $found, $hit, + $fn_found, $fn_hit, $br_found, $br_hit); $data_hash{$filename} = $data; } @@ -3164,179 +5411,6 @@ sub remove_unused_descriptions() } -# -# merge_checksums(ref1, ref2, filename) -# -# REF1 and REF2 are references to hashes containing a mapping -# -# line number -> checksum -# -# Merge checksum lists defined in REF1 and REF2 and return reference to -# resulting hash. Die if a checksum for a line is defined in both hashes -# but does not match. -# - -sub merge_checksums($$$) -{ - my $ref1 = $_[0]; - my $ref2 = $_[1]; - my $filename = $_[2]; - my %result; - my $line; - - foreach $line (keys(%{$ref1})) - { - if (defined($ref2->{$line}) && - ($ref1->{$line} ne $ref2->{$line})) - { - die("ERROR: checksum mismatch at $filename:$line\n"); - } - $result{$line} = $ref1->{$line}; - } - - foreach $line (keys(%{$ref2})) - { - $result{$line} = $ref2->{$line}; - } - - return \%result; -} - - -# -# combine_info_entries(entry_ref1, entry_ref2, filename) -# -# Combine .info data entry hashes referenced by ENTRY_REF1 and ENTRY_REF2. -# Return reference to resulting hash. -# - -sub combine_info_entries($$$) -{ - my $entry1 = $_[0]; # Reference to hash containing first entry - my $testdata1; - my $sumcount1; - my $funcdata1; - my $checkdata1; - - my $entry2 = $_[1]; # Reference to hash containing second entry - my $testdata2; - my $sumcount2; - my $funcdata2; - my $checkdata2; - - my %result; # Hash containing combined entry - my %result_testdata; - my $result_sumcount = {}; - my %result_funcdata; - my $lines_found; - my $lines_hit; - - my $testname; - my $filename = $_[2]; - - # Retrieve data - ($testdata1, $sumcount1, $funcdata1, $checkdata1) = - get_info_entry($entry1); - ($testdata2, $sumcount2, $funcdata2, $checkdata2) = - get_info_entry($entry2); - - # Merge checksums - $checkdata1 = merge_checksums($checkdata1, $checkdata2, $filename); - - # Combine funcdata - foreach (keys(%{$funcdata1})) - { - $result_funcdata{$_} = $funcdata1->{$_}; - } - - foreach (keys(%{$funcdata2})) - { - $result_funcdata{$_} = $funcdata2->{$_}; - } - - # Combine testdata - foreach $testname (keys(%{$testdata1})) - { - if (defined($testdata2->{$testname})) - { - # testname is present in both entries, requires - # combination - ($result_testdata{$testname}) = - add_counts($testdata1->{$testname}, - $testdata2->{$testname}); - } - else - { - # testname only present in entry1, add to result - $result_testdata{$testname} = $testdata1->{$testname}; - } - - # update sum count hash - ($result_sumcount, $lines_found, $lines_hit) = - add_counts($result_sumcount, - $result_testdata{$testname}); - } - - foreach $testname (keys(%{$testdata2})) - { - # Skip testnames already covered by previous iteration - if (defined($testdata1->{$testname})) { next; } - - # testname only present in entry2, add to result hash - $result_testdata{$testname} = $testdata2->{$testname}; - - # update sum count hash - ($result_sumcount, $lines_found, $lines_hit) = - add_counts($result_sumcount, - $result_testdata{$testname}); - } - - # Calculate resulting sumcount - - # Store result - set_info_entry(\%result, \%result_testdata, $result_sumcount, - \%result_funcdata, $checkdata1, $lines_found, - $lines_hit); - - return(\%result); -} - - -# -# combine_info_files(info_ref1, info_ref2) -# -# Combine .info data in hashes referenced by INFO_REF1 and INFO_REF2. Return -# reference to resulting hash. -# - -sub combine_info_files($$) -{ - my %hash1 = %{$_[0]}; - my %hash2 = %{$_[1]}; - my $filename; - - foreach $filename (keys(%hash2)) - { - if ($hash1{$filename}) - { - # Entry already exists in hash1, combine them - $hash1{$filename} = - combine_info_entries($hash1{$filename}, - $hash2{$filename}, - $filename); - } - else - { - # Entry is unique in both hashes, simply add to - # resulting hash - $hash1{$filename} = $hash2{$filename}; - } - } - - return(\%hash1); -} - - # # apply_prefix(filename, prefix) # @@ -3387,7 +5461,7 @@ sub system_no_output($@) # Redirect to /dev/null ($mode & 1) && open(STDOUT, ">/dev/null"); ($mode & 2) && open(STDERR, ">/dev/null"); - + system(@_); $result = $?; @@ -3398,7 +5472,7 @@ sub system_no_output($@) # Restore old handles ($mode & 1) && open(STDOUT, ">>&OLD_STDOUT"); ($mode & 2) && open(STDERR, ">>&OLD_STDERR"); - + return $result; } @@ -3473,3 +5547,102 @@ sub apply_config($) } } } + + +# +# 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("ERROR: cannot open html prolog $filename!\n"); + while () + { + $result .= $_; + } + close(HANDLE); + } + 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("ERROR: cannot open html epilog $filename!\n"); + while () + { + $result .= $_; + } + close(HANDLE); + } + else + { + $result = < + +END_OF_HTML + ; + } + + return $result; + +} + +sub warn_handler($) +{ + my ($msg) = @_; + + warn("$tool_name: $msg"); +} + +sub die_handler($) +{ + my ($msg) = @_; + + die("$tool_name: $msg"); +} diff --git a/utils/lcov/geninfo b/utils/lcov/geninfo index 9e2faec6b..dcb1a6781 100755 --- a/utils/lcov/geninfo +++ b/utils/lcov/geninfo @@ -1,6 +1,6 @@ #!/usr/bin/perl -w # -# Copyright (c) International Business Machines Corp., 2002 +# Copyright (c) International Business Machines Corp., 2002,2010 # # 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 @@ -44,18 +44,24 @@ # 2004-02-16 / Andreas Krebbel: Added support for .gcno/.gcda files and # gcov versioning # 2004-08-09 / Peter Oberparleiter: added configuration file support +# 2008-07-14 / Tom Zoerner: added --function-coverage command line option +# 2008-08-13 / Peter Oberparleiter: modified function coverage +# implementation (now enabled per default) # use strict; use File::Basename; +use File::Spec::Functions qw /abs2rel catdir file_name_is_absolute splitdir + splitpath/; use Getopt::Long; use Digest::MD5 qw(md5_base64); # Constants -our $lcov_version = "LTP GCOV extension version 1.4"; +our $lcov_version = 'LCOV version 1.9'; our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; our $gcov_tool = "gcov"; +our $tool_name = basename($0); our $GCOV_VERSION_3_4_0 = 0x30400; our $GCOV_VERSION_3_3_0 = 0x30300; @@ -64,31 +70,79 @@ our $GCNO_LINES_TAG = 0x01450000; our $GCNO_FILE_MAGIC = 0x67636e6f; our $BBG_FILE_MAGIC = 0x67626267; -our $COMPAT_SLES9 = "sles9"; +our $COMPAT_HAMMER = "hammer"; + +our $ERROR_GCOV = 0; +our $ERROR_SOURCE = 1; +our $ERROR_GRAPH = 2; + +our $EXCL_START = "LCOV_EXCL_START"; +our $EXCL_STOP = "LCOV_EXCL_STOP"; +our $EXCL_LINE = "LCOV_EXCL_LINE"; + +our $BR_LINE = 0; +our $BR_BLOCK = 1; +our $BR_BRANCH = 2; +our $BR_TAKEN = 3; +our $BR_VEC_ENTRIES = 4; +our $BR_VEC_WIDTH = 32; + +our $UNNAMED_BLOCK = 9999; # Prototypes sub print_usage(*); sub gen_info($); -sub process_dafile($); +sub process_dafile($$); sub match_filename($@); sub solve_ambiguous_match($$$); sub split_filename($); sub solve_relative_path($$); -sub get_dir($); sub read_gcov_header($); sub read_gcov_file($); -sub read_bb_file($); -sub read_string(*$); -sub read_gcno_file($); -sub read_gcno_string(*$); -sub read_sles9_bbg_file($); -sub read_sles9_bbg_string(*$); -sub unpack_int32($$); sub info(@); sub get_gcov_version(); sub system_no_output($@); sub read_config($); sub apply_config($); +sub get_exclusion_data($); +sub apply_exclusion_data($$); +sub process_graphfile($$); +sub filter_fn_name($); +sub warn_handler($); +sub die_handler($); +sub graph_error($$); +sub graph_expect($); +sub graph_read(*$;$); +sub graph_skip(*$;$); +sub sort_uniq(@); +sub sort_uniq_lex(@); +sub graph_cleanup($); +sub graph_find_base($); +sub graph_from_bb($$$); +sub graph_add_order($$$); +sub read_bb_word(*;$); +sub read_bb_value(*;$); +sub read_bb_string(*$); +sub read_bb($$); +sub read_bbg_word(*;$); +sub read_bbg_value(*;$); +sub read_bbg_string(*); +sub read_bbg_lines_record(*$$$$$$); +sub read_bbg($$); +sub read_gcno_word(*;$); +sub read_gcno_value(*$;$); +sub read_gcno_string(*$); +sub read_gcno_lines_record(*$$$$$$$); +sub read_gcno_function_record(*$$$$); +sub read_gcno($$); +sub get_gcov_capabilities(); +sub get_overall_line($$$$); +sub print_overall_rate($$$$$$$$$); +sub br_gvec_len($); +sub br_gvec_get($$); +sub debug($); +sub int_handler(); + # Global variables our $gcov_version; @@ -96,18 +150,30 @@ our $graph_file_extension; our $data_file_extension; our @data_directory; our $test_name = ""; -our $source_dirs = ""; our $quiet; our $help; our $output_filename; +our $base_directory; our $version; our $follow; -our $nochecksum; -our $preserve_paths; -our $adjust_testname = (`uname -m` =~ /^s390/); # Always on on s390 +our $checksum; +our $no_checksum; +our $compat_libtool; +our $no_compat_libtool; +our $adjust_testname; our $config; # Configuration file contents our $compatibility; # Compatibility version flag - used to indicate # non-standard GCOV data format versions +our @ignore_errors; # List of errors to ignore (parameter) +our @ignore; # List of errors to ignore (array) +our $initial; +our $no_recursion = 0; +our $maxdepth; +our $no_markers = 0; +our $opt_derive_func_data = 0; +our $debug = 0; +our $gcov_caps; +our @gcov_options; our $cwd = `pwd`; chomp($cwd); @@ -119,9 +185,17 @@ chomp($cwd); # Register handler routine to be called when interrupted $SIG{"INT"} = \&int_handler; +$SIG{__WARN__} = \&warn_handler; +$SIG{__DIE__} = \&die_handler; + +# Prettify version string +$lcov_version =~ s/\$\s*Revision\s*:?\s*(\S+)\s*\$/$1/; + +# Set LANG so that gcov output will be in a unified format +$ENV{"LANG"} = "C"; # Read configuration file if available -if (-r $ENV{"HOME"}."/.lcovrc") +if (defined($ENV{"HOME"}) && (-r $ENV{"HOME"}."/.lcovrc")) { $config = read_config($ENV{"HOME"}."/.lcovrc"); } @@ -136,23 +210,57 @@ if ($config) apply_config({ "geninfo_gcov_tool" => \$gcov_tool, "geninfo_adjust_testname" => \$adjust_testname, - "geninfo_no_checksum" => \$nochecksum}); + "geninfo_checksum" => \$checksum, + "geninfo_no_checksum" => \$no_checksum, # deprecated + "geninfo_compat_libtool" => \$compat_libtool}); + + # Merge options + if (defined($no_checksum)) + { + $checksum = ($no_checksum ? 0 : 1); + $no_checksum = undef; + } } # Parse command line options -if (!GetOptions("test-name=s" => \$test_name, - "output-filename=s" => \$output_filename, - "source-dirs=s" => \$source_dirs, - "no-checksum" => \$nochecksum, - "version" =>\$version, - "quiet" => \$quiet, - "help" => \$help, - "follow" => \$follow +if (!GetOptions("test-name|t=s" => \$test_name, + "output-filename|o=s" => \$output_filename, + "checksum" => \$checksum, + "no-checksum" => \$no_checksum, + "base-directory|b=s" => \$base_directory, + "version|v" =>\$version, + "quiet|q" => \$quiet, + "help|h|?" => \$help, + "follow|f" => \$follow, + "compat-libtool" => \$compat_libtool, + "no-compat-libtool" => \$no_compat_libtool, + "gcov-tool=s" => \$gcov_tool, + "ignore-errors=s" => \@ignore_errors, + "initial|i" => \$initial, + "no-recursion" => \$no_recursion, + "no-markers" => \$no_markers, + "derive-func-data" => \$opt_derive_func_data, + "debug" => \$debug, )) { - print_usage(*STDERR); + print(STDERR "Use $tool_name --help to get usage information\n"); exit(1); } +else +{ + # Merge options + if (defined($no_checksum)) + { + $checksum = ($no_checksum ? 0 : 1); + $no_checksum = undef; + } + + if (defined($no_compat_libtool)) + { + $compat_libtool = ($no_compat_libtool ? 0 : 1); + $no_compat_libtool = undef; + } +} @data_directory = @ARGV; @@ -166,17 +274,29 @@ if ($help) # Check for version option if ($version) { - print($lcov_version."\n"); + print("$tool_name: $lcov_version\n"); exit(0); } -# Adjust test name if necessary (standard for s390 architecture) +# Make sure test names only contain valid characters +if ($test_name =~ s/\W/_/g) +{ + warn("WARNING: invalid characters removed from testname!\n"); +} + +# Adjust test name to include uname output if requested if ($adjust_testname) { $test_name .= "__".`uname -a`; $test_name =~ s/\W/_/g; } +# Make sure base_directory contains an absolute path specification +if ($base_directory) +{ + $base_directory = solve_relative_path($cwd, $base_directory); +} + # Check for follow option if ($follow) { @@ -187,12 +307,44 @@ else $follow = ""; } +# Determine checksum mode +if (defined($checksum)) +{ + # Normalize to boolean + $checksum = ($checksum ? 1 : 0); +} +else +{ + # Default is off + $checksum = 0; +} + +# Determine libtool compatibility mode +if (defined($compat_libtool)) +{ + $compat_libtool = ($compat_libtool? 1 : 0); +} +else +{ + # Default is on + $compat_libtool = 1; +} + +# Determine max depth for recursion +if ($no_recursion) +{ + $maxdepth = "-maxdepth 1"; +} +else +{ + $maxdepth = ""; +} + # Check for directory name if (!@data_directory) { - print(STDERR "No directory specified\n"); - print_usage(*STDERR); - exit(1); + die("No directory specified\n". + "Use $tool_name --help to get usage information\n"); } else { @@ -206,6 +358,32 @@ else } } +if (@ignore_errors) +{ + my @expanded; + my $error; + + # Expand comma-separated entries + foreach (@ignore_errors) { + if (/,/) + { + push(@expanded, split(",", $_)); + } + else + { + push(@expanded, $_); + } + } + + foreach (@expanded) + { + /^gcov$/ && do { $ignore[$ERROR_GCOV] = 1; next; } ; + /^source$/ && do { $ignore[$ERROR_SOURCE] = 1; next; }; + /^graph$/ && do { $ignore[$ERROR_GRAPH] = 1; next; }; + die("ERROR: unknown argument for --ignore-errors: $_\n"); + } +} + if (system_no_output(3, $gcov_tool, "--help") == -1) { die("ERROR: need tool $gcov_tool!\n"); @@ -215,7 +393,7 @@ $gcov_version = get_gcov_version(); if ($gcov_version < $GCOV_VERSION_3_4_0) { - if (defined($compatibility) && $compatibility eq $COMPAT_SLES9) + if (defined($compatibility) && $compatibility eq $COMPAT_HAMMER) { $data_file_extension = ".da"; $graph_file_extension = ".bbg"; @@ -232,11 +410,12 @@ else $graph_file_extension = ".gcno"; } -# Check for availability of --preserve-paths option of gcov -if (`$gcov_tool --help` =~ /--preserve-paths/) -{ - $preserve_paths = "--preserve-paths"; -} +# Determine gcov options +$gcov_caps = get_gcov_capabilities(); +push(@gcov_options, "-b") if ($gcov_caps->{'branch-probabilities'}); +push(@gcov_options, "-c") if ($gcov_caps->{'branch-counts'}); +push(@gcov_options, "-a") if ($gcov_caps->{'all-blocks'}); +push(@gcov_options, "-p") if ($gcov_caps->{'preserve-paths'}); # Check output filename if (defined($output_filename) && ($output_filename ne "-")) @@ -257,9 +436,13 @@ if (defined($output_filename) && ($output_filename ne "-")) } # Do something -foreach (@data_directory) -{ - gen_info($_); +foreach my $entry (@data_directory) { + gen_info($entry); +} + +if ($initial) { + warn("Note: --initial does not generate branch coverage ". + "data\n"); } info("Finished .info-file creation\n"); @@ -276,28 +459,70 @@ exit(0); sub print_usage(*) { local *HANDLE = $_[0]; - my $tool_name = basename($0); print(HANDLE </dev/null`; + @file_list = `find "$directory" $maxdepth $follow -name \\*$ext -type f 2>/dev/null`; chomp(@file_list); - @file_list or die("ERROR: no $data_file_extension files found ". - "in $directory!\n"); - info("Found %d data files in %s\n", $#file_list+1, $directory); + @file_list or + die("ERROR: no $ext files found in $directory!\n"); + $prefix = get_common_prefix(1, @file_list); + info("Found %d %s files in %s\n", $#file_list+1, $type, + $directory); } else { @file_list = ($directory); + $prefix = ""; } # Process all files in list - foreach (@file_list) { process_dafile($_); } + foreach $file (@file_list) { + # Process file + if ($initial) { + process_graphfile($file, $prefix); + } else { + process_dafile($file, $prefix); + } + } } +sub derive_data($$$) +{ + my ($contentdata, $funcdata, $bbdata) = @_; + my @gcov_content = @{$contentdata}; + my @gcov_functions = @{$funcdata}; + my %fn_count; + my %ln_fn; + my $line; + my $maxline; + my %fn_name; + my $fn; + my $count; + + if (!defined($bbdata)) { + return @gcov_functions; + } + + # First add existing function data + while (@gcov_functions) { + $count = shift(@gcov_functions); + $fn = shift(@gcov_functions); + + $fn_count{$fn} = $count; + } + + # Convert line coverage data to function data + foreach $fn (keys(%{$bbdata})) { + my $line_data = $bbdata->{$fn}; + my $line; + + if ($fn eq "") { + next; + } + # Find the lowest line count for this function + $count = 0; + foreach $line (@$line_data) { + my $lcount = $gcov_content[ ( $line - 1 ) * 3 + 1 ]; + + if (($lcount > 0) && + (($count == 0) || ($lcount < $count))) { + $count = $lcount; + } + } + $fn_count{$fn} = $count; + } + + + # Check if we got data for all functions + foreach $fn (keys(%fn_name)) { + if ($fn eq "") { + next; + } + if (defined($fn_count{$fn})) { + next; + } + warn("WARNING: no derived data found for function $fn\n"); + } + + # Convert hash to list in @gcov_functions format + foreach $fn (sort(keys(%fn_count))) { + push(@gcov_functions, $fn_count{$fn}, $fn); + } + + return @gcov_functions; +} + # -# process_dafile(da_filename) +# get_filenames(directory, pattern) +# +# Return a list of filenames found in directory which match the specified +# pattern. +# +# Die on error. +# + +sub get_filenames($$) +{ + my ($dirname, $pattern) = @_; + my @result; + my $directory; + local *DIR; + + opendir(DIR, $dirname) or + die("ERROR: cannot read directory $dirname\n"); + while ($directory = readdir(DIR)) { + push(@result, $directory) if ($directory =~ /$pattern/); + } + closedir(DIR); + + return @result; +} + +# +# process_dafile(da_filename, dir) # # Create a .info file for a single data file. # # Die on error. # -sub process_dafile($) +sub process_dafile($$) { - info("Processing %s\n", $_[0]); - + my ($file, $dir) = @_; my $da_filename; # Name of data file to process my $da_dir; # Directory of data file + my $source_dir; # Directory of source file my $da_basename; # data filename without ".da/.gcda" extension my $bb_filename; # Name of respective graph file - my %bb_content; # Contents of graph file + my $bb_basename; # Basename of the original graph file + my $graph; # Contents of graph file + my $instr; # Contents of graph file part 2 my $gcov_error; # Error code of gcov tool my $object_dir; # Directory containing all object files my $source_filename; # Name of a source code file my $gcov_file; # Name of a .gcov file my @gcov_content; # Content of a .gcov file - my @gcov_branches; # Branch content of a .gcov file + my $gcov_branches; # Branch content of a .gcov file + my @gcov_functions; # Function calls of a .gcov file my @gcov_list; # List of generated .gcov files my $line_number; # Line number count my $lines_hit; # Number of instrumented lines hit my $lines_found; # Number of instrumented lines found + my $funcs_hit; # Number of instrumented functions hit + my $funcs_found; # Number of instrumented functions found + my $br_hit; + my $br_found; my $source; # gcov source header information my $object; # gcov object header information my @matches; # List of absolute paths matching filename my @unprocessed; # List of unprocessed source code files + my $base_dir; # Base directory for current file + my @tmp_links; # Temporary links to be cleaned up my @result; my $index; my $da_renamed; # If data file is to be renamed local *INFO_HANDLE; + info("Processing %s\n", abs2rel($file, $dir)); # Get path to data file in absolute and normalized form (begins with /, # contains no more ../ or ./) - $da_filename = solve_relative_path($cwd, $_[0]); + $da_filename = solve_relative_path($cwd, $file); # Get directory and basename of data file ($da_dir, $da_basename) = split_filename($da_filename); - # Check for writable $da_dir (gcov will try to write files there) - stat($da_dir); - if (!-w _) - { - die("ERROR: cannot write to directory $da_dir!\n"); + # avoid files from .libs dirs + if ($compat_libtool && $da_dir =~ m/(.*)\/\.libs$/) { + $source_dir = $1; + } else { + $source_dir = $da_dir; } if (-z $da_filename) @@ -417,15 +765,35 @@ sub process_dafile($) $da_renamed = 0; } - # Construct name of graph file - $bb_filename = $da_dir."/".$da_basename.$graph_file_extension; + # Construct base_dir for current file + if ($base_directory) + { + $base_dir = $base_directory; + } + else + { + $base_dir = $source_dir; + } + # Check for writable $base_dir (gcov will try to write files there) + stat($base_dir); + if (!-w _) + { + die("ERROR: cannot write to directory $base_dir!\n"); + } + + # Construct name of graph file + $bb_basename = $da_basename.$graph_file_extension; + $bb_filename = "$da_dir/$bb_basename"; # Find out the real location of graph file in case we're just looking at # a link while (readlink($bb_filename)) { + my $last_dir = dirname($bb_filename); + $bb_filename = readlink($bb_filename); + $bb_filename = solve_relative_path($last_dir, $bb_filename); } # Ignore empty graph file (e.g. source file with no statement) @@ -440,27 +808,27 @@ sub process_dafile($) # information about functions and their source code positions. if ($gcov_version < $GCOV_VERSION_3_4_0) { - if (defined($compatibility) && $compatibility eq $COMPAT_SLES9) + if (defined($compatibility) && $compatibility eq $COMPAT_HAMMER) { - %bb_content = read_sles9_bbg_file($bb_filename); + ($instr, $graph) = read_bbg($bb_filename, $base_dir); } else { - %bb_content = read_bb_file($bb_filename); + ($instr, $graph) = read_bb($bb_filename, $base_dir); } } else { - %bb_content = read_gcno_file($bb_filename); + ($instr, $graph) = read_gcno($bb_filename, $base_dir); } # Set $object_dir to real location of object files. This may differ # from $da_dir if the graph file is just a link to the "real" object - # file location. We need to apply GCOV with using that directory to - # ensure that all relative #include-files are found as well. - ($object_dir) = split_filename($bb_filename); + # file location. + $object_dir = dirname($bb_filename); - # Is the data file in the same directory with all the other files? + # Is the data file in a different directory? (this happens e.g. with + # the gcov-kernel patch) if ($object_dir ne $da_dir) { # Need to create link to data file in $object_dir @@ -468,10 +836,21 @@ sub process_dafile($) "$object_dir/$da_basename$data_file_extension") and die ("ERROR: cannot create link $object_dir/". "$da_basename$data_file_extension!\n"); + push(@tmp_links, + "$object_dir/$da_basename$data_file_extension"); + # Need to create link to graph file if basename of link + # and file are different (CONFIG_MODVERSION compat) + if ((basename($bb_filename) ne $bb_basename) && + (! -e "$object_dir/$bb_basename")) { + symlink($bb_filename, "$object_dir/$bb_basename") or + warn("WARNING: cannot create link ". + "$object_dir/$bb_basename\n"); + push(@tmp_links, "$object_dir/$bb_basename"); + } } # Change to directory containing data files and apply GCOV - #chdir($object_dir); + chdir($base_dir); if ($da_renamed) { @@ -482,19 +861,8 @@ sub process_dafile($) } # Execute gcov command and suppress standard output - if ($preserve_paths) - { - $gcov_error = system_no_output(1, $gcov_tool, $da_basename.".c", - "-o", $object_dir, - "--preserve-paths", - "-b"); - } - else - { - $gcov_error = system_no_output(1, $gcov_tool, $da_basename.".c", - "-o", $object_dir, - "-b"); - } + $gcov_error = system_no_output(1, $gcov_tool, $da_filename, + "-o", $object_dir, @gcov_options); if ($da_renamed) { @@ -502,16 +870,23 @@ sub process_dafile($) and die ("ERROR: cannot rename $da_filename.ori"); } - # Clean up link - if ($object_dir ne $da_dir) - { - unlink($object_dir."/".$da_basename.$data_file_extension); + # Clean up temporary links + foreach (@tmp_links) { + unlink($_); } - $gcov_error and die("ERROR: GCOV failed for $da_filename!\n"); + if ($gcov_error) + { + if ($ignore[$ERROR_GCOV]) + { + warn("WARNING: GCOV failed for $da_filename!\n"); + return; + } + die("ERROR: GCOV failed for $da_filename!\n"); + } # Collect data from resulting .gcov files and create .info file - @gcov_list = glob("*.gcov"); + @gcov_list = get_filenames('.', '\.gcov$'); # Check for files if (!@gcov_list) @@ -547,26 +922,35 @@ sub process_dafile($) # Traverse the list of generated .gcov files and combine them into a # single .info file - @unprocessed = keys(%bb_content); - foreach $gcov_file (@gcov_list) + @unprocessed = keys(%{$instr}); + foreach $gcov_file (sort(@gcov_list)) { + my $i; + my $num; + ($source, $object) = read_gcov_header($gcov_file); - if ($source) + if (defined($source)) { - $source = solve_relative_path($object_dir, $source); + $source = solve_relative_path($base_dir, $source); } # gcov will happily create output even if there's no source code - # available - this interfers with checksum creation so we need + # available - this interferes with checksum creation so we need # to pull the emergency brake here. - if (defined($source) && ! -r $source && ! $nochecksum) + if (defined($source) && ! -r $source && $checksum) { + if ($ignore[$ERROR_SOURCE]) + { + warn("WARNING: could not read source file ". + "$source\n"); + next; + } die("ERROR: could not read source file $source\n"); } @matches = match_filename(defined($source) ? $source : - $gcov_file, keys(%bb_content)); + $gcov_file, keys(%{$instr})); # Skip files that are not mentioned in the graph file if (!@matches) @@ -580,8 +964,15 @@ sub process_dafile($) # Read in contents of gcov file @result = read_gcov_file($gcov_file); + if (!defined($result[0])) { + warn("WARNING: skipping unreadable file ". + $gcov_file."\n"); + unlink($gcov_file); + next; + } @gcov_content = @{$result[0]}; - @gcov_branches = @{$result[1]}; + $gcov_branches = $result[1]; + @gcov_functions = @{$result[2]}; # Skip empty files if (!@gcov_content) @@ -616,15 +1007,96 @@ sub process_dafile($) # Write absolute path of source file printf(INFO_HANDLE "SF:%s\n", $source_filename); + # If requested, derive function coverage data from + # line coverage data of the first line of a function + if ($opt_derive_func_data) { + @gcov_functions = + derive_data(\@gcov_content, \@gcov_functions, + $graph->{$source_filename}); + } + # Write function-related information - foreach (split(",",$bb_content{$source_filename})) + if (defined($graph->{$source_filename})) { - # Write "line_number,function_name" for each function. - # Note that $_ contains this information in the form - # "function_name=line_number" so that the order of - # elements has to be reversed. - printf(INFO_HANDLE "FN:%s\n", - join(",", (split("=", $_))[1,0])); + my $fn_data = $graph->{$source_filename}; + my $fn; + + foreach $fn (sort + {$fn_data->{$a}->[0] <=> $fn_data->{$b}->[0]} + keys(%{$fn_data})) { + my $ln_data = $fn_data->{$fn}; + my $line = $ln_data->[0]; + + # Skip empty function + if ($fn eq "") { + next; + } + # Remove excluded functions + if (!$no_markers) { + my $gfn; + my $found = 0; + + foreach $gfn (@gcov_functions) { + if ($gfn eq $fn) { + $found = 1; + last; + } + } + if (!$found) { + next; + } + } + + # Normalize function name + $fn = filter_fn_name($fn); + + print(INFO_HANDLE "FN:$line,$fn\n"); + } + } + + #-- + #-- FNDA: , + #-- FNF: overall count of functions + #-- FNH: overall count of functions with non-zero call count + #-- + $funcs_found = 0; + $funcs_hit = 0; + while (@gcov_functions) + { + my $count = shift(@gcov_functions); + my $fn = shift(@gcov_functions); + + $fn = filter_fn_name($fn); + printf(INFO_HANDLE "FNDA:$count,$fn\n"); + $funcs_found++; + $funcs_hit++ if ($count > 0); + } + if ($funcs_found > 0) { + printf(INFO_HANDLE "FNF:%s\n", $funcs_found); + printf(INFO_HANDLE "FNH:%s\n", $funcs_hit); + } + + # Write coverage information for each instrumented branch: + # + # BRDA:,,, + # + # where 'taken' is the number of times the branch was taken + # or '-' if the block to which the branch belongs was never + # executed + $br_found = 0; + $br_hit = 0; + $num = br_gvec_len($gcov_branches); + for ($i = 0; $i < $num; $i++) { + my ($line, $block, $branch, $taken) = + br_gvec_get($gcov_branches, $i); + + print(INFO_HANDLE "BRDA:$line,$block,$branch,$taken\n"); + $br_found++; + $br_hit++ if ($taken ne '-' && $taken > 0); + } + if ($br_found > 0) { + printf(INFO_HANDLE "BRF:%s\n", $br_found); + printf(INFO_HANDLE "BRH:%s\n", $br_hit); } # Reset line counters @@ -644,8 +1116,8 @@ sub process_dafile($) { $lines_found++; printf(INFO_HANDLE "DA:".$line_number.",". - $gcov_content[1].($nochecksum ? "" : - ",". md5_base64($gcov_content[2])). + $gcov_content[1].($checksum ? + ",". md5_base64($gcov_content[2]) : ""). "\n"); # Increase $lines_hit in case of an execution @@ -657,27 +1129,6 @@ sub process_dafile($) splice(@gcov_content,0,3); } - #-- - #-- BA: , - #-- - #-- print one BA line for every branch of a - #-- conditional. values - #-- are: - #-- 0 - not executed - #-- 1 - executed but not taken - #-- 2 - executed and taken - #-- - while (@gcov_branches) - { - if ($gcov_branches[0]) - { - printf(INFO_HANDLE "BA:%s,%s\n", - $gcov_branches[0], - $gcov_branches[1]); - } - splice(@gcov_branches,0,2); - } - # Write line statistics and section separator printf(INFO_HANDLE "LF:%s\n", $lines_found); printf(INFO_HANDLE "LH:%s\n", $lines_hit); @@ -707,28 +1158,6 @@ sub process_dafile($) } -# cleanup_path (path) -sub cleanup_path($@) -{ - my $result = shift @_; - - # Remove // - $result =~ s/\/\//\//g; - - # Remove . - $result =~ s/\/\.\//\//g; - - # Solve .. - while ($result =~ s/\/[^\/]+\/\.\.\//\//) - { - } - - # Remove preceding .. - $result =~ s/^\/\.\.\//\//g; - - return $result; -} - # # solve_relative_path(path, dir) # @@ -748,18 +1177,20 @@ sub solve_relative_path($$) $result = "$path/$result"; } - $result = cleanup_path ($result); + # Remove // + $result =~ s/\/\//\//g; - if (-f $result) { - return $result; - } - for my $prefix (split (':', $source_dirs)) { - $result = cleanup_path ("$prefix/$dir"); - if (-f $result) { - return $result; - } + # Remove . + $result =~ s/\/\.\//\//g; + + # Solve .. + while ($result =~ s/\/[^\/]+\/\.\.\//\//) + { } + # Remove preceding .. + $result =~ s/^\/\.\.\//\//g; + return $result; } @@ -773,28 +1204,42 @@ sub solve_relative_path($$) sub match_filename($@) { - my $filename = shift; - my @list = @_; + my ($filename, @list) = @_; + my ($vol, $dir, $file) = splitpath($filename); + my @comp = splitdir($dir); + my $comps = scalar(@comp); + my $entry; my @result; - $filename =~ s/^(.*).gcov$/$1/; +entry: + foreach $entry (@list) { + my ($evol, $edir, $efile) = splitpath($entry); + my @ecomp; + my $ecomps; + my $i; - if ($filename =~ /^\/(.*)$/) - { - $filename = "$1"; - } - - foreach (@list) - { - if (/\/\Q$filename\E(.*)$/ && $1 eq "") - { - @result = (@result, $_); + # Filename component must match + if ($efile ne $file) { + next; } + # Check directory components last to first for match + @ecomp = splitdir($edir); + $ecomps = scalar(@ecomp); + if ($ecomps < $comps) { + next; + } + for ($i = 0; $i < $comps; $i++) { + if ($comp[$comps - $i - 1] ne + $ecomp[$ecomps - $i - 1]) { + next entry; + } + } + push(@result, $entry), } + return @result; } - # # solve_ambiguous_match(rel_filename, matches_ref, gcov_content_ref) # @@ -829,6 +1274,9 @@ sub solve_ambiguous_match($$$) { chomp; + # Also remove CR from line-end + s/\015$//; + if ($_ ne @$content[$index]) { $no_match = 1; @@ -866,21 +1314,6 @@ sub split_filename($) } -# -# get_dir(filename); -# -# Return the directory component of a given FILENAME. -# - -sub get_dir($) -{ - my @components = split("/", $_[0]); - pop(@components); - - return join("/", @components); -} - - # # read_gcov_header(gcov_filename) # @@ -903,13 +1336,23 @@ sub read_gcov_header($) my $object; local *INPUT; - open(INPUT, $_[0]) - or die("ERROR: cannot read $_[0]!\n"); + if (!open(INPUT, $_[0])) + { + if ($ignore_errors[$ERROR_GCOV]) + { + warn("WARNING: cannot read $_[0]!\n"); + return (undef,undef); + } + die("ERROR: cannot read $_[0]!\n"); + } while () { chomp($_); + # Also remove CR from line-end + s/\015$//; + if (/^\s+-:\s+0:Source:(.*)$/) { # Source: header entry @@ -932,11 +1375,89 @@ sub read_gcov_header($) } +# +# br_gvec_len(vector) +# +# Return the number of entries in the branch coverage vector. +# + +sub br_gvec_len($) +{ + my ($vec) = @_; + + return 0 if (!defined($vec)); + return (length($vec) * 8 / $BR_VEC_WIDTH) / $BR_VEC_ENTRIES; +} + + +# +# br_gvec_get(vector, number) +# +# Return an entry from the branch coverage vector. +# + +sub br_gvec_get($$) +{ + my ($vec, $num) = @_; + my $line; + my $block; + my $branch; + my $taken; + my $offset = $num * $BR_VEC_ENTRIES; + + # Retrieve data from vector + $line = vec($vec, $offset + $BR_LINE, $BR_VEC_WIDTH); + $block = vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH); + $branch = vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH); + $taken = vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH); + + # Decode taken value from an integer + if ($taken == 0) { + $taken = "-"; + } else { + $taken--; + } + + return ($line, $block, $branch, $taken); +} + + +# +# br_gvec_push(vector, line, block, branch, taken) +# +# Add an entry to the branch coverage vector. +# + +sub br_gvec_push($$$$$) +{ + my ($vec, $line, $block, $branch, $taken) = @_; + my $offset; + + $vec = "" if (!defined($vec)); + $offset = br_gvec_len($vec) * $BR_VEC_ENTRIES; + + # Encode taken value into an integer + if ($taken eq "-") { + $taken = 0; + } else { + $taken++; + } + + # Add to vector + vec($vec, $offset + $BR_LINE, $BR_VEC_WIDTH) = $line; + vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH) = $block; + vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH) = $branch; + vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH) = $taken; + + return $vec; +} + + # # read_gcov_file(gcov_filename) # # Parse file GCOV_FILENAME (.gcov file format) and return the list: -# (reference to gcov_content, reference to gcov_branch) +# (reference to gcov_content, reference to gcov_branch, reference to gcov_func) # # gcov_content is a list of 3 elements # (flag, count, source) for each source code line: @@ -945,8 +1466,11 @@ sub read_gcov_header($) # $result[($line_number-1)*3+1] = execution count for line $line_number # $result[($line_number-1)*3+2] = source code text for line $line_number # -# gcov_branch is a list of 2 elements -# (linenumber, branch result) for each branch +# gcov_branch is a vector of 4 4-byte long elements for each branch: +# line number, block number, branch number, count + 1 or 0 +# +# gcov_func is a list of 2 elements +# (number of calls, function name) for each function # # Die on error. # @@ -955,12 +1479,23 @@ sub read_gcov_file($) { my $filename = $_[0]; my @result = (); - my @branches = (); + my $branches = ""; + my @functions = (); my $number; + my $exclude_flag = 0; + my $exclude_line = 0; + my $last_block = $UNNAMED_BLOCK; + my $last_line = 0; local *INPUT; - open(INPUT, $filename) - or die("ERROR: cannot read $filename!\n"); + if (!open(INPUT, $filename)) { + if ($ignore_errors[$ERROR_GCOV]) + { + warn("WARNING: cannot read $filename!\n"); + return (undef, undef, undef); + } + die("ERROR: cannot read $filename!\n"); + } if ($gcov_version < $GCOV_VERSION_3_3_0) { @@ -969,45 +1504,61 @@ sub read_gcov_file($) { chomp($_); - if (/^\t\t(.*)$/) - { - # Uninstrumented line - push(@result, 0); - push(@result, 0); - push(@result, $1); + # Also remove CR from line-end + s/\015$//; + + if (/^branch\s+(\d+)\s+taken\s+=\s+(\d+)/) { + next if ($exclude_line); + $branches = br_gvec_push($branches, $last_line, + $last_block, $1, $2); + } elsif (/^branch\s+(\d+)\s+never\s+executed/) { + next if ($exclude_line); + $branches = br_gvec_push($branches, $last_line, + $last_block, $1, '-'); } - elsif (/^branch/) - { - # Branch execution data - push(@branches, scalar(@result) / 3); - if (/^branch \d+ never executed$/) - { - push(@branches, 0); - } - elsif (/^branch \d+ taken = 0%/) - { - push(@branches, 1); - } - else - { - push(@branches, 2); - } - } - elsif (/^call/) + elsif (/^call/ || /^function/) { # Function call return data } else { + $last_line++; + # Check for exclusion markers + if (!$no_markers) { + if (/$EXCL_STOP/) { + $exclude_flag = 0; + } elsif (/$EXCL_START/) { + $exclude_flag = 1; + } + if (/$EXCL_LINE/ || $exclude_flag) { + $exclude_line = 1; + } else { + $exclude_line = 0; + } + } # Source code execution data + if (/^\t\t(.*)$/) + { + # Uninstrumented line + push(@result, 0); + push(@result, 0); + push(@result, $1); + next; + } $number = (split(" ",substr($_, 0, 16)))[0]; # Check for zero count which is indicated # by ###### if ($number eq "######") { $number = 0; } - push(@result, 1); - push(@result, $number); + if ($exclude_line) { + # Register uninstrumented line instead + push(@result, 0); + push(@result, 0); + } else { + push(@result, 1); + push(@result, $number); + } push(@result, substr($_, 16)); } } @@ -1019,22 +1570,29 @@ sub read_gcov_file($) { chomp($_); - if (/^branch\s+\d+\s+(\S+)\s+(\S+)/) + # Also remove CR from line-end + s/\015$//; + + if (/^\s*(\d+|\$+):\s*(\d+)-block\s+(\d+)\s*$/) { + # Block information - used to group related + # branches + $last_line = $2; + $last_block = $3; + } elsif (/^branch\s+(\d+)\s+taken\s+(\d+)/) { + next if ($exclude_line); + $branches = br_gvec_push($branches, $last_line, + $last_block, $1, $2); + } elsif (/^branch\s+(\d+)\s+never\s+executed/) { + next if ($exclude_line); + $branches = br_gvec_push($branches, $last_line, + $last_block, $1, '-'); + } + elsif (/^function\s+(\S+)\s+called\s+(\d+)/) { - # Branch execution data - push(@branches, scalar(@result) / 3); - if ($1 eq "never") - { - push(@branches, 0); - } - elsif ($2 eq "0%") - { - push(@branches, 1); - } - else - { - push(@branches, 2); + if ($exclude_line) { + next; } + push(@functions, $2, $1); } elsif (/^call/) { @@ -1042,515 +1600,59 @@ sub read_gcov_file($) } elsif (/^\s*([^:]+):\s*([^:]+):(.*)$/) { + my ($count, $line, $code) = ($1, $2, $3); + + $last_line = $line; + $last_block = $UNNAMED_BLOCK; + # Check for exclusion markers + if (!$no_markers) { + if (/$EXCL_STOP/) { + $exclude_flag = 0; + } elsif (/$EXCL_START/) { + $exclude_flag = 1; + } + if (/$EXCL_LINE/ || $exclude_flag) { + $exclude_line = 1; + } else { + $exclude_line = 0; + } + } # :: - if ($2 eq "0") + if ($line eq "0") { # Extra data } - elsif ($1 eq "-") + elsif ($count eq "-") { # Uninstrumented line push(@result, 0); push(@result, 0); - push(@result, $3); + push(@result, $code); } else { - # Source code execution data - $number = $1; - - # Check for zero count - if ($number eq "#####") { $number = 0; } - - push(@result, 1); - push(@result, $number); - push(@result, $3); - } - } - } - } - - close(INPUT); - return(\@result, \@branches); -} - - -# -# read_bb_file(bb_filename) -# -# Read .bb file BB_FILENAME and return a hash containing the following -# mapping: -# -# filename -> comma-separated list of pairs (function name=starting -# line number) for each function found -# -# for each entry in the .bb file. Filenames are absolute, i.e. relative -# filenames are prefixed with bb_filename's path component. -# -# Die on error. -# - -sub read_bb_file($) -{ - my $bb_filename = $_[0]; - my %result; - my $filename; - my $function_name; - my $cwd = `pwd`; - chomp($cwd); - my $base_dir = get_dir(solve_relative_path( - $cwd, $bb_filename)); - my $minus_one = sprintf("%d", 0x80000001); - my $minus_two = sprintf("%d", 0x80000002); - my $value; - my $packed_word; - local *INPUT; - - open(INPUT, $bb_filename) - or die("ERROR: cannot read $bb_filename!\n"); - - binmode(INPUT); - - # Read data in words of 4 bytes - while (read(INPUT, $packed_word, 4) == 4) - { - # Decode integer in intel byteorder - $value = unpack_int32($packed_word, 0); - - # Note: the .bb file format is documented in GCC info pages - if ($value == $minus_one) - { - # Filename follows - $filename = read_string(*INPUT, $minus_one) - or die("ERROR: incomplete filename in ". - "$bb_filename!\n"); - - # Make path absolute - $filename = solve_relative_path($base_dir, $filename); - - # Insert into hash if not yet present. - # This is necessary because functions declared as - # "inline" are not listed as actual functions in - # .bb files - if (!$result{$filename}) - { - $result{$filename}=""; - } - } - elsif ($value == $minus_two) - { - # Function name follows - $function_name = read_string(*INPUT, $minus_two) - or die("ERROR: incomplete function ". - "name in $bb_filename!\n"); - } - elsif ($value > 0) - { - if ($function_name) - { - # Got a full entry filename, funcname, lineno - # Add to resulting hash - - $result{$filename}.= - ($result{$filename} ? "," : ""). - join("=",($function_name,$value)); - undef($function_name); - } - } - } - close(INPUT); - - if (!scalar(keys(%result))) - { - die("ERROR: no data found in $bb_filename!\n"); - } - return %result; -} - - -# -# read_string(handle, delimiter); -# -# Read and return a string in 4-byte chunks from HANDLE until DELIMITER -# is found. -# -# Return empty string on error. -# - -sub read_string(*$) -{ - my $HANDLE = $_[0]; - my $delimiter = $_[1]; - my $string = ""; - my $packed_word; - my $value; - - while (read($HANDLE,$packed_word,4) == 4) - { - $value = unpack_int32($packed_word, 0); - - if ($value == $delimiter) - { - # Remove trailing nil bytes - $/="\0"; - while (chomp($string)) {}; - $/="\n"; - return($string); - } - - $string = $string.$packed_word; - } - return(""); -} - - -# -# read_gcno_file(bb_filename) -# -# Read .gcno file BB_FILENAME and return a hash containing the following -# mapping: -# -# filename -> comma-separated list of pairs (function name=starting -# line number) for each function found -# -# for each entry in the .gcno file. Filenames are absolute, i.e. relative -# filenames are prefixed with bb_filename's path component. -# -# Die on error. -# - -sub read_gcno_file($) -{ - my $gcno_filename = $_[0]; - my %result; - my $filename; - my $function_name; - my $lineno; - my $length; - my $cwd = `pwd`; - my $value; - my $endianness; - my $blocks; - chomp($cwd); - my $base_dir = get_dir(solve_relative_path( - $cwd, $gcno_filename)); - my $packed_word; - local *INPUT; - - open(INPUT, $gcno_filename) - or die("ERROR: cannot read $gcno_filename!\n"); - - binmode(INPUT); - - read(INPUT, $packed_word, 4) == 4 - or die("ERROR: Invalid gcno file format\n"); - - $value = unpack_int32($packed_word, 0); - $endianness = !($value == $GCNO_FILE_MAGIC); - - unpack_int32($packed_word, $endianness) == $GCNO_FILE_MAGIC - or die("ERROR: gcno file magic does not match\n"); - - seek(INPUT, 8, 1); - - # Read data in words of 4 bytes - while (read(INPUT, $packed_word, 4) == 4) - { - # Decode integer in intel byteorder - $value = unpack_int32($packed_word, $endianness); - - if ($value == $GCNO_FUNCTION_TAG) - { - # skip length, ident and checksum - seek(INPUT, 12, 1); - (undef, $function_name) = - read_gcno_string(*INPUT, $endianness); - (undef, $filename) = - read_gcno_string(*INPUT, $endianness); - $filename = solve_relative_path($base_dir, $filename); - - read(INPUT, $packed_word, 4); - $lineno = unpack_int32($packed_word, $endianness); - - $result{$filename}.= - ($result{$filename} ? "," : ""). - join("=",($function_name,$lineno)); - } - elsif ($value == $GCNO_LINES_TAG) - { - # Check for names of files containing inlined code - # included in this file - read(INPUT, $packed_word, 4); - $length = unpack_int32($packed_word, $endianness); - while ($length > 0) - { - read(INPUT, $packed_word, 4); - $lineno = unpack_int32($packed_word, - $endianness); - $length--; - if ($lineno != 0) - { - next; - } - #added because if length is zero the following - #reading process will die, it will read garbage - if ($length == 0) - { - next; - } - ($blocks, $filename) = - read_gcno_string(*INPUT, $endianness); - if ($blocks > 1) - { - $filename = solve_relative_path( - $base_dir, $filename); - if (!defined($result{$filename})) - { - $result{$filename} = ""; + if ($exclude_line) { + push(@result, 0); + push(@result, 0); + } else { + # Check for zero count + if ($count eq "#####") { + $count = 0; + } + push(@result, 1); + push(@result, $count); } + push(@result, $code); } - $length -= $blocks; } } - else - { - read(INPUT, $packed_word, 4); - $length = unpack_int32($packed_word, $endianness); - seek(INPUT, 4 * $length, 1); - } } + close(INPUT); - - if (!scalar(keys(%result))) - { - die("ERROR: no data found in $gcno_filename!\n"); + if ($exclude_flag) { + warn("WARNING: unterminated exclusion section in $filename\n"); } - return %result; -} - - -# -# read_gcno_string(handle, endianness); -# -# Read a string in 4-byte chunks from HANDLE. -# -# Return (number of 4-byte chunks read, string). -# - -sub read_gcno_string(*$) -{ - my $handle = $_[0]; - my $endianness = $_[1]; - my $number_of_blocks = 0; - my $string = ""; - my $packed_word; - - read($handle, $packed_word, 4) == 4 - or die("ERROR: reading string\n"); - - $number_of_blocks = unpack_int32($packed_word, $endianness); - - if ($number_of_blocks == 0) - { - return (1, undef); - } - - read($handle, $packed_word, 4 * $number_of_blocks) == - 4 * $number_of_blocks or die("ERROR: reading string\n"); - - $string = $string . $packed_word; - - # Remove trailing nil bytes - $/="\0"; - while (chomp($string)) {}; - $/="\n"; - - return(1 + $number_of_blocks, $string); -} - - -# -# read_sles9_bbg_file(bb_filename) -# -# Read .bbg file BB_FILENAME and return a hash containing the following -# mapping: -# -# filename -> comma-separated list of pairs (function name=starting -# line number) for each function found -# -# for each entry in the .bbg file. Filenames are absolute, i.e. relative -# filenames are prefixed with bb_filename's path component. -# -# Die on error. -# - -sub read_sles9_bbg_file($) -{ - my $bbg_filename = $_[0]; - my %result; - my $filename; - my $function_name; - my $first_line; - my $lineno; - my $length; - my $cwd = `pwd`; - my $value; - my $endianness; - my $blocks; - chomp($cwd); - my $base_dir = get_dir(solve_relative_path($cwd, $bbg_filename)); - my $packed_word; - local *INPUT; - - open(INPUT, $bbg_filename) - or die("ERROR: cannot read $bbg_filename!\n"); - - binmode(INPUT); - - read(INPUT, $packed_word, 4) == 4 - or die("ERROR: invalid bbg file format\n"); - - $value = unpack_int32($packed_word, 0); - $endianness = 1; - - unpack_int32($packed_word, $endianness) == $BBG_FILE_MAGIC - or die("ERROR: bbg file magic does not match\n"); - - seek(INPUT, 4, 1); - - # Read data in words of 4 bytes - while (read(INPUT, $packed_word, 4) == 4) - { - # Decode integer in intel byteorder - $value = unpack_int32($packed_word, $endianness); - - # Get record length - read(INPUT, $packed_word, 4); - $length = unpack_int32($packed_word, $endianness); - - if ($value == $GCNO_FUNCTION_TAG) - { - # Get function name - ($value, $function_name) = - read_sles9_bbg_string(*INPUT, $endianness); - - seek(INPUT, $length - $value * 4, 1); - } - elsif ($value == $GCNO_LINES_TAG) - { - # Get linenumber and filename - - # Skip block number - seek(INPUT, 4, 1); - $length -= 4; - - while ($length > 0) - { - read(INPUT, $packed_word, 4); - $lineno = unpack_int32($packed_word, - $endianness); - $length -= 4; - if ($lineno != 0) - { - if (!defined($first_line)) - { - $first_line = $lineno; - } - next; - } - ($blocks, $value) = - read_sles9_bbg_string( - *INPUT, $endianness); - if (!defined($filename)) - { - $filename = $value; - } - $length -= $blocks * 4; - } - # Got a complete data set? - if (defined($filename) && defined($function_name) && - defined($first_line)) - { - $filename = solve_relative_path( - $base_dir, $filename); - # Add it to our result hash - if (defined($result{$filename})) - { - $result{$filename} .= - ",$function_name=$first_line"; - } - else - { - $result{$filename} = - "$function_name=$first_line"; - } - $filename = undef; - $function_name = undef; - $first_line = undef; - } - } - else - { - # Skip other records - seek(INPUT, $length, 1); - } - } - close(INPUT); - - if (!scalar(keys(%result))) - { - die("ERROR: no data found in $bbg_filename!\n"); - } - return %result; -} - - -# -# read_sles9_bbg_string(handle, endianness); -# -# Read a string in 4-byte chunks from HANDLE. -# -# Return (number of 4-byte chunks read, string). -# - -sub read_sles9_bbg_string(*$) -{ - my $handle = $_[0]; - my $endianness = $_[1]; - my $length = 0; - my $string = ""; - my $packed_word; - my $pad; - - read($handle, $packed_word, 4) == 4 - or die("ERROR: reading string\n"); - - $length = unpack_int32($packed_word, $endianness); - $pad = 4 - $length % 4; - - if ($length == 0) - { - return (1, undef); - } - - read($handle, $string, $length) == - $length or die("ERROR: reading string\n"); - seek($handle, $pad, 1); - - return(1 + ($length + $pad) / 4, $string); -} - -# -# unpack_int32(word, endianess) -# -# Interpret 4-byte binary string WORD as signed 32 bit integer in -# endian encoding defined by ENDIANNESS (0=little, 1=big) and return its -# value. -# - -sub unpack_int32($$) -{ - return sprintf("%d", unpack($_[1] ? "N" : "V",$_[0])); + return(\@result, $branches, \@functions); } @@ -1585,10 +1687,11 @@ sub get_gcov_version() $result = $1 << 16 | $2 << 8; } } - if ($version_string =~ /suse/i && $result == 0x30303) + if ($version_string =~ /suse/i && $result == 0x30303 || + $version_string =~ /mandrake/i && $result == 0x30302) { - info("Using compatibility mode for SUSE GCC 3.3.3\n"); - $compatibility = $COMPAT_SLES9; + info("Using compatibility mode for GCC 3.3 (hammer)\n"); + $compatibility = $COMPAT_HAMMER; } return $result; } @@ -1608,7 +1711,7 @@ sub info(@) # Print info string if (defined($output_filename) && ($output_filename eq "-")) { - # Don't interfer with the .info output to STDOUT + # Don't interfere with the .info output to STDOUT printf(STDERR @_); } else @@ -1745,3 +1848,1221 @@ sub apply_config($) } } } + + +# +# get_exclusion_data(filename) +# +# Scan specified source code file for exclusion markers and return +# linenumber -> 1 +# for all lines which should be excluded. +# + +sub get_exclusion_data($) +{ + my ($filename) = @_; + my %list; + my $flag = 0; + local *HANDLE; + + if (!open(HANDLE, "<$filename")) { + warn("WARNING: could not open $filename\n"); + return undef; + } + while () { + if (/$EXCL_STOP/) { + $flag = 0; + } elsif (/$EXCL_START/) { + $flag = 1; + } + if (/$EXCL_LINE/ || $flag) { + $list{$.} = 1; + } + } + close(HANDLE); + + if ($flag) { + warn("WARNING: unterminated exclusion section in $filename\n"); + } + + return \%list; +} + + +# +# apply_exclusion_data(instr, graph) +# +# Remove lines from instr and graph data structures which are marked +# for exclusion in the source code file. +# +# Return adjusted (instr, graph). +# +# graph : file name -> function data +# function data : function name -> line data +# line data : [ line1, line2, ... ] +# +# instr : filename -> line data +# line data : [ line1, line2, ... ] +# + +sub apply_exclusion_data($$) +{ + my ($instr, $graph) = @_; + my $filename; + my %excl_data; + my $excl_read_failed = 0; + + # Collect exclusion marker data + foreach $filename (sort_uniq_lex(keys(%{$graph}), keys(%{$instr}))) { + my $excl = get_exclusion_data($filename); + + # Skip and note if file could not be read + if (!defined($excl)) { + $excl_read_failed = 1; + next; + } + + # Add to collection if there are markers + $excl_data{$filename} = $excl if (keys(%{$excl}) > 0); + } + + # Warn if not all source files could be read + if ($excl_read_failed) { + warn("WARNING: some exclusion markers may be ignored\n"); + } + + # Skip if no markers were found + return ($instr, $graph) if (keys(%excl_data) == 0); + + # Apply exclusion marker data to graph + foreach $filename (keys(%excl_data)) { + my $function_data = $graph->{$filename}; + my $excl = $excl_data{$filename}; + my $function; + + next if (!defined($function_data)); + + foreach $function (keys(%{$function_data})) { + my $line_data = $function_data->{$function}; + my $line; + my @new_data; + + # To be consistent with exclusion parser in non-initial + # case we need to remove a function if the first line + # was excluded + if ($excl->{$line_data->[0]}) { + delete($function_data->{$function}); + next; + } + # Copy only lines which are not excluded + foreach $line (@{$line_data}) { + push(@new_data, $line) if (!$excl->{$line}); + } + + # Store modified list + if (scalar(@new_data) > 0) { + $function_data->{$function} = \@new_data; + } else { + # All of this function was excluded + delete($function_data->{$function}); + } + } + + # Check if all functions of this file were excluded + if (keys(%{$function_data}) == 0) { + delete($graph->{$filename}); + } + } + + # Apply exclusion marker data to instr + foreach $filename (keys(%excl_data)) { + my $line_data = $instr->{$filename}; + my $excl = $excl_data{$filename}; + my $line; + my @new_data; + + next if (!defined($line_data)); + + # Copy only lines which are not excluded + foreach $line (@{$line_data}) { + push(@new_data, $line) if (!$excl->{$line}); + } + + # Store modified list + if (scalar(@new_data) > 0) { + $instr->{$filename} = \@new_data; + } else { + # All of this file was excluded + delete($instr->{$filename}); + } + } + + return ($instr, $graph); +} + + +sub process_graphfile($$) +{ + my ($file, $dir) = @_; + my $graph_filename = $file; + my $graph_dir; + my $graph_basename; + my $source_dir; + my $base_dir; + my $graph; + my $instr; + my $filename; + local *INFO_HANDLE; + + info("Processing %s\n", abs2rel($file, $dir)); + + # Get path to data file in absolute and normalized form (begins with /, + # contains no more ../ or ./) + $graph_filename = solve_relative_path($cwd, $graph_filename); + + # Get directory and basename of data file + ($graph_dir, $graph_basename) = split_filename($graph_filename); + + # avoid files from .libs dirs + if ($compat_libtool && $graph_dir =~ m/(.*)\/\.libs$/) { + $source_dir = $1; + } else { + $source_dir = $graph_dir; + } + + # Construct base_dir for current file + if ($base_directory) + { + $base_dir = $base_directory; + } + else + { + $base_dir = $source_dir; + } + + if ($gcov_version < $GCOV_VERSION_3_4_0) + { + if (defined($compatibility) && $compatibility eq $COMPAT_HAMMER) + { + ($instr, $graph) = read_bbg($graph_filename, $base_dir); + } + else + { + ($instr, $graph) = read_bb($graph_filename, $base_dir); + } + } + else + { + ($instr, $graph) = read_gcno($graph_filename, $base_dir); + } + + if (!$no_markers) { + # Apply exclusion marker data to graph file data + ($instr, $graph) = apply_exclusion_data($instr, $graph); + } + + # Check whether we're writing to a single file + if ($output_filename) + { + if ($output_filename eq "-") + { + *INFO_HANDLE = *STDOUT; + } + else + { + # Append to output file + open(INFO_HANDLE, ">>$output_filename") + or die("ERROR: cannot write to ". + "$output_filename!\n"); + } + } + else + { + # Open .info file for output + open(INFO_HANDLE, ">$graph_filename.info") + or die("ERROR: cannot create $graph_filename.info!\n"); + } + + # Write test name + printf(INFO_HANDLE "TN:%s\n", $test_name); + foreach $filename (sort(keys(%{$instr}))) + { + my $funcdata = $graph->{$filename}; + my $line; + my $linedata; + + print(INFO_HANDLE "SF:$filename\n"); + + if (defined($funcdata)) { + my @functions = sort {$funcdata->{$a}->[0] <=> + $funcdata->{$b}->[0]} + keys(%{$funcdata}); + my $func; + + # Gather list of instrumented lines and functions + foreach $func (@functions) { + $linedata = $funcdata->{$func}; + + # Print function name and starting line + print(INFO_HANDLE "FN:".$linedata->[0]. + ",".filter_fn_name($func)."\n"); + } + # Print zero function coverage data + foreach $func (@functions) { + print(INFO_HANDLE "FNDA:0,". + filter_fn_name($func)."\n"); + } + # Print function summary + print(INFO_HANDLE "FNF:".scalar(@functions)."\n"); + print(INFO_HANDLE "FNH:0\n"); + } + # Print zero line coverage data + foreach $line (@{$instr->{$filename}}) { + print(INFO_HANDLE "DA:$line,0\n"); + } + # Print line summary + print(INFO_HANDLE "LF:".scalar(@{$instr->{$filename}})."\n"); + print(INFO_HANDLE "LH:0\n"); + + print(INFO_HANDLE "end_of_record\n"); + } + if (!($output_filename && ($output_filename eq "-"))) + { + close(INFO_HANDLE); + } +} + +sub filter_fn_name($) +{ + my ($fn) = @_; + + # Remove characters used internally as function name delimiters + $fn =~ s/[,=]/_/g; + + return $fn; +} + +sub warn_handler($) +{ + my ($msg) = @_; + + warn("$tool_name: $msg"); +} + +sub die_handler($) +{ + my ($msg) = @_; + + die("$tool_name: $msg"); +} + + +# +# graph_error(filename, message) +# +# Print message about error in graph file. If ignore_graph_error is set, return. +# Otherwise abort. +# + +sub graph_error($$) +{ + my ($filename, $msg) = @_; + + if ($ignore[$ERROR_GRAPH]) { + warn("WARNING: $filename: $msg - skipping\n"); + return; + } + die("ERROR: $filename: $msg\n"); +} + +# +# graph_expect(description) +# +# If debug is set to a non-zero value, print the specified description of what +# is expected to be read next from the graph file. +# + +sub graph_expect($) +{ + my ($msg) = @_; + + if (!$debug || !defined($msg)) { + return; + } + + print(STDERR "DEBUG: expecting $msg\n"); +} + +# +# graph_read(handle, bytes[, description]) +# +# Read and return the specified number of bytes from handle. Return undef +# if the number of bytes could not be read. +# + +sub graph_read(*$;$) +{ + my ($handle, $length, $desc) = @_; + my $data; + my $result; + + graph_expect($desc); + $result = read($handle, $data, $length); + if ($debug) { + my $ascii = ""; + my $hex = ""; + my $i; + + print(STDERR "DEBUG: read($length)=$result: "); + for ($i = 0; $i < length($data); $i++) { + my $c = substr($data, $i, 1);; + my $n = ord($c); + + $hex .= sprintf("%02x ", $n); + if ($n >= 32 && $n <= 127) { + $ascii .= $c; + } else { + $ascii .= "."; + } + } + print(STDERR "$hex |$ascii|"); + print(STDERR "\n"); + } + if ($result != $length) { + return undef; + } + return $data; +} + +# +# graph_skip(handle, bytes[, description]) +# +# Read and discard the specified number of bytes from handle. Return non-zero +# if bytes could be read, zero otherwise. +# + +sub graph_skip(*$;$) +{ + my ($handle, $length, $desc) = @_; + + if (defined(graph_read($handle, $length, $desc))) { + return 1; + } + return 0; +} + +# +# sort_uniq(list) +# +# Return list in numerically ascending order and without duplicate entries. +# + +sub sort_uniq(@) +{ + my (@list) = @_; + my %hash; + + foreach (@list) { + $hash{$_} = 1; + } + return sort { $a <=> $b } keys(%hash); +} + +# +# sort_uniq_lex(list) +# +# Return list in lexically ascending order and without duplicate entries. +# + +sub sort_uniq_lex(@) +{ + my (@list) = @_; + my %hash; + + foreach (@list) { + $hash{$_} = 1; + } + return sort keys(%hash); +} + +# +# graph_cleanup(graph) +# +# Remove entries for functions with no lines. Remove duplicate line numbers. +# Sort list of line numbers numerically ascending. +# + +sub graph_cleanup($) +{ + my ($graph) = @_; + my $filename; + + foreach $filename (keys(%{$graph})) { + my $per_file = $graph->{$filename}; + my $function; + + foreach $function (keys(%{$per_file})) { + my $lines = $per_file->{$function}; + + if (scalar(@$lines) == 0) { + # Remove empty function + delete($per_file->{$function}); + next; + } + # Normalize list + $per_file->{$function} = [ sort_uniq(@$lines) ]; + } + if (scalar(keys(%{$per_file})) == 0) { + # Remove empty file + delete($graph->{$filename}); + } + } +} + +# +# graph_find_base(bb) +# +# Try to identify the filename which is the base source file for the +# specified bb data. +# + +sub graph_find_base($) +{ + my ($bb) = @_; + my %file_count; + my $basefile; + my $file; + my $func; + my $filedata; + my $count; + my $num; + + # Identify base name for this bb data. + foreach $func (keys(%{$bb})) { + $filedata = $bb->{$func}; + + foreach $file (keys(%{$filedata})) { + $count = $file_count{$file}; + + # Count file occurrence + $file_count{$file} = defined($count) ? $count + 1 : 1; + } + } + $count = 0; + $num = 0; + foreach $file (keys(%file_count)) { + if ($file_count{$file} > $count) { + # The file that contains code for the most functions + # is likely the base file + $count = $file_count{$file}; + $num = 1; + $basefile = $file; + } elsif ($file_count{$file} == $count) { + # If more than one file could be the basefile, we + # don't have a basefile + $basefile = undef; + } + } + + return $basefile; +} + +# +# graph_from_bb(bb, fileorder, bb_filename) +# +# Convert data from bb to the graph format and list of instrumented lines. +# Returns (instr, graph). +# +# bb : function name -> file data +# : undef -> file order +# file data : filename -> line data +# line data : [ line1, line2, ... ] +# +# file order : function name -> [ filename1, filename2, ... ] +# +# graph : file name -> function data +# function data : function name -> line data +# line data : [ line1, line2, ... ] +# +# instr : filename -> line data +# line data : [ line1, line2, ... ] +# + +sub graph_from_bb($$$) +{ + my ($bb, $fileorder, $bb_filename) = @_; + my $graph = {}; + my $instr = {}; + my $basefile; + my $file; + my $func; + my $filedata; + my $linedata; + my $order; + + $basefile = graph_find_base($bb); + # Create graph structure + foreach $func (keys(%{$bb})) { + $filedata = $bb->{$func}; + $order = $fileorder->{$func}; + + # Account for lines in functions + if (defined($basefile) && defined($filedata->{$basefile})) { + # If the basefile contributes to this function, + # account this function to the basefile. + $graph->{$basefile}->{$func} = $filedata->{$basefile}; + } else { + # If the basefile does not contribute to this function, + # account this function to the first file contributing + # lines. + $graph->{$order->[0]}->{$func} = + $filedata->{$order->[0]}; + } + + foreach $file (keys(%{$filedata})) { + # Account for instrumented lines + $linedata = $filedata->{$file}; + push(@{$instr->{$file}}, @$linedata); + } + } + # Clean up array of instrumented lines + foreach $file (keys(%{$instr})) { + $instr->{$file} = [ sort_uniq(@{$instr->{$file}}) ]; + } + + return ($instr, $graph); +} + +# +# graph_add_order(fileorder, function, filename) +# +# Add an entry for filename to the fileorder data set for function. +# + +sub graph_add_order($$$) +{ + my ($fileorder, $function, $filename) = @_; + my $item; + my $list; + + $list = $fileorder->{$function}; + foreach $item (@$list) { + if ($item eq $filename) { + return; + } + } + push(@$list, $filename); + $fileorder->{$function} = $list; +} +# +# read_bb_word(handle[, description]) +# +# Read and return a word in .bb format from handle. +# + +sub read_bb_word(*;$) +{ + my ($handle, $desc) = @_; + + return graph_read($handle, 4, $desc); +} + +# +# read_bb_value(handle[, description]) +# +# Read a word in .bb format from handle and return the word and its integer +# value. +# + +sub read_bb_value(*;$) +{ + my ($handle, $desc) = @_; + my $word; + + $word = read_bb_word($handle, $desc); + return undef if (!defined($word)); + + return ($word, unpack("V", $word)); +} + +# +# read_bb_string(handle, delimiter) +# +# Read and return a string in .bb format from handle up to the specified +# delimiter value. +# + +sub read_bb_string(*$) +{ + my ($handle, $delimiter) = @_; + my $word; + my $value; + my $string = ""; + + graph_expect("string"); + do { + ($word, $value) = read_bb_value($handle, "string or delimiter"); + return undef if (!defined($value)); + if ($value != $delimiter) { + $string .= $word; + } + } while ($value != $delimiter); + $string =~ s/\0//g; + + return $string; +} + +# +# read_bb(filename, base_dir) +# +# Read the contents of the specified .bb file and return (instr, graph), where: +# +# instr : filename -> line data +# line data : [ line1, line2, ... ] +# +# graph : filename -> file_data +# file_data : function name -> line_data +# line_data : [ line1, line2, ... ] +# +# Relative filenames are converted to absolute form using base_dir as +# base directory. See the gcov info pages of gcc 2.95 for a description of +# the .bb file format. +# + +sub read_bb($$) +{ + my ($bb_filename, $base) = @_; + my $minus_one = 0x80000001; + my $minus_two = 0x80000002; + my $value; + my $filename; + my $function; + my $bb = {}; + my $fileorder = {}; + my $instr; + my $graph; + local *HANDLE; + + open(HANDLE, "<$bb_filename") or goto open_error; + binmode(HANDLE); + while (!eof(HANDLE)) { + $value = read_bb_value(*HANDLE, "data word"); + goto incomplete if (!defined($value)); + if ($value == $minus_one) { + # Source file name + graph_expect("filename"); + $filename = read_bb_string(*HANDLE, $minus_one); + goto incomplete if (!defined($filename)); + if ($filename ne "") { + $filename = solve_relative_path($base, + $filename); + } + } elsif ($value == $minus_two) { + # Function name + graph_expect("function name"); + $function = read_bb_string(*HANDLE, $minus_two); + goto incomplete if (!defined($function)); + } elsif ($value > 0) { + # Line number + if (!defined($filename) || !defined($function)) { + warn("WARNING: unassigned line number ". + "$value\n"); + next; + } + push(@{$bb->{$function}->{$filename}}, $value); + graph_add_order($fileorder, $function, $filename); + } + } + close(HANDLE); + ($instr, $graph) = graph_from_bb($bb, $fileorder, $bb_filename); + graph_cleanup($graph); + + return ($instr, $graph); + +open_error: + graph_error($bb_filename, "could not open file"); + return undef; +incomplete: + graph_error($bb_filename, "reached unexpected end of file"); + return undef; +} + +# +# read_bbg_word(handle[, description]) +# +# Read and return a word in .bbg format. +# + +sub read_bbg_word(*;$) +{ + my ($handle, $desc) = @_; + + return graph_read($handle, 4, $desc); +} + +# +# read_bbg_value(handle[, description]) +# +# Read a word in .bbg format from handle and return its integer value. +# + +sub read_bbg_value(*;$) +{ + my ($handle, $desc) = @_; + my $word; + + $word = read_bbg_word($handle, $desc); + return undef if (!defined($word)); + + return unpack("N", $word); +} + +# +# read_bbg_string(handle) +# +# Read and return a string in .bbg format. +# + +sub read_bbg_string(*) +{ + my ($handle, $desc) = @_; + my $length; + my $string; + + graph_expect("string"); + # Read string length + $length = read_bbg_value($handle, "string length"); + return undef if (!defined($length)); + if ($length == 0) { + return ""; + } + # Read string + $string = graph_read($handle, $length, "string"); + return undef if (!defined($string)); + # Skip padding + graph_skip($handle, 4 - $length % 4, "string padding") or return undef; + + return $string; +} + +# +# read_bbg_lines_record(handle, bbg_filename, bb, fileorder, filename, +# function, base) +# +# Read a bbg format lines record from handle and add the relevant data to +# bb and fileorder. Return filename on success, undef on error. +# + +sub read_bbg_lines_record(*$$$$$$) +{ + my ($handle, $bbg_filename, $bb, $fileorder, $filename, $function, + $base) = @_; + my $string; + my $lineno; + + graph_expect("lines record"); + # Skip basic block index + graph_skip($handle, 4, "basic block index") or return undef; + while (1) { + # Read line number + $lineno = read_bbg_value($handle, "line number"); + return undef if (!defined($lineno)); + if ($lineno == 0) { + # Got a marker for a new filename + graph_expect("filename"); + $string = read_bbg_string($handle); + return undef if (!defined($string)); + # Check for end of record + if ($string eq "") { + return $filename; + } + $filename = solve_relative_path($base, $string); + next; + } + # Got an actual line number + if (!defined($filename)) { + warn("WARNING: unassigned line number in ". + "$bbg_filename\n"); + next; + } + push(@{$bb->{$function}->{$filename}}, $lineno); + graph_add_order($fileorder, $function, $filename); + } +} + +# +# read_bbg(filename, base_dir) +# +# Read the contents of the specified .bbg file and return the following mapping: +# graph: filename -> file_data +# file_data: function name -> line_data +# line_data: [ line1, line2, ... ] +# +# Relative filenames are converted to absolute form using base_dir as +# base directory. See the gcov-io.h file in the SLES 9 gcc 3.3.3 source code +# for a description of the .bbg format. +# + +sub read_bbg($$) +{ + my ($bbg_filename, $base) = @_; + my $file_magic = 0x67626267; + my $tag_function = 0x01000000; + my $tag_lines = 0x01450000; + my $word; + my $tag; + my $length; + my $function; + my $filename; + my $bb = {}; + my $fileorder = {}; + my $instr; + my $graph; + local *HANDLE; + + open(HANDLE, "<$bbg_filename") or goto open_error; + binmode(HANDLE); + # Read magic + $word = read_bbg_value(*HANDLE, "file magic"); + goto incomplete if (!defined($word)); + # Check magic + if ($word != $file_magic) { + goto magic_error; + } + # Skip version + graph_skip(*HANDLE, 4, "version") or goto incomplete; + while (!eof(HANDLE)) { + # Read record tag + $tag = read_bbg_value(*HANDLE, "record tag"); + goto incomplete if (!defined($tag)); + # Read record length + $length = read_bbg_value(*HANDLE, "record length"); + goto incomplete if (!defined($tag)); + if ($tag == $tag_function) { + graph_expect("function record"); + # Read function name + graph_expect("function name"); + $function = read_bbg_string(*HANDLE); + goto incomplete if (!defined($function)); + $filename = undef; + # Skip function checksum + graph_skip(*HANDLE, 4, "function checksum") + or goto incomplete; + } elsif ($tag == $tag_lines) { + # Read lines record + $filename = read_bbg_lines_record(HANDLE, $bbg_filename, + $bb, $fileorder, $filename, + $function, $base); + goto incomplete if (!defined($filename)); + } else { + # Skip record contents + graph_skip(*HANDLE, $length, "unhandled record") + or goto incomplete; + } + } + close(HANDLE); + ($instr, $graph) = graph_from_bb($bb, $fileorder, $bbg_filename); + graph_cleanup($graph); + + return ($instr, $graph); + +open_error: + graph_error($bbg_filename, "could not open file"); + return undef; +incomplete: + graph_error($bbg_filename, "reached unexpected end of file"); + return undef; +magic_error: + graph_error($bbg_filename, "found unrecognized bbg file magic"); + return undef; +} + +# +# read_gcno_word(handle[, description]) +# +# Read and return a word in .gcno format. +# + +sub read_gcno_word(*;$) +{ + my ($handle, $desc) = @_; + + return graph_read($handle, 4, $desc); +} + +# +# read_gcno_value(handle, big_endian[, description]) +# +# Read a word in .gcno format from handle and return its integer value +# according to the specified endianness. +# + +sub read_gcno_value(*$;$) +{ + my ($handle, $big_endian, $desc) = @_; + my $word; + + $word = read_gcno_word($handle, $desc); + return undef if (!defined($word)); + if ($big_endian) { + return unpack("N", $word); + } else { + return unpack("V", $word); + } +} + +# +# read_gcno_string(handle, big_endian) +# +# Read and return a string in .gcno format. +# + +sub read_gcno_string(*$) +{ + my ($handle, $big_endian) = @_; + my $length; + my $string; + + graph_expect("string"); + # Read string length + $length = read_gcno_value($handle, $big_endian, "string length"); + return undef if (!defined($length)); + if ($length == 0) { + return ""; + } + $length *= 4; + # Read string + $string = graph_read($handle, $length, "string and padding"); + return undef if (!defined($string)); + $string =~ s/\0//g; + + return $string; +} + +# +# read_gcno_lines_record(handle, gcno_filename, bb, fileorder, filename, +# function, base, big_endian) +# +# Read a gcno format lines record from handle and add the relevant data to +# bb and fileorder. Return filename on success, undef on error. +# + +sub read_gcno_lines_record(*$$$$$$$) +{ + my ($handle, $gcno_filename, $bb, $fileorder, $filename, $function, + $base, $big_endian) = @_; + my $string; + my $lineno; + + graph_expect("lines record"); + # Skip basic block index + graph_skip($handle, 4, "basic block index") or return undef; + while (1) { + # Read line number + $lineno = read_gcno_value($handle, $big_endian, "line number"); + return undef if (!defined($lineno)); + if ($lineno == 0) { + # Got a marker for a new filename + graph_expect("filename"); + $string = read_gcno_string($handle, $big_endian); + return undef if (!defined($string)); + # Check for end of record + if ($string eq "") { + return $filename; + } + $filename = solve_relative_path($base, $string); + next; + } + # Got an actual line number + if (!defined($filename)) { + warn("WARNING: unassigned line number in ". + "$gcno_filename\n"); + next; + } + # Add to list + push(@{$bb->{$function}->{$filename}}, $lineno); + graph_add_order($fileorder, $function, $filename); + } +} + +# +# read_gcno_function_record(handle, graph, base, big_endian) +# +# Read a gcno format function record from handle and add the relevant data +# to graph. Return (filename, function) on success, undef on error. +# + +sub read_gcno_function_record(*$$$$) +{ + my ($handle, $bb, $fileorder, $base, $big_endian) = @_; + my $filename; + my $function; + my $lineno; + my $lines; + + graph_expect("function record"); + # Skip ident and checksum + graph_skip($handle, 8, "function ident and checksum") or return undef; + # Read function name + graph_expect("function name"); + $function = read_gcno_string($handle, $big_endian); + return undef if (!defined($function)); + # Read filename + graph_expect("filename"); + $filename = read_gcno_string($handle, $big_endian); + return undef if (!defined($filename)); + $filename = solve_relative_path($base, $filename); + # Read first line number + $lineno = read_gcno_value($handle, $big_endian, "initial line number"); + return undef if (!defined($lineno)); + # Add to list + push(@{$bb->{$function}->{$filename}}, $lineno); + graph_add_order($fileorder, $function, $filename); + + return ($filename, $function); +} + +# +# read_gcno(filename, base_dir) +# +# Read the contents of the specified .gcno file and return the following +# mapping: +# graph: filename -> file_data +# file_data: function name -> line_data +# line_data: [ line1, line2, ... ] +# +# Relative filenames are converted to absolute form using base_dir as +# base directory. See the gcov-io.h file in the gcc 3.3 source code +# for a description of the .gcno format. +# + +sub read_gcno($$) +{ + my ($gcno_filename, $base) = @_; + my $file_magic = 0x67636e6f; + my $tag_function = 0x01000000; + my $tag_lines = 0x01450000; + my $big_endian; + my $word; + my $tag; + my $length; + my $filename; + my $function; + my $bb = {}; + my $fileorder = {}; + my $instr; + my $graph; + local *HANDLE; + + open(HANDLE, "<$gcno_filename") or goto open_error; + binmode(HANDLE); + # Read magic + $word = read_gcno_word(*HANDLE, "file magic"); + goto incomplete if (!defined($word)); + # Determine file endianness + if (unpack("N", $word) == $file_magic) { + $big_endian = 1; + } elsif (unpack("V", $word) == $file_magic) { + $big_endian = 0; + } else { + goto magic_error; + } + # Skip version and stamp + graph_skip(*HANDLE, 8, "version and stamp") or goto incomplete; + while (!eof(HANDLE)) { + my $next_pos; + my $curr_pos; + + # Read record tag + $tag = read_gcno_value(*HANDLE, $big_endian, "record tag"); + goto incomplete if (!defined($tag)); + # Read record length + $length = read_gcno_value(*HANDLE, $big_endian, + "record length"); + goto incomplete if (!defined($length)); + # Convert length to bytes + $length *= 4; + # Calculate start of next record + $next_pos = tell(HANDLE); + goto tell_error if ($next_pos == -1); + $next_pos += $length; + # Process record + if ($tag == $tag_function) { + ($filename, $function) = read_gcno_function_record( + *HANDLE, $bb, $fileorder, $base, $big_endian); + goto incomplete if (!defined($function)); + } elsif ($tag == $tag_lines) { + # Read lines record + $filename = read_gcno_lines_record(*HANDLE, + $gcno_filename, $bb, $fileorder, + $filename, $function, $base, + $big_endian); + goto incomplete if (!defined($filename)); + } else { + # Skip record contents + graph_skip(*HANDLE, $length, "unhandled record") + or goto incomplete; + } + # Ensure that we are at the start of the next record + $curr_pos = tell(HANDLE); + goto tell_error if ($curr_pos == -1); + next if ($curr_pos == $next_pos); + goto record_error if ($curr_pos > $next_pos); + graph_skip(*HANDLE, $next_pos - $curr_pos, + "unhandled record content") + or goto incomplete; + } + close(HANDLE); + ($instr, $graph) = graph_from_bb($bb, $fileorder, $gcno_filename); + graph_cleanup($graph); + + return ($instr, $graph); + +open_error: + graph_error($gcno_filename, "could not open file"); + return undef; +incomplete: + graph_error($gcno_filename, "reached unexpected end of file"); + return undef; +magic_error: + graph_error($gcno_filename, "found unrecognized gcno file magic"); + return undef; +tell_error: + graph_error($gcno_filename, "could not determine file position"); + return undef; +record_error: + graph_error($gcno_filename, "found unrecognized record format"); + return undef; +} + +sub debug($) +{ + my ($msg) = @_; + + return if (!$debug); + print(STDERR "DEBUG: $msg"); +} + +# +# get_gcov_capabilities +# +# Determine the list of available gcov options. +# + +sub get_gcov_capabilities() +{ + my $help = `$gcov_tool --help`; + my %capabilities; + + foreach (split(/\n/, $help)) { + next if (!/--(\S+)/); + next if ($1 eq 'help'); + next if ($1 eq 'version'); + next if ($1 eq 'object-directory'); + + $capabilities{$1} = 1; + debug("gcov has capability '$1'\n"); + } + + return \%capabilities; +} diff --git a/utils/lcov/lcov b/utils/lcov/lcov index 706bb5101..4e392ffa3 100755 --- a/utils/lcov/lcov +++ b/utils/lcov/lcov @@ -1,6 +1,6 @@ #!/usr/bin/perl -w # -# Copyright (c) International Business Machines Corp., 2002 +# Copyright (c) International Business Machines Corp., 2002,2010 # # 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 @@ -56,38 +56,47 @@ # modprobe before insmod (needed for 2.6) # 2004-03-30 / Peter Oberparleiter: added --path option # 2004-08-09 / Peter Oberparleiter: added configuration file support +# 2008-08-13 / Peter Oberparleiter: added function coverage support # use strict; -use File::Basename; +use File::Basename; +use File::Path; +use File::Find; +use File::Temp qw /tempdir/; +use File::Spec::Functions qw /abs2rel canonpath catdir catfile catpath + file_name_is_absolute rootdir splitdir splitpath/; use Getopt::Long; +use Cwd qw /abs_path getcwd/; # Global constants -our $lcov_version = "LTP GCOV extension version 1.4"; +our $lcov_version = 'LCOV version 1.9'; our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; - -# Names of the GCOV kernel module -our @gcovmod = ("gcov-prof", "gcov-proc"); +our $tool_name = basename($0); # Directory containing gcov kernel files -our $gcov_dir = "/proc/gcov"; - -# The location of the insmod tool -our $insmod_tool = "/sbin/insmod"; - -# The location of the modprobe tool -our $modprobe_tool = "/sbin/modprobe"; - -# The location of the rmmod tool -our $rmmod_tool = "/sbin/rmmod"; +our $gcov_dir; # Where to create temporary directories -our $tmp_dir = "/tmp"; +our $tmp_dir; -# How to prefix a temporary directory name -our $tmp_prefix = "tmpdir"; +# Internal constants +our $GKV_PROC = 0; # gcov-kernel data in /proc via external patch +our $GKV_SYS = 1; # gcov-kernel data in /sys via vanilla 2.6.31+ +our @GKV_NAME = ( "external", "upstream" ); +our $pkg_gkv_file = ".gcov_kernel_version"; +our $pkg_build_file = ".build_directory"; +our $BR_BLOCK = 0; +our $BR_BRANCH = 1; +our $BR_TAKEN = 2; +our $BR_VEC_ENTRIES = 3; +our $BR_VEC_WIDTH = 32; + +# Branch data combination types +our $BR_SUB = 0; +our $BR_ADD = 1; # Prototypes sub print_usage(*); @@ -96,10 +105,12 @@ sub userspace_reset(); sub userspace_capture(); sub kernel_reset(); sub kernel_capture(); +sub kernel_capture_initial(); +sub package_capture(); sub add_traces(); sub read_info_file($); sub get_info_entry($); -sub set_info_entry($$$$$;$$); +sub set_info_entry($$$$$$$$$;$$$$$$); sub add_counts($$); sub merge_checksums($$$); sub combine_info_entries($$$); @@ -114,13 +125,20 @@ sub diff(); sub system_no_output($@); sub read_config($); sub apply_config($); - sub info(@); -sub unload_module($); -sub check_and_load_kernel_module(); sub create_temp_dir(); sub transform_pattern($); - +sub warn_handler($); +sub die_handler($); +sub abort_handler($); +sub temp_cleanup(); +sub setup_gkv(); +sub get_overall_line($$$$); +sub print_overall_rate($$$$$$$$$); +sub lcov_geninfo(@); +sub create_package($$$;$); +sub get_func_found_and_hit($); +sub br_ivec_get($$); # Global variables & initialization our @directory; # Specifies where to get coverage data from @@ -134,28 +152,60 @@ our $reset; # If set, reset all coverage data to zero our $capture; # If set, capture data our $output_filename; # Name for file to write coverage data to our $test_name = ""; # Test case name -our $source_dirs = ""; our $quiet = ""; # If set, suppress information messages our $help; # Help option flag our $version; # Version option flag -our $nochecksum =""; # If set, don't calculate a checksum for each line our $convert_filenames; # If set, convert filenames when applying diff our $strip; # If set, strip leading directories when applying diff -our $need_unload; # If set, unload gcov kernel module our $temp_dir_name; # Name of temporary directory our $cwd = `pwd`; # Current working directory our $to_file; # If set, indicates that output is written to a file our $follow; # If set, indicates that find shall follow links our $diff_path = ""; # Path removed from tracefile when applying diff +our $base_directory; # Base directory (cwd of gcc during compilation) +our $checksum; # If set, calculate a checksum for each line +our $no_checksum; # If set, don't calculate a checksum for each line +our $compat_libtool; # If set, indicates that libtool mode is to be enabled +our $no_compat_libtool; # If set, indicates that libtool mode is to be disabled +our $gcov_tool; +our $ignore_errors; +our $initial; +our $no_recursion = 0; +our $to_package; +our $from_package; +our $maxdepth; +our $no_markers; our $config; # Configuration file contents chomp($cwd); our $tool_dir = dirname($0); # Directory where genhtml tool is installed +our @temp_dirs; +our $gcov_gkv; # gcov kernel support version found on machine +our $opt_derive_func_data; +our $opt_debug; +our $opt_list_full_path; +our $opt_no_list_full_path; +our $opt_list_width = 80; +our $opt_list_truncate_max = 20; +our $ln_overall_found; +our $ln_overall_hit; +our $fn_overall_found; +our $fn_overall_hit; +our $br_overall_found; +our $br_overall_hit; # # Code entry point # +$SIG{__WARN__} = \&warn_handler; +$SIG{__DIE__} = \&die_handler; +$SIG{'INT'} = \&abort_handler; +$SIG{'QUIT'} = \&abort_handler; + +# Prettify version string +$lcov_version =~ s/\$\s*Revision\s*:?\s*(\S+)\s*\$/$1/; + # Add current working directory if $tool_dir is not already an absolute path if (! ($tool_dir =~ /^\/(.*)$/)) { @@ -163,7 +213,7 @@ if (! ($tool_dir =~ /^\/(.*)$/)) } # Read configuration file if available -if (-r $ENV{"HOME"}."/.lcovrc") +if (defined($ENV{"HOME"}) && (-r $ENV{"HOME"}."/.lcovrc")) { $config = read_config($ENV{"HOME"}."/.lcovrc"); } @@ -177,38 +227,74 @@ if ($config) # Copy configuration file values to variables apply_config({ "lcov_gcov_dir" => \$gcov_dir, - "lcov_insmod_tool" => \$insmod_tool, - "lcov_modprobe_tool" => \$modprobe_tool, - "lcov_rmmod_tool" => \$rmmod_tool, - "lcov_tmp_dir" => \$tmp_dir}); + "lcov_tmp_dir" => \$tmp_dir, + "lcov_list_full_path" => \$opt_list_full_path, + "lcov_list_width" => \$opt_list_width, + "lcov_list_truncate_max"=> \$opt_list_truncate_max, + }); } # Parse command line options if (!GetOptions("directory|d|di=s" => \@directory, - "add-tracefile=s" => \@add_tracefile, - "list=s" => \$list, - "kernel-directory=s" => \@kernel_directory, - "extract=s" => \$extract, - "remove=s" => \$remove, + "add-tracefile|a=s" => \@add_tracefile, + "list|l=s" => \$list, + "kernel-directory|k=s" => \@kernel_directory, + "extract|e=s" => \$extract, + "remove|r=s" => \$remove, "diff=s" => \$diff, - "no-checksum" => \$nochecksum, "convert-filenames" => \$convert_filenames, "strip=i" => \$strip, "capture|c" => \$capture, - "output-file=s" => \$output_filename, - "test-name=s" => \$test_name, - "source-dirs=s" => \$source_dirs, - "zerocounters" => \$reset, - "quiet" => \$quiet, - "help" => \$help, - "version" => \$version, - "follow" => \$follow, - "path=s" => \$diff_path + "output-file|o=s" => \$output_filename, + "test-name|t=s" => \$test_name, + "zerocounters|z" => \$reset, + "quiet|q" => \$quiet, + "help|h|?" => \$help, + "version|v" => \$version, + "follow|f" => \$follow, + "path=s" => \$diff_path, + "base-directory|b=s" => \$base_directory, + "checksum" => \$checksum, + "no-checksum" => \$no_checksum, + "compat-libtool" => \$compat_libtool, + "no-compat-libtool" => \$no_compat_libtool, + "gcov-tool=s" => \$gcov_tool, + "ignore-errors=s" => \$ignore_errors, + "initial|i" => \$initial, + "no-recursion" => \$no_recursion, + "to-package=s" => \$to_package, + "from-package=s" => \$from_package, + "no-markers" => \$no_markers, + "derive-func-data" => \$opt_derive_func_data, + "debug" => \$opt_debug, + "list-full-path" => \$opt_list_full_path, + "no-list-full-path" => \$opt_no_list_full_path, )) { - print_usage(*STDERR); + print(STDERR "Use $tool_name --help to get usage information\n"); exit(1); } +else +{ + # Merge options + if (defined($no_checksum)) + { + $checksum = ($no_checksum ? 0 : 1); + $no_checksum = undef; + } + + if (defined($no_compat_libtool)) + { + $compat_libtool = ($no_compat_libtool ? 0 : 1); + $no_compat_libtool = undef; + } + + if (defined($opt_no_list_full_path)) + { + $opt_list_full_path = ($opt_no_list_full_path ? 0 : 1); + $opt_no_list_full_path = undef; + } +} # Check for help option if ($help) @@ -220,21 +306,45 @@ if ($help) # Check for version option if ($version) { - print("$lcov_version\n"); + print("$tool_name: $lcov_version\n"); exit(0); } +# Check list width option +if ($opt_list_width <= 40) { + die("ERROR: lcov_list_width parameter out of range (needs to be ". + "larger than 40)\n"); +} + # Normalize --path text $diff_path =~ s/\/$//; +if ($follow) +{ + $follow = "-follow"; +} +else +{ + $follow = ""; +} + +if ($no_recursion) +{ + $maxdepth = "-maxdepth 1"; +} +else +{ + $maxdepth = ""; +} + # Check for valid options check_options(); # Only --extract, --remove and --diff allow unnamed parameters if (@ARGV && !($extract || $remove || $diff)) { - print_usage(*STDERR); - exit(1); + die("Extra parameter found: '".join(" ", @ARGV)."'\n". + "Use $tool_name --help to get usage information\n"); } # Check for output filename @@ -249,6 +359,11 @@ if ($capture) } } +# Determine kernel directory for gcov data +if (!$from_package && !@directory && ($capture || $reset)) { + ($gcov_gkv, $gcov_dir) = setup_gkv(); +} + # Check for requested functionality if ($reset) { @@ -264,27 +379,40 @@ if ($reset) } elsif ($capture) { - # Differentiate between user space and kernel - if (@directory) - { + # Capture source can be user space, kernel or package + if ($from_package) { + package_capture(); + } elsif (@directory) { userspace_capture(); - } - else - { - kernel_capture(); + } else { + if ($initial) { + if (defined($to_package)) { + die("ERROR: --initial cannot be used together ". + "with --to-package\n"); + } + kernel_capture_initial(); + } else { + kernel_capture(); + } } } elsif (@add_tracefile) { - add_traces(); + ($ln_overall_found, $ln_overall_hit, + $fn_overall_found, $fn_overall_hit, + $br_overall_found, $br_overall_hit) = add_traces(); } elsif ($remove) { - remove(); + ($ln_overall_found, $ln_overall_hit, + $fn_overall_found, $fn_overall_hit, + $br_overall_found, $br_overall_hit) = remove(); } elsif ($extract) { - extract(); + ($ln_overall_found, $ln_overall_hit, + $fn_overall_found, $fn_overall_hit, + $br_overall_found, $br_overall_hit) = extract(); } elsif ($list) { @@ -294,20 +422,25 @@ elsif ($diff) { if (scalar(@ARGV) != 1) { - die("ERROR: option --diff requires one additional argument!\n"); + die("ERROR: option --diff requires one additional argument!\n". + "Use $tool_name --help to get usage information\n"); } - diff(); + ($ln_overall_found, $ln_overall_hit, + $fn_overall_found, $fn_overall_hit, + $br_overall_found, $br_overall_hit) = diff(); } -info("Done.\n"); +temp_cleanup(); + +if (defined($ln_overall_found)) { + print_overall_rate(1, $ln_overall_found, $ln_overall_hit, + 1, $fn_overall_found, $fn_overall_hit, + 1, $br_overall_found, $br_overall_hit); +} else { + info("Done.\n") if (!$list && !$capture); +} exit(0); -# Check for follow option -if ($follow) -{ - $follow = "--follow"; -} - # # print_usage(handle) # @@ -317,7 +450,6 @@ if ($follow) sub print_usage(*) { local *HANDLE = $_[0]; - my $tool_name = basename($0); print(HANDLE < 1) { - die("ERROR: only one of -z, -c, -e, -a, -r, -l or ". - "--diff allowed!\n"); + die("ERROR: only one of -z, -c, -a, -e, -r, -l or ". + "--diff allowed!\n". + "Use $tool_name --help to get usage information\n"); } } @@ -401,18 +549,9 @@ sub userspace_reset() foreach $current_dir (@directory) { - info("Deleting all .da and .gcda files in $current_dir and ". - "subdirectories\n"); - if ($follow) - { - @file_list = - `find $current_dir -follow \\( -name \\*.gcda -o -name \\*.da \\) -type f 2>/dev/null`; - } - else - { - @file_list = - `find $current_dir \\( -name \\*.gcda -o -name \\*.da \\) -type f 2>/dev/null`; - } + info("Deleting all .da files in $current_dir". + ($no_recursion?"\n":" and subdirectories\n")); + @file_list = `find "$current_dir" $maxdepth $follow -name \\*\\.da -o -name \\*\\.gcda -type f 2>/dev/null`; chomp(@file_list); foreach (@file_list) { @@ -425,43 +564,31 @@ sub userspace_reset() # # userspace_capture() # -# Capture coverage data found in DIRECTORY and write it to OUTPUT_FILENAME -# if specified, otherwise to STDOUT. +# Capture coverage data found in DIRECTORY and write it to a package (if +# TO_PACKAGE specified) or to OUTPUT_FILENAME or STDOUT. # # Die on error. # sub userspace_capture() { - my @param; - my $file_list = join(" ", @directory); + my $dir; + my $build; - info("Capturing coverage data from $file_list\n"); - @param = ("$tool_dir/geninfo", @directory); - if ($output_filename) - { - @param = (@param, "--output-filename", $output_filename); + if (!defined($to_package)) { + lcov_geninfo(@directory); + return; } - if ($test_name) - { - @param = (@param, "--test-name", $test_name); + if (scalar(@directory) != 1) { + die("ERROR: -d may be specified only once with --to-package\n"); } - if ($follow) - { - @param = (@param, "--follow"); + $dir = $directory[0]; + if (defined($base_directory)) { + $build = $base_directory; + } else { + $build = $dir; } - if ($quiet) - { - @param = (@param, "--quiet"); - } - if ($nochecksum) - { - @param = (@param, "--no-checksum"); - } - @param = (@param, "--source-dirs", $source_dirs); - - system(@param); - exit($? >> 8); + create_package($to_package, $dir, $build); } @@ -476,93 +603,158 @@ sub userspace_capture() sub kernel_reset() { local *HANDLE; - check_and_load_kernel_module(); + my $reset_file; info("Resetting kernel execution counters\n"); - open(HANDLE, ">$gcov_dir/vmlinux") or - die("ERROR: cannot write to $gcov_dir/vmlinux!\n"); + if (-e "$gcov_dir/vmlinux") { + $reset_file = "$gcov_dir/vmlinux"; + } elsif (-e "$gcov_dir/reset") { + $reset_file = "$gcov_dir/reset"; + } else { + die("ERROR: no reset control found in $gcov_dir\n"); + } + open(HANDLE, ">$reset_file") or + die("ERROR: cannot write to $reset_file!\n"); print(HANDLE "0"); close(HANDLE); - - # Unload module if we loaded it in the first place - if ($need_unload) - { - unload_module($need_unload); - } } # -# kernel_capture() +# lcov_copy_single(from, to) +# +# Copy single regular file FROM to TO without checking its size. This is +# required to work with special files generated by the kernel +# seq_file-interface. # -# Capture kernel coverage data and write it to OUTPUT_FILENAME if specified, -# otherwise stdout. # - -sub kernel_capture() +sub lcov_copy_single($$) { - my @param; + my ($from, $to) = @_; + my $content; + local $/; + local *HANDLE; - check_and_load_kernel_module(); + open(HANDLE, "<$from") or die("ERROR: cannot read $from: $!\n"); + $content = ; + close(HANDLE); + open(HANDLE, ">$to") or die("ERROR: cannot write $from: $!\n"); + if (defined($content)) { + print(HANDLE $content); + } + close(HANDLE); +} - # Make sure the temporary directory is removed upon script termination - END - { - if ($temp_dir_name) - { - stat($temp_dir_name); - if (-r _) - { - info("Removing temporary directory ". - "$temp_dir_name\n"); +# +# lcov_find(dir, function, data[, extension, ...)]) +# +# Search DIR for files and directories whose name matches PATTERN and run +# FUNCTION for each match. If not pattern is specified, match all names. +# +# FUNCTION has the following prototype: +# function(dir, relative_name, data) +# +# Where: +# dir: the base directory for this search +# relative_name: the name relative to the base directory of this entry +# data: the DATA variable passed to lcov_find +# +sub lcov_find($$$;@) +{ + my ($dir, $fn, $data, @pattern) = @_; + my $result; + my $_fn = sub { + my $filename = $File::Find::name; - # Remove temporary directory - system("rm", "-rf", $temp_dir_name) - and warn("WARNING: cannot remove ". - "temporary directory ". - "$temp_dir_name!\n"); + if (defined($result)) { + return; + } + $filename = abs2rel($filename, $dir); + foreach (@pattern) { + if ($filename =~ /$_/) { + goto ok; } } + return; + ok: + $result = &$fn($dir, $filename, $data); + }; + if (scalar(@pattern) == 0) { + @pattern = ".*"; } + find( { wanted => $_fn, no_chdir => 1 }, $dir); - # Get temporary directory - $temp_dir_name = create_temp_dir(); + return $result; +} - info("Copying kernel data to temporary directory $temp_dir_name\n"); +# +# lcov_copy_fn(from, rel, to) +# +# Copy directories, files and links from/rel to to/rel. +# - if (!@kernel_directory) - { - # Copy files from gcov kernel directory - system("cp", "-dr", $gcov_dir, $temp_dir_name) - and die("ERROR: cannot copy files from $gcov_dir!\n"); +sub lcov_copy_fn($$$) +{ + my ($from, $rel, $to) = @_; + my $absfrom = canonpath(catfile($from, $rel)); + my $absto = canonpath(catfile($to, $rel)); + + if (-d) { + if (! -d $absto) { + mkpath($absto) or + die("ERROR: cannot create directory $absto\n"); + chmod(0700, $absto); + } + } elsif (-l) { + # Copy symbolic link + my $link = readlink($absfrom); + + if (!defined($link)) { + die("ERROR: cannot read link $absfrom: $!\n"); + } + symlink($link, $absto) or + die("ERROR: cannot create link $absto: $!\n"); + } else { + lcov_copy_single($absfrom, $absto); + chmod(0600, $absto); } - else - { - # Prefix list of kernel sub-directories with the gcov kernel - # directory - my $file_list = join(" ", map {"$gcov_dir/$_";} - @kernel_directory); + return undef; +} - # Copy files from gcov kernel directory - system("cp", "-dr", $file_list, $temp_dir_name) - and die("ERROR: cannot copy files from $file_list!\n"); +# +# lcov_copy(from, to, subdirs) +# +# Copy all specified SUBDIRS and files from directory FROM to directory TO. For +# regular files, copy file contents without checking its size. This is required +# to work with seq_file-generated files. +# + +sub lcov_copy($$;@) +{ + my ($from, $to, @subdirs) = @_; + my @pattern; + + foreach (@subdirs) { + push(@pattern, "^$_"); } + lcov_find($from, \&lcov_copy_fn, $to, @pattern); +} - # Make directories writable - system("find", $temp_dir_name, "-type", "d", "-exec", "chmod", "u+w", - "{}", ";") - and die("ERROR: cannot modify access rights for ". - "$temp_dir_name!\n"); +# +# lcov_geninfo(directory) +# +# Call geninfo for the specified directory and with the parameters specified +# at the command line. +# - # Make files writable - system("find", $temp_dir_name, "-type", "f", "-exec", "chmod", "u+w", - "{}", ";") - and die("ERROR: cannot modify access rights for ". - "$temp_dir_name!\n"); +sub lcov_geninfo(@) +{ + my (@dir) = @_; + my @param; # Capture data - info("Capturing coverage data from $temp_dir_name\n"); - @param = ("$tool_dir/geninfo", $temp_dir_name); + info("Capturing coverage data from ".join(" ", @dir)."\n"); + @param = ("$tool_dir/geninfo", @dir); if ($output_filename) { @param = (@param, "--output-filename", $output_filename); @@ -579,17 +771,478 @@ sub kernel_capture() { @param = (@param, "--quiet"); } - if ($nochecksum) + if (defined($checksum)) { - @param = (@param, "--no-checksum"); + if ($checksum) + { + @param = (@param, "--checksum"); + } + else + { + @param = (@param, "--no-checksum"); + } + } + if ($base_directory) + { + @param = (@param, "--base-directory", $base_directory); + } + if ($no_compat_libtool) + { + @param = (@param, "--no-compat-libtool"); + } + elsif ($compat_libtool) + { + @param = (@param, "--compat-libtool"); + } + if ($gcov_tool) + { + @param = (@param, "--gcov-tool", $gcov_tool); + } + if ($ignore_errors) + { + @param = (@param, "--ignore-errors", $ignore_errors); + } + if ($initial) + { + @param = (@param, "--initial"); + } + if ($no_markers) + { + @param = (@param, "--no-markers"); + } + if ($opt_derive_func_data) + { + @param = (@param, "--derive-func-data"); + } + if ($opt_debug) + { + @param = (@param, "--debug"); } system(@param) and exit($? >> 8); +} +# +# read_file(filename) +# +# Return the contents of the file defined by filename. +# - # Unload module if we loaded it in the first place - if ($need_unload) - { - unload_module($need_unload); +sub read_file($) +{ + my ($filename) = @_; + my $content; + local $\; + local *HANDLE; + + open(HANDLE, "<$filename") || return undef; + $content = ; + close(HANDLE); + + return $content; +} + +# +# get_package(package_file) +# +# Unpack unprocessed coverage data files from package_file to a temporary +# directory and return directory name, build directory and gcov kernel version +# as found in package. +# + +sub get_package($) +{ + my ($file) = @_; + my $dir = create_temp_dir(); + my $gkv; + my $build; + my $cwd = getcwd(); + my $count; + local *HANDLE; + + info("Reading package $file:\n"); + info(" data directory .......: $dir\n"); + $file = abs_path($file); + chdir($dir); + open(HANDLE, "tar xvfz $file 2>/dev/null|") + or die("ERROR: could not process package $file\n"); + while () { + if (/\.da$/ || /\.gcda$/) { + $count++; + } + } + close(HANDLE); + $build = read_file("$dir/$pkg_build_file"); + if (defined($build)) { + info(" build directory ......: $build\n"); + } + $gkv = read_file("$dir/$pkg_gkv_file"); + if (defined($gkv)) { + $gkv = int($gkv); + if ($gkv != $GKV_PROC && $gkv != $GKV_SYS) { + die("ERROR: unsupported gcov kernel version found ". + "($gkv)\n"); + } + info(" content type .........: kernel data\n"); + info(" gcov kernel version ..: %s\n", $GKV_NAME[$gkv]); + } else { + info(" content type .........: application data\n"); + } + info(" data files ...........: $count\n"); + chdir($cwd); + + return ($dir, $build, $gkv); +} + +# +# write_file(filename, $content) +# +# Create a file named filename and write the specified content to it. +# + +sub write_file($$) +{ + my ($filename, $content) = @_; + local *HANDLE; + + open(HANDLE, ">$filename") || return 0; + print(HANDLE $content); + close(HANDLE) || return 0; + + return 1; +} + +# count_package_data(filename) +# +# Count the number of coverage data files in the specified package file. +# + +sub count_package_data($) +{ + my ($filename) = @_; + local *HANDLE; + my $count = 0; + + open(HANDLE, "tar tfz $filename|") or return undef; + while () { + if (/\.da$/ || /\.gcda$/) { + $count++; + } + } + close(HANDLE); + return $count; +} + +# +# create_package(package_file, source_directory, build_directory[, +# kernel_gcov_version]) +# +# Store unprocessed coverage data files from source_directory to package_file. +# + +sub create_package($$$;$) +{ + my ($file, $dir, $build, $gkv) = @_; + my $cwd = getcwd(); + + # Print information about the package + info("Creating package $file:\n"); + info(" data directory .......: $dir\n"); + + # Handle build directory + if (defined($build)) { + info(" build directory ......: $build\n"); + write_file("$dir/$pkg_build_file", $build) + or die("ERROR: could not write to ". + "$dir/$pkg_build_file\n"); + } + + # Handle gcov kernel version data + if (defined($gkv)) { + info(" content type .........: kernel data\n"); + info(" gcov kernel version ..: %s\n", $GKV_NAME[$gkv]); + write_file("$dir/$pkg_gkv_file", $gkv) + or die("ERROR: could not write to ". + "$dir/$pkg_gkv_file\n"); + } else { + info(" content type .........: application data\n"); + } + + # Create package + $file = abs_path($file); + chdir($dir); + system("tar cfz $file .") + and die("ERROR: could not create package $file\n"); + + # Remove temporary files + unlink("$dir/$pkg_build_file"); + unlink("$dir/$pkg_gkv_file"); + + # Show number of data files + if (!$quiet) { + my $count = count_package_data($file); + + if (defined($count)) { + info(" data files ...........: $count\n"); + } + } + chdir($cwd); +} + +sub find_link_fn($$$) +{ + my ($from, $rel, $filename) = @_; + my $absfile = catfile($from, $rel, $filename); + + if (-l $absfile) { + return $absfile; + } + return undef; +} + +# +# get_base(dir) +# +# Return (BASE, OBJ), where +# - BASE: is the path to the kernel base directory relative to dir +# - OBJ: is the absolute path to the kernel build directory +# + +sub get_base($) +{ + my ($dir) = @_; + my $marker = "kernel/gcov/base.gcno"; + my $markerfile; + my $sys; + my $obj; + my $link; + + $markerfile = lcov_find($dir, \&find_link_fn, $marker); + if (!defined($markerfile)) { + return (undef, undef); + } + + # sys base is parent of parent of markerfile. + $sys = abs2rel(dirname(dirname(dirname($markerfile))), $dir); + + # obj base is parent of parent of markerfile link target. + $link = readlink($markerfile); + if (!defined($link)) { + die("ERROR: could not read $markerfile\n"); + } + $obj = dirname(dirname(dirname($link))); + + return ($sys, $obj); +} + +# +# apply_base_dir(data_dir, base_dir, build_dir, @directories) +# +# Make entries in @directories relative to data_dir. +# + +sub apply_base_dir($$$@) +{ + my ($data, $base, $build, @dirs) = @_; + my $dir; + my @result; + + foreach $dir (@dirs) { + # Is directory path relative to data directory? + if (-d catdir($data, $dir)) { + push(@result, $dir); + next; + } + # Relative to the auto-detected base-directory? + if (defined($base)) { + if (-d catdir($data, $base, $dir)) { + push(@result, catdir($base, $dir)); + next; + } + } + # Relative to the specified base-directory? + if (defined($base_directory)) { + if (file_name_is_absolute($base_directory)) { + $base = abs2rel($base_directory, rootdir()); + } else { + $base = $base_directory; + } + if (-d catdir($data, $base, $dir)) { + push(@result, catdir($base, $dir)); + next; + } + } + # Relative to the build directory? + if (defined($build)) { + if (file_name_is_absolute($build)) { + $base = abs2rel($build, rootdir()); + } else { + $base = $build; + } + if (-d catdir($data, $base, $dir)) { + push(@result, catdir($base, $dir)); + next; + } + } + die("ERROR: subdirectory $dir not found\n". + "Please use -b to specify the correct directory\n"); + } + return @result; +} + +# +# copy_gcov_dir(dir, [@subdirectories]) +# +# Create a temporary directory and copy all or, if specified, only some +# subdirectories from dir to that directory. Return the name of the temporary +# directory. +# + +sub copy_gcov_dir($;@) +{ + my ($data, @dirs) = @_; + my $tempdir = create_temp_dir(); + + info("Copying data to temporary directory $tempdir\n"); + lcov_copy($data, $tempdir, @dirs); + + return $tempdir; +} + +# +# kernel_capture_initial +# +# Capture initial kernel coverage data, i.e. create a coverage data file from +# static graph files which contains zero coverage data for all instrumented +# lines. +# + +sub kernel_capture_initial() +{ + my $build; + my $source; + my @params; + + if (defined($base_directory)) { + $build = $base_directory; + $source = "specified"; + } else { + (undef, $build) = get_base($gcov_dir); + if (!defined($build)) { + die("ERROR: could not auto-detect build directory.\n". + "Please use -b to specify the build directory\n"); + } + $source = "auto-detected"; + } + info("Using $build as kernel build directory ($source)\n"); + # Build directory needs to be passed to geninfo + $base_directory = $build; + if (@kernel_directory) { + foreach my $dir (@kernel_directory) { + push(@params, "$build/$dir"); + } + } else { + push(@params, $build); + } + lcov_geninfo(@params); +} + +# +# kernel_capture_from_dir(directory, gcov_kernel_version, build) +# +# Perform the actual kernel coverage capturing from the specified directory +# assuming that the data was copied from the specified gcov kernel version. +# + +sub kernel_capture_from_dir($$$) +{ + my ($dir, $gkv, $build) = @_; + + # Create package or coverage file + if (defined($to_package)) { + create_package($to_package, $dir, $build, $gkv); + } else { + # Build directory needs to be passed to geninfo + $base_directory = $build; + lcov_geninfo($dir); + } +} + +# +# adjust_kernel_dir(dir, build) +# +# Adjust directories specified with -k so that they point to the directory +# relative to DIR. Return the build directory if specified or the auto- +# detected build-directory. +# + +sub adjust_kernel_dir($$) +{ + my ($dir, $build) = @_; + my ($sys_base, $build_auto) = get_base($dir); + + if (!defined($build)) { + $build = $build_auto; + } + if (!defined($build)) { + die("ERROR: could not auto-detect build directory.\n". + "Please use -b to specify the build directory\n"); + } + # Make @kernel_directory relative to sysfs base + if (@kernel_directory) { + @kernel_directory = apply_base_dir($dir, $sys_base, $build, + @kernel_directory); + } + return $build; +} + +sub kernel_capture() +{ + my $data_dir; + my $build = $base_directory; + + if ($gcov_gkv == $GKV_SYS) { + $build = adjust_kernel_dir($gcov_dir, $build); + } + $data_dir = copy_gcov_dir($gcov_dir, @kernel_directory); + kernel_capture_from_dir($data_dir, $gcov_gkv, $build); +} + +# +# package_capture() +# +# Capture coverage data from a package of unprocessed coverage data files +# as generated by lcov --to-package. +# + +sub package_capture() +{ + my $dir; + my $build; + my $gkv; + + ($dir, $build, $gkv) = get_package($from_package); + + # Check for build directory + if (defined($base_directory)) { + if (defined($build)) { + info("Using build directory specified by -b.\n"); + } + $build = $base_directory; + } + + # Do the actual capture + if (defined($gkv)) { + if ($gkv == $GKV_SYS) { + $build = adjust_kernel_dir($dir, $build); + } + if (@kernel_directory) { + $dir = copy_gcov_dir($dir, @kernel_directory); + } + kernel_capture_from_dir($dir, $gkv, $build); + } else { + # Build directory needs to be passed to geninfo + $base_directory = $build; + lcov_geninfo($dir); } } @@ -608,117 +1261,17 @@ sub info(@) # Print info string if ($to_file) { - print(@_) + printf(@_) } else { - # Don't interfer with the .info output to STDOUT + # Don't interfere with the .info output to STDOUT printf(STDERR @_); } } } -# -# Check if the gcov kernel module is loaded. If it is, exit, if not, try -# to load it. -# -# Die on error. -# - -sub check_and_load_kernel_module() -{ - my $module_name; - - # Is it loaded already? - stat("$gcov_dir"); - if (-r _) { return(); } - - info("Loading required gcov kernel module.\n"); - - # Do we have access to the insmod tool? - stat($insmod_tool); - if (!-x _) - { - die("ERROR: need insmod tool ($insmod_tool) to access kernel ". - "coverage data!\n"); - } - # Do we have access to the modprobe tool? - stat($modprobe_tool); - if (!-x _) - { - die("ERROR: need modprobe tool ($modprobe_tool) to access ". - "kernel coverage data!\n"); - } - - # Try some possibilities of where the gcov kernel module may be found - foreach $module_name (@gcovmod) - { - # Try to load module from system wide module directory - # /lib/modules - if (system_no_output(3, $modprobe_tool, $module_name) == 0) - { - # Succeeded - $need_unload = $module_name; - return(); - } - - # Try to load linux 2.5/2.6 module from tool directory - if (system_no_output(3, $insmod_tool, - "$tool_dir/$module_name.ko") == 0) - { - # Succeeded - $need_unload = $module_name; - return(); - } - - # Try to load linux 2.4 module from tool directory - if (system_no_output(3, $insmod_tool, - "$tool_dir/$module_name.o") == 0) - { - # Succeeded - $need_unload = $module_name; - return(); - } - } - - # Hm, loading failed - maybe we aren't root? - if ($> != 0) - { - die("ERROR: need root access to load kernel module!\n"); - } - - die("ERROR: cannot load required gcov kernel module!\n"); -} - - -# -# unload_module() -# -# Unload the gcov kernel module. -# - -sub unload_module($) -{ - my $module = $_[0]; - - info("Unloading kernel module $module\n"); - - # Do we have access to the rmmod tool? - stat($rmmod_tool); - if (!-x _) - { - warn("WARNING: cannot execute rmmod tool at $rmmod_tool - ". - "gcov module still loaded!\n"); - } - - # Unload gcov kernel module - system_no_output(1, $rmmod_tool, $module) - and warn("WARNING: cannot unload gcov kernel module ". - "$module!\n"); -} - - # # create_temp_dir() # @@ -729,24 +1282,200 @@ sub unload_module($) sub create_temp_dir() { - my $dirname; - my $number = sprintf("%d", rand(1000)); + my $dir; - # Endless loops are evil - while ($number++ < 1000) - { - $dirname = "$tmp_dir/$tmp_prefix$number"; - stat($dirname); - if (-e _) { next; } + if (defined($tmp_dir)) { + $dir = tempdir(DIR => $tmp_dir, CLEANUP => 1); + } else { + $dir = tempdir(CLEANUP => 1); + } + if (!defined($dir)) { + die("ERROR: cannot create temporary directory\n"); + } + push(@temp_dirs, $dir); - mkdir($dirname) - or die("ERROR: cannot create temporary directory ". - "$dirname!\n"); + return $dir; +} - return($dirname); + +# +# br_taken_to_num(taken) +# +# Convert a branch taken value .info format to number format. +# + +sub br_taken_to_num($) +{ + my ($taken) = @_; + + return 0 if ($taken eq '-'); + return $taken + 1; +} + + +# +# br_num_to_taken(taken) +# +# Convert a branch taken value in number format to .info format. +# + +sub br_num_to_taken($) +{ + my ($taken) = @_; + + return '-' if ($taken == 0); + return $taken - 1; +} + + +# +# br_taken_add(taken1, taken2) +# +# Return the result of taken1 + taken2 for 'branch taken' values. +# + +sub br_taken_add($$) +{ + my ($t1, $t2) = @_; + + return $t1 if (!defined($t2)); + return $t2 if (!defined($t1)); + return $t1 if ($t2 eq '-'); + return $t2 if ($t1 eq '-'); + return $t1 + $t2; +} + + +# +# br_taken_sub(taken1, taken2) +# +# Return the result of taken1 - taken2 for 'branch taken' values. Return 0 +# if the result would become negative. +# + +sub br_taken_sub($$) +{ + my ($t1, $t2) = @_; + + return $t1 if (!defined($t2)); + return undef if (!defined($t1)); + return $t1 if ($t1 eq '-'); + return $t1 if ($t2 eq '-'); + return 0 if $t2 > $t1; + return $t1 - $t2; +} + + +# +# +# br_ivec_len(vector) +# +# Return the number of entries in the branch coverage vector. +# + +sub br_ivec_len($) +{ + my ($vec) = @_; + + return 0 if (!defined($vec)); + return (length($vec) * 8 / $BR_VEC_WIDTH) / $BR_VEC_ENTRIES; +} + + +# +# br_ivec_push(vector, block, branch, taken) +# +# Add an entry to the branch coverage vector. If an entry with the same +# branch ID already exists, add the corresponding taken values. +# + +sub br_ivec_push($$$$) +{ + my ($vec, $block, $branch, $taken) = @_; + my $offset; + my $num = br_ivec_len($vec); + my $i; + + $vec = "" if (!defined($vec)); + + # Check if branch already exists in vector + for ($i = 0; $i < $num; $i++) { + my ($v_block, $v_branch, $v_taken) = br_ivec_get($vec, $i); + + next if ($v_block != $block || $v_branch != $branch); + + # Add taken counts + $taken = br_taken_add($taken, $v_taken); + last; } - die("ERROR: cannot create temporary directory in $tmp_dir!\n"); + $offset = $i * $BR_VEC_ENTRIES; + $taken = br_taken_to_num($taken); + + # Add to vector + vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH) = $block; + vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH) = $branch; + vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH) = $taken; + + return $vec; +} + + +# +# br_ivec_get(vector, number) +# +# Return an entry from the branch coverage vector. +# + +sub br_ivec_get($$) +{ + my ($vec, $num) = @_; + my $block; + my $branch; + my $taken; + my $offset = $num * $BR_VEC_ENTRIES; + + # Retrieve data from vector + $block = vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH); + $branch = vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH); + $taken = vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH); + + # Decode taken value from an integer + $taken = br_num_to_taken($taken); + + return ($block, $branch, $taken); +} + + +# +# get_br_found_and_hit(brcount) +# +# Return (br_found, br_hit) for brcount +# + +sub get_br_found_and_hit($) +{ + my ($brcount) = @_; + my $line; + my $br_found = 0; + my $br_hit = 0; + + foreach $line (keys(%{$brcount})) { + my $brdata = $brcount->{$line}; + my $i; + my $num = br_ivec_len($brdata); + + for ($i = 0; $i < $num; $i++) { + my $taken; + + (undef, undef, $taken) = br_ivec_get($brdata, $i); + + $br_found++; + $br_hit++ if ($taken ne "-" && $taken > 0); + } + } + + return ($br_found, $br_hit); } @@ -764,13 +1493,24 @@ sub create_temp_dir() # "found" -> $lines_found (number of instrumented lines found in file) # "hit" -> $lines_hit (number of executed lines in file) # "check" -> \%checkdata +# "testfnc" -> \%testfncdata +# "sumfnc" -> \%sumfnccount +# "testbr" -> \%testbrdata +# "sumbr" -> \%sumbrcount # -# %testdata: name of test affecting this file -> \%testcount +# %testdata : name of test affecting this file -> \%testcount +# %testfncdata: name of test affecting this file -> \%testfnccount +# %testbrdata: name of test affecting this file -> \%testbrcount # -# %testcount: line number -> execution count for a single test -# %sumcount : line number -> execution count for all tests -# %funcdata : line number -> name of function beginning at that line -# %checkdata: line number -> checksum of source code line +# %testcount : line number -> execution count for a single test +# %testfnccount: function name -> execution count for a single test +# %testbrcount : line number -> branch coverage data for a single test +# %sumcount : line number -> execution count for all tests +# %sumfnccount : function name -> execution count for all tests +# %sumbrcount : line number -> branch coverage data for all tests +# %funcdata : function name -> line number +# %checkdata : line number -> checksum of source code line +# $brdata : vector of items: block, branch, taken # # Note that .info file sections referring to the same file and test name # will automatically be combined by adding all execution counts. @@ -792,13 +1532,20 @@ sub read_info_file($) my $sumcount; # " " my $funcdata; # " " my $checkdata; # " " + my $testfncdata; + my $testfnccount; + my $sumfnccount; + my $testbrdata; + my $testbrcount; + my $sumbrcount; my $line; # Current line read from .info file my $testname; # Current test name my $filename; # Current filename my $hitcount; # Count for lines hit my $count; # Execution count of current line my $negative; # If set, warn about negative counts - my $checksum; # Checksum of current line + my $changed_testname; # If set, warn about changed testname + my $line_checksum; # Checksum of current line local *INFO_HANDLE; # Filehandle for .info file info("Reading tracefile $tracefile\n"); @@ -849,10 +1596,15 @@ sub read_info_file($) # Switch statement foreach ($line) { - /^TN:(\w*(,\w+)?)/ && do + /^TN:([^,]*)(,diff)?/ && do { # Test name information found $testname = defined($1) ? $1 : ""; + if ($testname =~ s/\W/_/g) + { + $changed_testname = 1; + } + $testname .= $2 if (defined($2)); last; }; @@ -863,17 +1615,22 @@ sub read_info_file($) $filename = $1; $data = $result{$filename}; - ($testdata, $sumcount, $funcdata, $checkdata) = + ($testdata, $sumcount, $funcdata, $checkdata, + $testfncdata, $sumfnccount, $testbrdata, + $sumbrcount) = get_info_entry($data); if (defined($testname)) { $testcount = $testdata->{$testname}; + $testfnccount = $testfncdata->{$testname}; + $testbrcount = $testbrdata->{$testname}; } else { - my %new_hash; - $testcount = \%new_hash; + $testcount = {}; + $testfnccount = {}; + $testbrcount = {}; } last; }; @@ -899,17 +1656,18 @@ sub read_info_file($) # Store line checksum if available if (defined($3)) { - $checksum = substr($3, 1); + $line_checksum = substr($3, 1); # Does it match a previous definition if (defined($checkdata->{$1}) && - ($checkdata->{$1} ne $checksum)) + ($checkdata->{$1} ne + $line_checksum)) { die("ERROR: checksum mismatch ". "at $filename:$1\n"); } - $checkdata->{$1} = $checksum; + $checkdata->{$1} = $line_checksum; } last; }; @@ -917,7 +1675,52 @@ sub read_info_file($) /^FN:(\d+),([^,]+)/ && do { # Function data found, add to structure - $funcdata->{$1} = $2; + $funcdata->{$2} = $1; + + # Also initialize function call data + if (!defined($sumfnccount->{$2})) { + $sumfnccount->{$2} = 0; + } + if (defined($testname)) + { + if (!defined($testfnccount->{$2})) { + $testfnccount->{$2} = 0; + } + } + last; + }; + + /^FNDA:(\d+),([^,]+)/ && do + { + # Function call count found, add to structure + # Add summary counts + $sumfnccount->{$2} += $1; + + # Add test-specific counts + if (defined($testname)) + { + $testfnccount->{$2} += $1; + } + last; + }; + + /^BRDA:(\d+),(\d+),(\d+),(\d+|-)/ && do { + # Branch coverage data found + my ($line, $block, $branch, $taken) = + ($1, $2, $3, $4); + + $sumbrcount->{$line} = + br_ivec_push($sumbrcount->{$line}, + $block, $branch, $taken); + + # Add test-specific counts + if (defined($testname)) { + $testbrcount->{$line} = + br_ivec_push( + $testbrcount->{$line}, + $block, $branch, + $taken); + } last; }; @@ -931,10 +1734,18 @@ sub read_info_file($) { $testdata->{$testname} = $testcount; + $testfncdata->{$testname} = + $testfnccount; + $testbrdata->{$testname} = + $testbrcount; } + set_info_entry($data, $testdata, $sumcount, $funcdata, - $checkdata); + $checkdata, $testfncdata, + $sumfnccount, + $testbrdata, + $sumbrcount); $result{$filename} = $data; last; } @@ -946,25 +1757,29 @@ sub read_info_file($) } close(INFO_HANDLE); - # Calculate lines_found and lines_hit for each file + # Calculate hit and found values for lines and functions of each file foreach $filename (keys(%result)) { $data = $result{$filename}; - ($testdata, $sumcount, $funcdata) = get_info_entry($data); + ($testdata, $sumcount, undef, undef, $testfncdata, + $sumfnccount, $testbrdata, $sumbrcount) = + get_info_entry($data); - # Filter out empty test cases + # Filter out empty files if (scalar(keys(%{$sumcount})) == 0) { delete($result{$filename}); next; } + # Filter out empty test cases foreach $testname (keys(%{$testdata})) { if (!defined($testdata->{$testname}) || scalar(keys(%{$testdata->{$testname}})) == 0) { delete($testdata->{$testname}); + delete($testfncdata->{$testname}); } } @@ -978,7 +1793,24 @@ sub read_info_file($) $data->{"hit"} = $hitcount; - $result{$filename} = $data; + # Get found/hit values for function call data + $data->{"f_found"} = scalar(keys(%{$sumfnccount})); + $hitcount = 0; + + foreach (keys(%{$sumfnccount})) { + if ($sumfnccount->{$_} > 0) { + $hitcount++; + } + } + $data->{"f_hit"} = $hitcount; + + # Get found/hit values for branch data + { + my ($br_found, $br_hit) = get_br_found_and_hit($sumbrcount); + + $data->{"b_found"} = $br_found; + $data->{"b_hit"} = $br_hit; + } } if (scalar(keys(%result)) == 0) @@ -990,6 +1822,11 @@ sub read_info_file($) warn("WARNING: negative counts found in tracefile ". "$tracefile\n"); } + if ($changed_testname) + { + warn("WARNING: invalid characters removed from testname in ". + "tracefile $tracefile\n"); + } return(\%result); } @@ -1001,7 +1838,9 @@ sub read_info_file($) # Retrieve data from an entry of the structure generated by read_info_file(). # Return a list of references to hashes: # (test data hash ref, sum count hash ref, funcdata hash ref, checkdata hash -# ref, lines found, lines hit) +# ref, testfncdata hash ref, sumfnccount hash ref, testbrdata hash ref, +# sumbrcount hash ref, lines found, lines hit, functions found, +# functions hit, branches found, branches hit) # sub get_info_entry($) @@ -1010,22 +1849,34 @@ sub get_info_entry($) my $sumcount_ref = $_[0]->{"sum"}; my $funcdata_ref = $_[0]->{"func"}; my $checkdata_ref = $_[0]->{"check"}; + my $testfncdata = $_[0]->{"testfnc"}; + my $sumfnccount = $_[0]->{"sumfnc"}; + my $testbrdata = $_[0]->{"testbr"}; + my $sumbrcount = $_[0]->{"sumbr"}; my $lines_found = $_[0]->{"found"}; my $lines_hit = $_[0]->{"hit"}; + my $f_found = $_[0]->{"f_found"}; + my $f_hit = $_[0]->{"f_hit"}; + my $br_found = $_[0]->{"b_found"}; + my $br_hit = $_[0]->{"b_hit"}; return ($testdata_ref, $sumcount_ref, $funcdata_ref, $checkdata_ref, - $lines_found, $lines_hit); + $testfncdata, $sumfnccount, $testbrdata, $sumbrcount, + $lines_found, $lines_hit, $f_found, $f_hit, + $br_found, $br_hit); } # # set_info_entry(hash_ref, testdata_ref, sumcount_ref, funcdata_ref, -# checkdata_ref[,lines_found, lines_hit]) +# checkdata_ref, testfncdata_ref, sumfcncount_ref, +# testbrdata_ref, sumbrcount_ref[,lines_found, +# lines_hit, f_found, f_hit, $b_found, $b_hit]) # # Update the hash referenced by HASH_REF with the provided data references. # -sub set_info_entry($$$$$;$$) +sub set_info_entry($$$$$$$$$;$$$$$$) { my $data_ref = $_[0]; @@ -1033,9 +1884,17 @@ sub set_info_entry($$$$$;$$) $data_ref->{"sum"} = $_[2]; $data_ref->{"func"} = $_[3]; $data_ref->{"check"} = $_[4]; + $data_ref->{"testfnc"} = $_[5]; + $data_ref->{"sumfnc"} = $_[6]; + $data_ref->{"testbr"} = $_[7]; + $data_ref->{"sumbr"} = $_[8]; - if (defined($_[5])) { $data_ref->{"found"} = $_[5]; } - if (defined($_[6])) { $data_ref->{"hit"} = $_[6]; } + if (defined($_[9])) { $data_ref->{"found"} = $_[9]; } + if (defined($_[10])) { $data_ref->{"hit"} = $_[10]; } + if (defined($_[11])) { $data_ref->{"f_found"} = $_[11]; } + if (defined($_[12])) { $data_ref->{"f_hit"} = $_[12]; } + if (defined($_[13])) { $data_ref->{"b_found"} = $_[13]; } + if (defined($_[14])) { $data_ref->{"b_hit"} = $_[14]; } } @@ -1134,48 +1993,262 @@ sub merge_checksums($$$) # -# merge_func_data(ref1, ref2, filename) +# merge_func_data(funcdata1, funcdata2, filename) # sub merge_func_data($$$) { - my $ref1 = $_[0]; - my $ref2 = $_[1]; - my $filename = $_[2]; + my ($funcdata1, $funcdata2, $filename) = @_; my %result; - my %ignore; - my $line1; - my $line2; + my $func; - # Check for mismatch - foreach $line1 (keys(%{$ref1})) - { - foreach $line2 (keys(%{$ref2})) - { - if (($ref1->{$line1} eq $ref2->{$line2}) && - ($line1 != $line2)) - { - warn("WARNING: function data mismatch at ". - "$filename:$ref1->{$line1}\n"); - $ignore{$line2} = 1; + if (defined($funcdata1)) { + %result = %{$funcdata1}; + } + + foreach $func (keys(%{$funcdata2})) { + my $line1 = $result{$func}; + my $line2 = $funcdata2->{$func}; + + if (defined($line1) && ($line1 != $line2)) { + warn("WARNING: function data mismatch at ". + "$filename:$line2\n"); + next; + } + $result{$func} = $line2; + } + + return \%result; +} + + +# +# add_fnccount(fnccount1, fnccount2) +# +# Add function call count data. Return list (fnccount_added, f_found, f_hit) +# + +sub add_fnccount($$) +{ + my ($fnccount1, $fnccount2) = @_; + my %result; + my $f_found; + my $f_hit; + my $function; + + if (defined($fnccount1)) { + %result = %{$fnccount1}; + } + foreach $function (keys(%{$fnccount2})) { + $result{$function} += $fnccount2->{$function}; + } + $f_found = scalar(keys(%result)); + $f_hit = 0; + foreach $function (keys(%result)) { + if ($result{$function} > 0) { + $f_hit++; + } + } + + return (\%result, $f_found, $f_hit); +} + +# +# add_testfncdata(testfncdata1, testfncdata2) +# +# Add function call count data for several tests. Return reference to +# added_testfncdata. +# + +sub add_testfncdata($$) +{ + my ($testfncdata1, $testfncdata2) = @_; + my %result; + my $testname; + + foreach $testname (keys(%{$testfncdata1})) { + if (defined($testfncdata2->{$testname})) { + my $fnccount; + + # Function call count data for this testname exists + # in both data sets: merge + ($fnccount) = add_fnccount( + $testfncdata1->{$testname}, + $testfncdata2->{$testname}); + $result{$testname} = $fnccount; + next; + } + # Function call count data for this testname is unique to + # data set 1: copy + $result{$testname} = $testfncdata1->{$testname}; + } + + # Add count data for testnames unique to data set 2 + foreach $testname (keys(%{$testfncdata2})) { + if (!defined($result{$testname})) { + $result{$testname} = $testfncdata2->{$testname}; + } + } + return \%result; +} + + +# +# brcount_to_db(brcount) +# +# Convert brcount data to the following format: +# +# db: line number -> block hash +# block hash: block number -> branch hash +# branch hash: branch number -> taken value +# + +sub brcount_to_db($) +{ + my ($brcount) = @_; + my $line; + my $db; + + # Add branches from first count to database + foreach $line (keys(%{$brcount})) { + my $brdata = $brcount->{$line}; + my $i; + my $num = br_ivec_len($brdata); + + for ($i = 0; $i < $num; $i++) { + my ($block, $branch, $taken) = br_ivec_get($brdata, $i); + + $db->{$line}->{$block}->{$branch} = $taken; + } + } + + return $db; +} + + +# +# db_to_brcount(db) +# +# Convert branch coverage data back to brcount format. +# + +sub db_to_brcount($) +{ + my ($db) = @_; + my $line; + my $brcount = {}; + my $br_found = 0; + my $br_hit = 0; + + # Convert database back to brcount format + foreach $line (sort({$a <=> $b} keys(%{$db}))) { + my $ldata = $db->{$line}; + my $brdata; + my $block; + + foreach $block (sort({$a <=> $b} keys(%{$ldata}))) { + my $bdata = $ldata->{$block}; + my $branch; + + foreach $branch (sort({$a <=> $b} keys(%{$bdata}))) { + my $taken = $bdata->{$branch}; + + $br_found++; + $br_hit++ if ($taken ne "-" && $taken > 0); + $brdata = br_ivec_push($brdata, $block, + $branch, $taken); } } + $brcount->{$line} = $brdata; } - # Merge - foreach (keys(%{$ref1})) - { - $result{$_} = $ref1->{$_}; - } + return ($brcount, $br_found, $br_hit); +} - foreach (keys(%{$ref2})) - { - if (!$ignore{$_}) - { - $result{$_} = $ref2->{$_}; + +# combine_brcount(brcount1, brcount2, type) +# +# If add is BR_ADD, add branch coverage data and return list (brcount_added, +# br_found, br_hit). If add is BR_SUB, subtract the taken values of brcount2 +# from brcount1 and return (brcount_sub, br_found, br_hit). +# + +sub combine_brcount($$$) +{ + my ($brcount1, $brcount2, $type) = @_; + my $line; + my $block; + my $branch; + my $taken; + my $db; + my $br_found = 0; + my $br_hit = 0; + my $result; + + # Convert branches from first count to database + $db = brcount_to_db($brcount1); + # Combine values from database and second count + foreach $line (keys(%{$brcount2})) { + my $brdata = $brcount2->{$line}; + my $num = br_ivec_len($brdata); + my $i; + + for ($i = 0; $i < $num; $i++) { + ($block, $branch, $taken) = br_ivec_get($brdata, $i); + my $new_taken = $db->{$line}->{$block}->{$branch}; + + if ($type == $BR_ADD) { + $new_taken = br_taken_add($new_taken, $taken); + } elsif ($type == $BR_SUB) { + $new_taken = br_taken_sub($new_taken, $taken); + } + $db->{$line}->{$block}->{$branch} = $new_taken + if (defined($new_taken)); } } + # Convert database back to brcount format + ($result, $br_found, $br_hit) = db_to_brcount($db); + return ($result, $br_found, $br_hit); +} + + +# +# add_testbrdata(testbrdata1, testbrdata2) +# +# Add branch coverage data for several tests. Return reference to +# added_testbrdata. +# + +sub add_testbrdata($$) +{ + my ($testbrdata1, $testbrdata2) = @_; + my %result; + my $testname; + + foreach $testname (keys(%{$testbrdata1})) { + if (defined($testbrdata2->{$testname})) { + my $brcount; + + # Branch coverage data for this testname exists + # in both data sets: add + ($brcount) = combine_brcount( + $testbrdata1->{$testname}, + $testbrdata2->{$testname}, $BR_ADD); + $result{$testname} = $brcount; + next; + } + # Branch coverage data for this testname is unique to + # data set 1: copy + $result{$testname} = $testbrdata1->{$testname}; + } + + # Add count data for testnames unique to data set 2 + foreach $testname (keys(%{$testbrdata2})) { + if (!defined($result{$testname})) { + $result{$testname} = $testbrdata2->{$testname}; + } + } return \%result; } @@ -1194,35 +2267,61 @@ sub combine_info_entries($$$) my $sumcount1; my $funcdata1; my $checkdata1; + my $testfncdata1; + my $sumfnccount1; + my $testbrdata1; + my $sumbrcount1; my $entry2 = $_[1]; # Reference to hash containing second entry my $testdata2; my $sumcount2; my $funcdata2; my $checkdata2; + my $testfncdata2; + my $sumfnccount2; + my $testbrdata2; + my $sumbrcount2; my %result; # Hash containing combined entry my %result_testdata; my $result_sumcount = {}; my $result_funcdata; + my $result_testfncdata; + my $result_sumfnccount; + my $result_testbrdata; + my $result_sumbrcount; my $lines_found; my $lines_hit; + my $f_found; + my $f_hit; + my $br_found; + my $br_hit; my $testname; my $filename = $_[2]; # Retrieve data - ($testdata1, $sumcount1, $funcdata1, $checkdata1) = - get_info_entry($entry1); - ($testdata2, $sumcount2, $funcdata2, $checkdata2) = - get_info_entry($entry2); + ($testdata1, $sumcount1, $funcdata1, $checkdata1, $testfncdata1, + $sumfnccount1, $testbrdata1, $sumbrcount1) = get_info_entry($entry1); + ($testdata2, $sumcount2, $funcdata2, $checkdata2, $testfncdata2, + $sumfnccount2, $testbrdata2, $sumbrcount2) = get_info_entry($entry2); # Merge checksums $checkdata1 = merge_checksums($checkdata1, $checkdata2, $filename); # Combine funcdata $result_funcdata = merge_func_data($funcdata1, $funcdata2, $filename); - + + # Combine function call count data + $result_testfncdata = add_testfncdata($testfncdata1, $testfncdata2); + ($result_sumfnccount, $f_found, $f_hit) = + add_fnccount($sumfnccount1, $sumfnccount2); + + # Combine branch coverage data + $result_testbrdata = add_testbrdata($testbrdata1, $testbrdata2); + ($result_sumbrcount, $br_found, $br_hit) = + combine_brcount($sumbrcount1, $sumbrcount2, $BR_ADD); + # Combine testdata foreach $testname (keys(%{$testdata1})) { @@ -1264,8 +2363,10 @@ sub combine_info_entries($$$) # Store result set_info_entry(\%result, \%result_testdata, $result_sumcount, - $result_funcdata, $checkdata1, $lines_found, - $lines_hit); + $result_funcdata, $checkdata1, $result_testfncdata, + $result_sumfnccount, $result_testbrdata, + $result_sumbrcount, $lines_found, $lines_hit, + $f_found, $f_hit, $br_found, $br_hit); return(\%result); } @@ -1315,6 +2416,7 @@ sub add_traces() my $total_trace; my $current_trace; my $tracefile; + my @result; local *INFO_HANDLE; info("Combining tracefiles.\n"); @@ -1339,13 +2441,15 @@ sub add_traces() info("Writing data to $output_filename\n"); open(INFO_HANDLE, ">$output_filename") or die("ERROR: cannot write to $output_filename!\n"); - write_info_file(*INFO_HANDLE, $total_trace); + @result = write_info_file(*INFO_HANDLE, $total_trace); close(*INFO_HANDLE); } else { - write_info_file(*STDOUT, $total_trace); + @result = write_info_file(*STDOUT, $total_trace); } + + return @result; } @@ -1363,38 +2467,106 @@ sub write_info_file(*$) my $sumcount; my $funcdata; my $checkdata; + my $testfncdata; + my $sumfnccount; + my $testbrdata; + my $sumbrcount; my $testname; my $line; + my $func; my $testcount; + my $testfnccount; + my $testbrcount; my $found; my $hit; + my $f_found; + my $f_hit; + my $br_found; + my $br_hit; + my $ln_total_found = 0; + my $ln_total_hit = 0; + my $fn_total_found = 0; + my $fn_total_hit = 0; + my $br_total_found = 0; + my $br_total_hit = 0; - foreach $source_file (keys(%data)) + foreach $source_file (sort(keys(%data))) { $entry = $data{$source_file}; - ($testdata, $sumcount, $funcdata, $checkdata) = + ($testdata, $sumcount, $funcdata, $checkdata, $testfncdata, + $sumfnccount, $testbrdata, $sumbrcount, $found, $hit, + $f_found, $f_hit, $br_found, $br_hit) = get_info_entry($entry); - foreach $testname (keys(%{$testdata})) + + # Add to totals + $ln_total_found += $found; + $ln_total_hit += $hit; + $fn_total_found += $f_found; + $fn_total_hit += $f_hit; + $br_total_found += $br_found; + $br_total_hit += $br_hit; + + foreach $testname (sort(keys(%{$testdata}))) { $testcount = $testdata->{$testname}; + $testfnccount = $testfncdata->{$testname}; + $testbrcount = $testbrdata->{$testname}; $found = 0; $hit = 0; print(INFO_HANDLE "TN:$testname\n"); print(INFO_HANDLE "SF:$source_file\n"); - foreach $line (sort({$a <=> $b} keys(%{$funcdata}))) + # Write function related data + foreach $func ( + sort({$funcdata->{$a} <=> $funcdata->{$b}} + keys(%{$funcdata}))) { - print(INFO_HANDLE "FN:$line,". - $funcdata->{$line}."\n"); + print(INFO_HANDLE "FN:".$funcdata->{$func}. + ",$func\n"); + } + foreach $func (keys(%{$testfnccount})) { + print(INFO_HANDLE "FNDA:". + $testfnccount->{$func}. + ",$func\n"); + } + ($f_found, $f_hit) = + get_func_found_and_hit($testfnccount); + print(INFO_HANDLE "FNF:$f_found\n"); + print(INFO_HANDLE "FNH:$f_hit\n"); + + # Write branch related data + $br_found = 0; + $br_hit = 0; + foreach $line (sort({$a <=> $b} + keys(%{$testbrcount}))) { + my $brdata = $testbrcount->{$line}; + my $num = br_ivec_len($brdata); + my $i; + + for ($i = 0; $i < $num; $i++) { + my ($block, $branch, $taken) = + br_ivec_get($brdata, $i); + + print(INFO_HANDLE "BRDA:$line,$block,". + "$branch,$taken\n"); + $br_found++; + $br_hit++ if ($taken ne '-' && + $taken > 0); + } + } + if ($br_found > 0) { + print(INFO_HANDLE "BRF:$br_found\n"); + print(INFO_HANDLE "BRH:$br_hit\n"); } + # Write line related data foreach $line (sort({$a <=> $b} keys(%{$testcount}))) { print(INFO_HANDLE "DA:$line,". $testcount->{$line}. (defined($checkdata->{$line}) && - !$nochecksum ? + $checksum ? ",".$checkdata->{$line} : "")."\n"); $found++; if ($testcount->{$line} > 0) @@ -1408,6 +2580,9 @@ sub write_info_file(*$) print(INFO_HANDLE "end_of_record\n"); } } + + return ($ln_total_found, $ln_total_hit, $fn_total_found, $fn_total_hit, + $br_total_found, $br_total_hit); } @@ -1461,6 +2636,7 @@ sub extract() my $pattern; my @pattern_list; my $extracted = 0; + my @result; local *INFO_HANDLE; # Need perlreg expressions instead of shell pattern @@ -1495,13 +2671,15 @@ sub extract() info("Writing data to $output_filename\n"); open(INFO_HANDLE, ">$output_filename") or die("ERROR: cannot write to $output_filename!\n"); - write_info_file(*INFO_HANDLE, $data); + @result = write_info_file(*INFO_HANDLE, $data); close(*INFO_HANDLE); } else { - write_info_file(*STDOUT, $data); + @result = write_info_file(*STDOUT, $data); } + + return @result; } @@ -1517,6 +2695,7 @@ sub remove() my $pattern; my @pattern_list; my $removed = 0; + my @result; local *INFO_HANDLE; # Need perlreg expressions instead of shell pattern @@ -1548,16 +2727,126 @@ sub remove() info("Writing data to $output_filename\n"); open(INFO_HANDLE, ">$output_filename") or die("ERROR: cannot write to $output_filename!\n"); - write_info_file(*INFO_HANDLE, $data); + @result = write_info_file(*INFO_HANDLE, $data); close(*INFO_HANDLE); } else { - write_info_file(*STDOUT, $data); + @result = write_info_file(*STDOUT, $data); } + + return @result; } +# get_prefix(max_width, max_percentage_too_long, path_list) +# +# Return a path prefix that satisfies the following requirements: +# - is shared by more paths in path_list than any other prefix +# - the percentage of paths which would exceed the given max_width length +# after applying the prefix does not exceed max_percentage_too_long +# +# If multiple prefixes satisfy all requirements, the longest prefix is +# returned. Return an empty string if no prefix could be found. + +sub get_prefix($$@) +{ + my ($max_width, $max_long, @path_list) = @_; + my $path; + my $ENTRY_NUM = 0; + my $ENTRY_LONG = 1; + my %prefix; + + # Build prefix hash + foreach $path (@path_list) { + my ($v, $d, $f) = splitpath($path); + my @dirs = splitdir($d); + my $p_len = length($path); + my $i; + + # Remove trailing '/' + pop(@dirs) if ($dirs[scalar(@dirs) - 1] eq ''); + for ($i = 0; $i < scalar(@dirs); $i++) { + my $subpath = catpath($v, catdir(@dirs[0..$i]), ''); + my $entry = $prefix{$subpath}; + + $entry = [ 0, 0 ] if (!defined($entry)); + $entry->[$ENTRY_NUM]++; + if (($p_len - length($subpath) - 1) > $max_width) { + $entry->[$ENTRY_LONG]++; + } + $prefix{$subpath} = $entry; + } + } + # Find suitable prefix (sort descending by two keys: 1. number of + # entries covered by a prefix, 2. length of prefix) + foreach $path (sort {($prefix{$a}->[$ENTRY_NUM] == + $prefix{$b}->[$ENTRY_NUM]) ? + length($b) <=> length($a) : + $prefix{$b}->[$ENTRY_NUM] <=> + $prefix{$a}->[$ENTRY_NUM]} + keys(%prefix)) { + my ($num, $long) = @{$prefix{$path}}; + + # Check for additional requirement: number of filenames + # that would be too long may not exceed a certain percentage + if ($long <= $num * $max_long / 100) { + return $path; + } + } + + return ""; +} + + +# +# shorten_filename(filename, width) +# +# Truncate filename if it is longer than width characters. +# + +sub shorten_filename($$) +{ + my ($filename, $width) = @_; + my $l = length($filename); + my $s; + my $e; + + return $filename if ($l <= $width); + $e = int(($width - 3) / 2); + $s = $width - 3 - $e; + + return substr($filename, 0, $s).'...'.substr($filename, $l - $e); +} + + +sub shorten_number($$) +{ + my ($number, $width) = @_; + my $result = sprintf("%*d", $width, $number); + + return $result if (length($result) <= $width); + $number = $number / 1000; + return $result if (length($result) <= $width); + $result = sprintf("%*dk", $width - 1, $number); + return $result if (length($result) <= $width); + $number = $number / 1000; + $result = sprintf("%*dM", $width - 1, $number); + return $result if (length($result) <= $width); + return '#'; +} + +sub shorten_rate($$) +{ + my ($rate, $width) = @_; + my $result = sprintf("%*.1f%%", $width - 3, $rate); + + return $result if (length($result) <= $width); + $result = sprintf("%*d%%", $width - 1, $rate); + return $result if (length($result) <= $width); + return "#"; +} + # # list() # @@ -1569,17 +2858,278 @@ sub list() my $found; my $hit; my $entry; + my $fn_found; + my $fn_hit; + my $br_found; + my $br_hit; + my $total_found = 0; + my $total_hit = 0; + my $fn_total_found = 0; + my $fn_total_hit = 0; + my $br_total_found = 0; + my $br_total_hit = 0; + my $prefix; + my $strlen = length("Filename"); + my $format; + my $heading1; + my $heading2; + my @footer; + my $barlen; + my $rate; + my $fnrate; + my $brrate; + my $lastpath; + my $F_LN_NUM = 0; + my $F_LN_RATE = 1; + my $F_FN_NUM = 2; + my $F_FN_RATE = 3; + my $F_BR_NUM = 4; + my $F_BR_RATE = 5; + my @fwidth_narrow = (5, 5, 3, 5, 4, 5); + my @fwidth_wide = (6, 5, 5, 5, 6, 5); + my @fwidth = @fwidth_wide; + my $w; + my $max_width = $opt_list_width; + my $max_long = $opt_list_truncate_max; + my $fwidth_narrow_length; + my $fwidth_wide_length; + my $got_prefix = 0; + my $root_prefix = 0; - info("Listing contents of $list:\n"); + # Calculate total width of narrow fields + $fwidth_narrow_length = 0; + foreach $w (@fwidth_narrow) { + $fwidth_narrow_length += $w + 1; + } + # Calculate total width of wide fields + $fwidth_wide_length = 0; + foreach $w (@fwidth_wide) { + $fwidth_wide_length += $w + 1; + } + # Get common file path prefix + $prefix = get_prefix($max_width - $fwidth_narrow_length, $max_long, + keys(%{$data})); + $root_prefix = 1 if ($prefix eq rootdir()); + $got_prefix = 1 if (length($prefix) > 0); + $prefix =~ s/\/$//; + # Get longest filename length + foreach $filename (keys(%{$data})) { + if (!$opt_list_full_path) { + if (!$got_prefix || !$root_prefix && + !($filename =~ s/^\Q$prefix\/\E//)) { + my ($v, $d, $f) = splitpath($filename); - # List all files + $filename = $f; + } + } + # Determine maximum length of entries + if (length($filename) > $strlen) { + $strlen = length($filename) + } + } + if (!$opt_list_full_path) { + my $blanks; + + $w = $fwidth_wide_length; + # Check if all columns fit into max_width characters + if ($strlen + $fwidth_wide_length > $max_width) { + # Use narrow fields + @fwidth = @fwidth_narrow; + $w = $fwidth_narrow_length; + if (($strlen + $fwidth_narrow_length) > $max_width) { + # Truncate filenames at max width + $strlen = $max_width - $fwidth_narrow_length; + } + } + # Add some blanks between filename and fields if possible + $blanks = int($strlen * 0.5); + $blanks = 4 if ($blanks < 4); + $blanks = 8 if ($blanks > 8); + if (($strlen + $w + $blanks) < $max_width) { + $strlen += $blanks; + } else { + $strlen = $max_width - $w; + } + } + # Filename + $w = $strlen; + $format = "%-${w}s|"; + $heading1 = sprintf("%*s|", $w, ""); + $heading2 = sprintf("%-*s|", $w, "Filename"); + $barlen = $w + 1; + # Line coverage rate + $w = $fwidth[$F_LN_RATE]; + $format .= "%${w}s "; + $heading1 .= sprintf("%-*s |", $w + $fwidth[$F_LN_NUM], + "Lines"); + $heading2 .= sprintf("%-*s ", $w, "Rate"); + $barlen += $w + 1; + # Number of lines + $w = $fwidth[$F_LN_NUM]; + $format .= "%${w}s|"; + $heading2 .= sprintf("%*s|", $w, "Num"); + $barlen += $w + 1; + # Function coverage rate + $w = $fwidth[$F_FN_RATE]; + $format .= "%${w}s "; + $heading1 .= sprintf("%-*s|", $w + $fwidth[$F_FN_NUM] + 1, + "Functions"); + $heading2 .= sprintf("%-*s ", $w, "Rate"); + $barlen += $w + 1; + # Number of functions + $w = $fwidth[$F_FN_NUM]; + $format .= "%${w}s|"; + $heading2 .= sprintf("%*s|", $w, "Num"); + $barlen += $w + 1; + # Branch coverage rate + $w = $fwidth[$F_BR_RATE]; + $format .= "%${w}s "; + $heading1 .= sprintf("%-*s", $w + $fwidth[$F_BR_NUM] + 1, + "Branches"); + $heading2 .= sprintf("%-*s ", $w, "Rate"); + $barlen += $w + 1; + # Number of branches + $w = $fwidth[$F_BR_NUM]; + $format .= "%${w}s"; + $heading2 .= sprintf("%*s", $w, "Num"); + $barlen += $w; + # Line end + $format .= "\n"; + $heading1 .= "\n"; + $heading2 .= "\n"; + + # Print heading + print($heading1); + print($heading2); + print(("="x$barlen)."\n"); + + # Print per file information foreach $filename (sort(keys(%{$data}))) { + my @file_data; + my $print_filename = $filename; + $entry = $data->{$filename}; - (undef, undef, undef, undef, $found, $hit) = + if (!$opt_list_full_path) { + my $p; + + $print_filename = $filename; + if (!$got_prefix || !$root_prefix && + !($print_filename =~ s/^\Q$prefix\/\E//)) { + my ($v, $d, $f) = splitpath($filename); + + $p = catpath($v, $d, ""); + $p =~ s/\/$//; + $print_filename = $f; + } else { + $p = $prefix; + } + + if (!defined($lastpath) || $lastpath ne $p) { + print("\n") if (defined($lastpath)); + $lastpath = $p; + print("[$lastpath/]\n") if (!$root_prefix); + } + $print_filename = shorten_filename($print_filename, + $strlen); + } + + (undef, undef, undef, undef, undef, undef, undef, undef, + $found, $hit, $fn_found, $fn_hit, $br_found, $br_hit) = get_info_entry($entry); - printf("$filename: $hit of $found lines hit\n"); + + # Assume zero count if there is no function data for this file + if (!defined($fn_found) || !defined($fn_hit)) { + $fn_found = 0; + $fn_hit = 0; + } + # Assume zero count if there is no branch data for this file + if (!defined($br_found) || !defined($br_hit)) { + $br_found = 0; + $br_hit = 0; + } + + # Add line coverage totals + $total_found += $found; + $total_hit += $hit; + # Add function coverage totals + $fn_total_found += $fn_found; + $fn_total_hit += $fn_hit; + # Add branch coverage totals + $br_total_found += $br_found; + $br_total_hit += $br_hit; + + # Determine line coverage rate for this file + if ($found == 0) { + $rate = "-"; + } else { + $rate = shorten_rate(100 * $hit / $found, + $fwidth[$F_LN_RATE]); + } + # Determine function coverage rate for this file + if (!defined($fn_found) || $fn_found == 0) { + $fnrate = "-"; + } else { + $fnrate = shorten_rate(100 * $fn_hit / $fn_found, + $fwidth[$F_FN_RATE]); + } + # Determine branch coverage rate for this file + if (!defined($br_found) || $br_found == 0) { + $brrate = "-"; + } else { + $brrate = shorten_rate(100 * $br_hit / $br_found, + $fwidth[$F_BR_RATE]); + } + + # Assemble line parameters + push(@file_data, $print_filename); + push(@file_data, $rate); + push(@file_data, shorten_number($found, $fwidth[$F_LN_NUM])); + push(@file_data, $fnrate); + push(@file_data, shorten_number($fn_found, $fwidth[$F_FN_NUM])); + push(@file_data, $brrate); + push(@file_data, shorten_number($br_found, $fwidth[$F_BR_NUM])); + + # Print assembled line + printf($format, @file_data); } + + # Determine total line coverage rate + if ($total_found == 0) { + $rate = "-"; + } else { + $rate = shorten_rate(100 * $total_hit / $total_found, + $fwidth[$F_LN_RATE]); + } + # Determine total function coverage rate + if ($fn_total_found == 0) { + $fnrate = "-"; + } else { + $fnrate = shorten_rate(100 * $fn_total_hit / $fn_total_found, + $fwidth[$F_FN_RATE]); + } + # Determine total branch coverage rate + if ($br_total_found == 0) { + $brrate = "-"; + } else { + $brrate = shorten_rate(100 * $br_total_hit / $br_total_found, + $fwidth[$F_BR_RATE]); + } + + # Print separator + print(("="x$barlen)."\n"); + + # Assemble line parameters + push(@footer, sprintf("%*s", $strlen, "Total:")); + push(@footer, $rate); + push(@footer, shorten_number($total_found, $fwidth[$F_LN_NUM])); + push(@footer, $fnrate); + push(@footer, shorten_number($fn_total_found, $fwidth[$F_FN_NUM])); + push(@footer, $brrate); + push(@footer, shorten_number($br_total_found, $fwidth[$F_BR_NUM])); + + # Print assembled line + printf($format, @footer); } @@ -1863,7 +3413,7 @@ sub apply_diff($$) # Transform all other lines which come after the last diff entry foreach (sort({$a <=> $b} keys(%{$count_data}))) { - if ($_ < $last_old) + if ($_ <= $last_old) { # Skip lines which were covered by line hash next; @@ -1876,10 +3426,92 @@ sub apply_diff($$) } +# +# apply_diff_to_brcount(brcount, linedata) +# +# Adjust line numbers of branch coverage data according to linedata. +# + +sub apply_diff_to_brcount($$) +{ + my ($brcount, $linedata) = @_; + my $db; + + # Convert brcount to db format + $db = brcount_to_db($brcount); + # Apply diff to db format + $db = apply_diff($db, $linedata); + # Convert db format back to brcount format + ($brcount) = db_to_brcount($db); + + return $brcount; +} + + +# +# get_hash_max(hash_ref) +# +# Return the highest integer key from hash. +# + +sub get_hash_max($) +{ + my ($hash) = @_; + my $max; + + foreach (keys(%{$hash})) { + if (!defined($max)) { + $max = $_; + } elsif ($hash->{$_} > $max) { + $max = $_; + } + } + return $max; +} + +sub get_hash_reverse($) +{ + my ($hash) = @_; + my %result; + + foreach (keys(%{$hash})) { + $result{$hash->{$_}} = $_; + } + + return \%result; +} + +# +# apply_diff_to_funcdata(funcdata, line_hash) +# + +sub apply_diff_to_funcdata($$) +{ + my ($funcdata, $linedata) = @_; + my $last_new = get_hash_max($linedata); + my $last_old = $linedata->{$last_new}; + my $func; + my %result; + my $line_diff = get_hash_reverse($linedata); + + foreach $func (keys(%{$funcdata})) { + my $line = $funcdata->{$func}; + + if (defined($line_diff->{$line})) { + $result{$func} = $line_diff->{$line}; + } elsif ($line > $last_old) { + $result{$func} = $line + $last_new - $last_old; + } + } + + return \%result; +} + + # # get_line_hash($filename, $diff_data, $path_data) # -# Find line hash in DIFF_DATA which matches FILENAME. On succes, return list +# Find line hash in DIFF_DATA which matches FILENAME. On success, return list # line hash. or undef in case of no match. Die if more than one line hashes in # DIFF_DATA match. # @@ -1897,10 +3529,16 @@ sub get_line_hash($$$) my $old_depth; my $new_depth; + # Remove trailing slash from diff path + $diff_path =~ s/\/$//; foreach (keys(%{$diff_data})) { + my $sep = ""; + + $sep = '/' if (!/^\//); + # Try to match diff filename with filename - if ($filename =~ /^\Q$diff_path\E\/$_$/) + if ($filename =~ /^\Q$diff_path$sep$_\E$/) { if ($diff_name) { @@ -2013,6 +3651,63 @@ sub convert_paths($$) } } +# +# sub adjust_fncdata(funcdata, testfncdata, sumfnccount) +# +# Remove function call count data from testfncdata and sumfnccount which +# is no longer present in funcdata. +# + +sub adjust_fncdata($$$) +{ + my ($funcdata, $testfncdata, $sumfnccount) = @_; + my $testname; + my $func; + my $f_found; + my $f_hit; + + # Remove count data in testfncdata for functions which are no longer + # in funcdata + foreach $testname (%{$testfncdata}) { + my $fnccount = $testfncdata->{$testname}; + + foreach $func (%{$fnccount}) { + if (!defined($funcdata->{$func})) { + delete($fnccount->{$func}); + } + } + } + # Remove count data in sumfnccount for functions which are no longer + # in funcdata + foreach $func (%{$sumfnccount}) { + if (!defined($funcdata->{$func})) { + delete($sumfnccount->{$func}); + } + } +} + +# +# get_func_found_and_hit(sumfnccount) +# +# Return (f_found, f_hit) for sumfnccount +# + +sub get_func_found_and_hit($) +{ + my ($sumfnccount) = @_; + my $function; + my $f_found; + my $f_hit; + + $f_found = scalar(keys(%{$sumfnccount})); + $f_hit = 0; + foreach $function (keys(%{$sumfnccount})) { + if ($sumfnccount->{$function} > 0) { + $f_hit++; + } + } + return ($f_found, $f_hit); +} # # diff() @@ -2035,10 +3730,19 @@ sub diff() my $sumcount; my $funcdata; my $checkdata; + my $testfncdata; + my $sumfnccount; + my $testbrdata; + my $sumbrcount; my $found; my $hit; + my $f_found; + my $f_hit; + my $br_found; + my $br_hit; my $converted = 0; my $unchanged = 0; + my @result; local *INFO_HANDLE; ($diff_data, $path_data) = read_diff($ARGV[0]); @@ -2068,17 +3772,25 @@ sub diff() } info("Converting $filename\n"); $entry = $trace_data->{$filename}; - ($testdata, $sumcount, $funcdata, $checkdata) = + ($testdata, $sumcount, $funcdata, $checkdata, $testfncdata, + $sumfnccount, $testbrdata, $sumbrcount) = get_info_entry($entry); # Convert test data foreach $testname (keys(%{$testdata})) { + # Adjust line numbers of line coverage data $testdata->{$testname} = apply_diff($testdata->{$testname}, $line_hash); + # Adjust line numbers of branch coverage data + $testbrdata->{$testname} = + apply_diff_to_brcount($testbrdata->{$testname}, + $line_hash); # Remove empty sets of test data if (scalar(keys(%{$testdata->{$testname}})) == 0) { delete($testdata->{$testname}); + delete($testfncdata->{$testname}); + delete($testbrdata->{$testname}); } } # Rename test data to indicate conversion @@ -2097,16 +3809,43 @@ sub diff() $testdata->{$testname}, $testdata->{$testname.",diff"}); delete($testdata->{$testname.",diff"}); + # Add function call counts + ($testfncdata->{$testname}) = add_fnccount( + $testfncdata->{$testname}, + $testfncdata->{$testname.",diff"}); + delete($testfncdata->{$testname.",diff"}); + # Add branch counts + ($testbrdata->{$testname}) = combine_brcount( + $testbrdata->{$testname}, + $testbrdata->{$testname.",diff"}, + $BR_ADD); + delete($testbrdata->{$testname.",diff"}); } + # Move test data to new testname $testdata->{$testname.",diff"} = $testdata->{$testname}; delete($testdata->{$testname}); + # Move function call count data to new testname + $testfncdata->{$testname.",diff"} = + $testfncdata->{$testname}; + delete($testfncdata->{$testname}); + # Move branch count data to new testname + $testbrdata->{$testname.",diff"} = + $testbrdata->{$testname}; + delete($testbrdata->{$testname}); } # Convert summary of test data $sumcount = apply_diff($sumcount, $line_hash); # Convert function data - $funcdata = apply_diff($funcdata, $line_hash); + $funcdata = apply_diff_to_funcdata($funcdata, $line_hash); + # Convert branch coverage data + $sumbrcount = apply_diff_to_brcount($sumbrcount, $line_hash); + # Update found/hit numbers # Convert checksum data $checkdata = apply_diff($checkdata, $line_hash); + # Convert function call count data + adjust_fncdata($funcdata, $testfncdata, $sumfnccount); + ($f_found, $f_hit) = get_func_found_and_hit($sumfnccount); + ($br_found, $br_hit) = get_br_found_and_hit($sumbrcount); # Update found/hit numbers $found = 0; $hit = 0; @@ -2122,7 +3861,9 @@ sub diff() { # Store converted entry set_info_entry($entry, $testdata, $sumcount, $funcdata, - $checkdata, $found, $hit); + $checkdata, $testfncdata, $sumfnccount, + $testbrdata, $sumbrcount, $found, $hit, + $f_found, $f_hit, $br_found, $br_hit); } else { @@ -2147,13 +3888,15 @@ sub diff() info("Writing data to $output_filename\n"); open(INFO_HANDLE, ">$output_filename") or die("ERROR: cannot write to $output_filename!\n"); - write_info_file(*INFO_HANDLE, $trace_data); + @result = write_info_file(*INFO_HANDLE, $trace_data); close(*INFO_HANDLE); } else { - write_info_file(*STDOUT, $trace_data); + @result = write_info_file(*STDOUT, $trace_data); } + + return @result; } @@ -2269,3 +4012,164 @@ sub apply_config($) } } } + +sub warn_handler($) +{ + my ($msg) = @_; + + temp_cleanup(); + warn("$tool_name: $msg"); +} + +sub die_handler($) +{ + my ($msg) = @_; + + temp_cleanup(); + die("$tool_name: $msg"); +} + +sub abort_handler($) +{ + temp_cleanup(); + exit(1); +} + +sub temp_cleanup() +{ + if (@temp_dirs) { + info("Removing temporary directories.\n"); + foreach (@temp_dirs) { + rmtree($_); + } + @temp_dirs = (); + } +} + +sub setup_gkv_sys() +{ + system_no_output(3, "mount", "-t", "debugfs", "nodev", + "/sys/kernel/debug"); +} + +sub setup_gkv_proc() +{ + if (system_no_output(3, "modprobe", "gcov_proc")) { + system_no_output(3, "modprobe", "gcov_prof"); + } +} + +sub check_gkv_sys($) +{ + my ($dir) = @_; + + if (-e "$dir/reset") { + return 1; + } + return 0; +} + +sub check_gkv_proc($) +{ + my ($dir) = @_; + + if (-e "$dir/vmlinux") { + return 1; + } + return 0; +} + +sub setup_gkv() +{ + my $dir; + my $sys_dir = "/sys/kernel/debug/gcov"; + my $proc_dir = "/proc/gcov"; + my @todo; + + if (!defined($gcov_dir)) { + info("Auto-detecting gcov kernel support.\n"); + @todo = ( "cs", "cp", "ss", "cs", "sp", "cp" ); + } elsif ($gcov_dir =~ /proc/) { + info("Checking gcov kernel support at $gcov_dir ". + "(user-specified).\n"); + @todo = ( "cp", "sp", "cp", "cs", "ss", "cs"); + } else { + info("Checking gcov kernel support at $gcov_dir ". + "(user-specified).\n"); + @todo = ( "cs", "ss", "cs", "cp", "sp", "cp", ); + } + foreach (@todo) { + if ($_ eq "cs") { + # Check /sys + $dir = defined($gcov_dir) ? $gcov_dir : $sys_dir; + if (check_gkv_sys($dir)) { + info("Found ".$GKV_NAME[$GKV_SYS]." gcov ". + "kernel support at $dir\n"); + return ($GKV_SYS, $dir); + } + } elsif ($_ eq "cp") { + # Check /proc + $dir = defined($gcov_dir) ? $gcov_dir : $proc_dir; + if (check_gkv_proc($dir)) { + info("Found ".$GKV_NAME[$GKV_PROC]." gcov ". + "kernel support at $dir\n"); + return ($GKV_PROC, $dir); + } + } elsif ($_ eq "ss") { + # Setup /sys + setup_gkv_sys(); + } elsif ($_ eq "sp") { + # Setup /proc + setup_gkv_proc(); + } + } + if (defined($gcov_dir)) { + die("ERROR: could not find gcov kernel data at $gcov_dir\n"); + } else { + die("ERROR: no gcov kernel data found\n"); + } +} + + +# +# get_overall_line(found, hit, name_singular, name_plural) +# +# Return a string containing overall information for the specified +# found/hit data. +# + +sub get_overall_line($$$$) +{ + my ($found, $hit, $name_sn, $name_pl) = @_; + my $name; + + return "no data found" if (!defined($found) || $found == 0); + $name = ($found == 1) ? $name_sn : $name_pl; + return sprintf("%.1f%% (%d of %d %s)", $hit * 100 / $found, $hit, + $found, $name); +} + + +# +# print_overall_rate(ln_do, ln_found, ln_hit, fn_do, fn_found, fn_hit, br_do +# br_found, br_hit) +# +# Print overall coverage rates for the specified coverage types. +# + +sub print_overall_rate($$$$$$$$$) +{ + my ($ln_do, $ln_found, $ln_hit, $fn_do, $fn_found, $fn_hit, + $br_do, $br_found, $br_hit) = @_; + + info("Overall coverage rate:\n"); + info(" lines......: %s\n", + get_overall_line($ln_found, $ln_hit, "line", "lines")) + if ($ln_do); + info(" functions..: %s\n", + get_overall_line($fn_found, $fn_hit, "function", "functions")) + if ($fn_do); + info(" branches...: %s\n", + get_overall_line($br_found, $br_hit, "branch", "branches")) + if ($br_do); +} diff --git a/wscript b/wscript index 32941c1b6..bc5040bc1 100644 --- a/wscript +++ b/wscript @@ -313,7 +313,7 @@ def configure(conf): env.append_value('CCFLAGS', '-ftest-coverage') env.append_value('CXXFLAGS', '-fprofile-arcs') env.append_value('CXXFLAGS', '-ftest-coverage') - env.append_value('LINKFLAGS', '-fprofile-arcs') + env.append_value('LINKFLAGS', '-lgcov') if Options.options.build_profile == 'debug': env.append_value('DEFINES', 'NS3_ASSERT_ENABLE') @@ -1053,8 +1053,7 @@ def lcov_report(bld): info_file = os.path.join(lcov_report_dir, 'report.info') lcov_command = "../utils/lcov/lcov -c -d . -o " + info_file - lcov_command += " --source-dirs=" + os.getcwd() - lcov_command += ":" + os.path.join(os.getcwd(), 'include') + lcov_command += " -b " + os.getcwd() if subprocess.Popen(lcov_command, shell=True).wait(): raise SystemExit(1)

$func_code$count_code
$name$count