| #!/usr/bin/perl |
| # SPDX-License-Identifier: MIT |
| use strict; |
| use warnings; |
| use Getopt::Long; |
| BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Termcap'; } |
| use Pod::Usage; |
| use Pod::Man; |
| |
| my $prefix = qr ".*?(linux)\w*/"; |
| |
| my $title = ""; |
| |
| my %used_func; |
| my %all_func; |
| my %all_branch; |
| my %all_line; |
| my %used_source; |
| my %record; |
| my %files; |
| my @func_regexes; |
| my @func_exclude_regexes; |
| my %test_names; |
| my @src_regexes; |
| my @src_exclude_regexes; |
| |
| my $verbose = 0; |
| my $ignore_unused = 0; |
| my $skip_func = 0; |
| |
| sub is_function_excluded($) |
| { |
| return 0 if (!@func_regexes && !@func_exclude_regexes); |
| |
| my $func = shift; |
| |
| foreach my $r (@func_exclude_regexes) { |
| return 1 if ($func =~ m/$r/); |
| } |
| |
| return 0 if (!@func_regexes); |
| |
| foreach my $r (@func_regexes) { |
| return 0 if ($func =~ m/$r/); |
| } |
| |
| return 1; |
| } |
| |
| sub filter_file($) |
| { |
| my $s = shift; |
| |
| return 0 if (!@src_regexes && !@src_exclude_regexes); |
| |
| foreach my $r (@src_exclude_regexes) { |
| return 1 if ($s =~ m/$r/); |
| } |
| |
| return 0 if (!@src_regexes); |
| |
| foreach my $r (@src_regexes) { |
| return 0 if ($s =~ m/$r/); |
| } |
| |
| return 1; |
| } |
| |
| # Use something that comes before any real function |
| my $before_sf = "!!!!"; |
| |
| sub parse_info_data($) |
| { |
| my $file = shift; |
| my $was_used = 0; |
| my $has_func = 0; |
| my $ignore = 0; |
| my $source = $before_sf; |
| my $func = $before_sf; |
| my $cur_test = ""; |
| |
| # First step: parse data |
| |
| print "reading $file...\n" if ($verbose); |
| open IN, $file or die "can't open $file"; |
| # For details on .info file format, see "man geninfo" |
| # http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php |
| while (<IN>) { |
| # TN:<test name> |
| if (m/^TN:(.*)/) { |
| if ($1 ne $cur_test) { |
| $cur_test = $1; |
| $test_names{$cur_test} = 1; |
| } |
| $source = $before_sf; |
| $func = $before_sf; |
| next; |
| } |
| |
| # SF:<absolute path to the source file> |
| if (m/^[SK]F:(.*)/) { |
| $source = $1; |
| |
| $was_used = 0; |
| $has_func = 0; |
| $func = $before_sf; |
| $files{$source} = 1; |
| |
| # Just ignore files explictly set as such |
| $ignore = filter_file($source); |
| next; |
| } |
| |
| # End of record |
| if (m/^end_of_record/) { |
| if (!$source) { |
| print "bad end_of_record field at $file, line $. Ignoring...\n"; |
| next; |
| } |
| |
| my $s = $source; |
| |
| $source = $before_sf; |
| $func = $before_sf; |
| |
| next if ($ignore); |
| next if ($ignore_unused && !$was_used); |
| |
| # Mark that the source was not ignored |
| $used_source{$s} = 1; |
| next; |
| } |
| |
| next if ($ignore); |
| |
| # Function coverage |
| |
| # FN:<line number of function start>,<function name> |
| # Note: /w+ intentionally removes IPA gcc-optimization names, as |
| # this is more related to branch coverage |
| if (m/^FN:(-?\d+),(\w+)/) { |
| my $ln = $1; |
| |
| $func = $2; |
| $has_func = 1; |
| |
| if (is_function_excluded($func)) { |
| $skip_func = 1; |
| next; |
| } |
| |
| $skip_func = 0; |
| |
| $record{$source}{$func}{fn} = $ln; |
| $all_func{$func}{$source}->{ln} = $ln; |
| next; |
| } |
| |
| # Parse functions that were actually used |
| # FNDA:<execution count>,<function name> |
| # Note: /w+ intentionally removes IPA gcc-optimization names, as |
| # this is more related to branch coverage |
| if (m/^FNDA:(-?\d+),(\w+)/) { |
| my $count = $1; |
| |
| # Negative gcov results are possible, as reported at: |
| # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67937 |
| # Lcov ignores those. So, let's do the same here. |
| next if ($count <= 0); |
| |
| $func = $2; |
| $has_func = 1; |
| |
| if (is_function_excluded($func)) { |
| $skip_func = 1; |
| next; |
| } |
| |
| $skip_func = 0; |
| $was_used = 1; |
| |
| $record{$source}{$func}{fnda} += $count; |
| $used_func{$func}{$source}->{count} += $count; |
| next; |
| } |
| |
| # Ignore data from skipped functions |
| next if ($skip_func); |
| |
| # Ignore DA/BRDA that aren't associated with functions |
| # Those are present on header files (maybe defines?) |
| next if (@func_regexes && !$has_func); |
| |
| # FNF:<number of functions found> |
| if (m/^FNF:(-?\d+)/) { |
| $record{$source}{$func}{fnf} = $1; |
| next; |
| } |
| # FNH:<number of function hit> |
| if (m/^FNH:(-?\d+)/) { |
| my $hits = $1; |
| if (!defined($record{$source}{$func}{fnh}) || $record{$source}{$func}{fnh} < $hits) { |
| $record{$source}{$func}{fnh} = $hits; |
| } |
| next; |
| } |
| |
| # Branch coverage |
| |
| # BRDA:<line number>,<block number>,<branch number>,<taken> |
| if (m/^BRDA:(-?\d+),(-?\d+),(-?\d+),(.*)/) { |
| my $ln = $1; |
| my $block = $2; |
| my $branch = $3; |
| my $taken = $4; |
| |
| my $where = "$ln,$block,$branch"; |
| |
| $taken = 0 if ($taken eq '-'); |
| |
| # Negative gcov results are possible, as reported at: |
| # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67937 |
| # Lcov ignores those. So, let's do the same here. |
| $taken = 0 if ($taken < 0); |
| |
| $was_used = 1 if ($taken > 0); |
| |
| $record{$source}{$func}{brda}{$where} += $taken; |
| $all_branch{$source}{"$where"} += $taken; |
| next; |
| } |
| |
| # BRF:<number of branches found> |
| if (m/^BRF:(-?\d+)/) { |
| $record{$source}{$func}{brf} = $1; |
| next; |
| } |
| # BRH:<number of branches hit> |
| if (m/^BRH:(-?\d+)/) { |
| my $hits = $1; |
| if (!defined($record{$source}{$func}{brh}) || $record{$source}{$func}{brh} < $hits) { |
| $record{$source}{$func}{brh} = $hits; |
| } |
| next; |
| } |
| |
| # Line coverage |
| |
| # DA:<line number>,<execution count>[,<checksum>] |
| if (m/^DA:(-?\d+),(-?\d+)(,.*)?/) { |
| my $ln = $1; |
| my $count = $2; |
| |
| # Negative gcov results are possible, as reported at: |
| # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67937 |
| # Lcov ignores those. So, let's do the same here. |
| $count = 0 if ($count < 0); |
| |
| $was_used = 1 if ($count > 0); |
| |
| $record{$source}{$func}{da}{$ln} += $count; |
| $all_line{$source}{"$ln"} += $count; |
| next; |
| } |
| |
| # LF:<number of instrumented lines> |
| if (m/^LF:(-?\d+)/) { |
| $record{$source}{$func}{lf} = $1; |
| next; |
| } |
| |
| # LH:<number of lines with a non-zero execution count> |
| if (m/^LH:(-?\d+)/) { |
| my $hits = $1; |
| if (!defined($record{$source}{$func}{lh}) || $record{$source}{$func}{lh} < $hits) { |
| $record{$source}{$func}{lh} = $hits; |
| } |
| next; |
| } |
| |
| printf("Warning: invalid line: $_"); |
| } |
| |
| close IN or die; |
| } |
| |
| sub write_filtered_file($) |
| { |
| my $filter = shift; |
| |
| my $filtered = ""; |
| |
| if ($title eq "") { |
| foreach my $testname(sort keys %test_names) { |
| $filtered .= "TN:$testname\n"; |
| } |
| } else { |
| $filtered .= "TN:$title\n"; |
| } |
| |
| # Generates filtered data |
| foreach my $source(sort keys %record) { |
| next if (!$used_source{$source}); |
| |
| if ($source ne $before_sf) { |
| $filtered .= "SF:$source\n"; |
| } |
| |
| foreach my $func(sort keys %{ $record{$source} }) { |
| if ($func ne $before_sf) { |
| my $fn; |
| my $fnda; |
| |
| if (defined($record{$source}{$func}{fn})) { |
| $filtered .= "FN:" . $record{$source}{$func}{fn} . ",$func\n"; |
| } |
| if (defined($record{$source}{$func}{fnda})) { |
| $filtered .= "FNDA:" . $record{$source}{$func}{fnda} . ",$func\n"; |
| } |
| if ($record{$source}{fnf}) { |
| $filtered .= "FNF:". $record{$source}{$func}{fnf} ."\n"; |
| } |
| if ($record{$source}{fnh}) { |
| $filtered .= "FNH:". $record{$source}{$func}{fnh} ."\n"; |
| } |
| } |
| |
| foreach my $ln(sort keys %{ $record{$source}{$func}{da} }) { |
| $filtered .= "DA:$ln," . $record{$source}{$func}{da}{$ln} . "\n"; |
| } |
| foreach my $where(sort keys %{ $record{$source}{$func}{brda} }) { |
| my $taken = $record{$source}{$func}{brda}{$where}; |
| $taken = "-" if (!$taken); |
| $filtered .= "BRDA:$where,$taken\n"; |
| } |
| if ($record{$source}{$func}{brf}) { |
| $filtered .= "BRF:". $record{$source}{$func}{brf} ."\n"; |
| } |
| if ($record{$source}{$func}{brh}) { |
| $filtered .= "BRH:". $record{$source}{$func}{brh} ."\n"; |
| } |
| if ($record{$source}{$func}{lf}) { |
| $filtered .= "LF:". $record{$source}{$func}{lf} ."\n"; |
| } |
| if ($record{$source}{$func}{lh}) { |
| $filtered .= "LH:". $record{$source}{$func}{lh} ."\n"; |
| } |
| } |
| |
| $filtered .= "end_of_record\n"; |
| } |
| open OUT, ">$filter" or die "Can't open $filter"; |
| print OUT $filtered or die "Failed to write to $filter"; |
| close OUT or die "Failed to close to $filter"; |
| } |
| |
| sub print_code_coverage($$$) |
| { |
| my $print_used = shift; |
| my $print_unused = shift; |
| my $show_lines = shift; |
| |
| return if (!$print_used && !$print_unused); |
| |
| my $prev_file = ""; |
| |
| foreach my $func (sort keys(%all_func)) { |
| my @keys = sort keys(%{$all_func{$func}}); |
| foreach my $file (@keys) { |
| my $count = 0; |
| my $name; |
| |
| if ($used_func{$func}) { |
| if ($used_func{$func}->{$file}) { |
| $count = $used_func{$func}->{$file}->{count}; |
| } |
| } |
| |
| if ($show_lines) { |
| my $ln = $all_func{$func}{$file}->{ln}; |
| $file =~ s,$prefix,linux/,; |
| $name = "$func() from $file"; |
| $name .= ":" . $ln if ($ln); |
| } elsif (scalar @keys > 1) { |
| $file =~ s,$prefix,linux/,; |
| $name = "$func() from $file:"; |
| } else { |
| $name = "$func():"; |
| } |
| if ($print_unused) { |
| if (!$count) { |
| print "$name unused\n"; |
| } elsif ($print_used) { |
| print "$name executed $count times\n"; |
| } |
| } elsif ($count) { |
| print "$name executed $count times\n"; |
| } |
| } |
| } |
| } |
| |
| my %stats; |
| |
| sub gen_stats() |
| { |
| # per-line coverage statistics |
| $stats{"line_count"} = 0; |
| $stats{"line_reached"} = 0; |
| |
| foreach my $source (keys(%all_line)) { |
| next if (!$used_source{$source}); |
| |
| foreach my $where (keys(%{$all_line{$source}})) { |
| $stats{"line_count"}++; |
| $stats{"line_reached"}++ if ($all_line{$source}{$where} != 0); |
| } |
| } |
| |
| # per-function coverage statistics |
| $stats{"func_count"} = 0; |
| $stats{"func_used"} = 0; |
| |
| foreach my $func (keys(%all_func)) { |
| foreach my $file (keys(%{$all_func{$func}})) { |
| $stats{"func_count"}++; |
| if ($used_func{$func}) { |
| if ($used_func{$func}->{$file}) { |
| $stats{"func_used"}++; |
| } |
| } |
| } |
| } |
| |
| # per-branch coverage statistics |
| $stats{"branch_count"} = 0; |
| $stats{"branch_reached"} = 0; |
| |
| foreach my $source (keys(%all_branch)) { |
| next if (!$used_source{$source}); |
| |
| foreach my $where (keys(%{$all_branch{$source}})) { |
| $stats{"branch_count"}++; |
| $stats{"branch_reached"}++ if ($all_branch{$source}{$where} != 0); |
| } |
| } |
| |
| # per-file coverage stats |
| $stats{"all_files"} = scalar keys(%files); |
| $stats{"filtered_files"} = scalar keys(%record); |
| $stats{"used_files"} = scalar keys(%used_source); |
| } |
| |
| sub print_summary() |
| { |
| if ($stats{"line_count"}) { |
| my $percent = 100. * $stats{"line_reached"} / $stats{"line_count"}; |
| printf " lines......: %.1f%% (%d of %d lines)\n", |
| $percent, $stats{"line_reached"}, $stats{"line_count"}; |
| } else { |
| print "No line coverage data.\n"; |
| } |
| |
| if ($stats{"func_count"}) { |
| my $percent = 100. * $stats{"func_used"} / $stats{"func_count"}; |
| printf " functions..: %.1f%% (%d of %d functions)\n", |
| $percent, $stats{"func_used"}, $stats{"func_count"}; |
| } else { |
| print "No functions reported. Wrong filters?\n"; |
| return; |
| } |
| |
| if ($stats{"branch_count"}) { |
| my $percent = 100. * $stats{"branch_reached"} / $stats{"branch_count"}; |
| printf " branches...: %.1f%% (%d of %d branches)\n", |
| $percent, $stats{"branch_reached"}, $stats{"branch_count"}; |
| } else { |
| print "No branch coverage data.\n"; |
| } |
| } |
| |
| sub open_filter_file($$$) |
| { |
| my $fname = shift; |
| my $include = shift; |
| my $exclude = shift; |
| my $match_str = ""; |
| my $not_match_str = ""; |
| my $filter = ""; |
| my $i; |
| |
| # Handle regexes that came from command line params |
| |
| for ($i = 0;$i < scalar(@{$include}); $i++) { |
| my $op = @{$include}[$i]; |
| $match_str .= sprintf "m`$op` "; |
| @{$include}[$i] = qr /$op/; |
| } |
| |
| for ($i = 0;$i < scalar(@{$exclude}); $i++) { |
| my $op = @{$exclude}[$i]; |
| $not_match_str .= sprintf "m`$op` "; |
| @{$exclude}[$i] = qr /$op/; |
| } |
| |
| if ($fname) { |
| open IN, $fname or die "Can't open $fname"; |
| while (<IN>) { |
| s/^\s+//; |
| s/\s+$//; |
| next if (m/^#/ || m/^$/); |
| if (m/^([+\-])\s*(.*)/) { |
| if ($1 eq "+") { |
| $match_str .= sprintf "m`$2` "; |
| push @{$include}, qr /$2/; |
| } else { |
| $not_match_str .= sprintf "m`$2` "; |
| push @{$exclude}, qr /$2/; |
| } |
| } else { |
| $match_str .= sprintf "m`$_` "; |
| push @{$include}, qr /$_/; |
| } |
| } |
| close IN; |
| } |
| |
| $filter .= "not match: $not_match_str" if ($not_match_str); |
| if ($match_str) { |
| $filter .= "and " if ($filter ne ""); |
| $filter .= "match: $match_str"; |
| } |
| |
| $filter =~ s/\s+$//; |
| |
| return $filter; |
| } |
| |
| my $gen_report; |
| my $css_file; |
| my $html_prolog; |
| my $html_epilog; |
| my %report; |
| |
| sub generate_report($) |
| { |
| my $filter_str = shift; |
| my $percent; |
| my $prolog = ""; |
| my $epilog = ""; |
| my @info_files = sort(keys %report); |
| |
| $title = "Code coverage results" if ($title eq ""); |
| |
| if ($html_prolog) { |
| open IN, $html_prolog or die "Can't open prolog file"; |
| $prolog .= $_ while (<IN>); |
| close IN; |
| } |
| |
| if ($html_epilog) { |
| open IN, $html_epilog or die "Can't open epilog file"; |
| $epilog .= $_ while (<IN>); |
| close IN; |
| } |
| |
| # Re-generate the hashes used to report stats in order to procuce the |
| # Total results |
| |
| %used_func = (); |
| %all_func = (); |
| %all_branch = (); |
| %all_line = (); |
| %used_source = (); |
| %files = (); |
| %test_names = (); |
| my @all_func_keys; |
| my %info_files_with_func; |
| |
| foreach my $f (@info_files) { |
| foreach my $source (keys(%{$report{$f}{"all_line"}})) { |
| $used_source{$source} = 1 if ($report{$f}{"used_source"}); |
| foreach my $where (keys(%{$report{$f}{"all_line"}{$source}})) { |
| $all_line{$source}{$where} += $report{$f}{"all_line"}{$source}{$where}; |
| } |
| } |
| foreach my $func (keys(%{$report{$f}{"all_func"}})) { |
| foreach my $file (keys(%{$report{$f}{"all_func"}{$func}})) { |
| $all_func{$func}{$file}->{ln} = $report{$f}{"all_func"}{$func}{$file}->{ln}; |
| $used_func{$func}->{$file} = 1 if ($report{$f}{"used_func"}{$func}->{$file}); |
| if ($report{$f}{"used_func"}{$func}->{$file}) { |
| $used_func{$func}->{$file} = 1; |
| if (!$info_files_with_func{"$file $func"}) { |
| $info_files_with_func{"$file $func"} = 1; |
| } else { |
| $info_files_with_func{"$file $func"}++; |
| } |
| } |
| } |
| } |
| foreach my $source (keys(%{$report{$f}{"all_branch"}})) { |
| foreach my $where (keys(%{$report{$f}{"all_branch"}{$source}})) { |
| $all_branch{$source}{"$where"} += $report{$f}{"all_branch"}{$source}{$where}; |
| } |
| } |
| for my $source(keys(%{$report{$f}{"files"}})) { |
| $files{$source} = 1; |
| $used_source{$source} = 1 if ($report{$f}{"used_source"}{$source}); |
| } |
| for my $test(keys(%{$report{$f}{"test_names"}})) { |
| $test_names{$test} = 1; |
| } |
| } |
| gen_stats(); |
| |
| # Gen code coverage set comparision counters |
| my $common_func_count = 0; |
| foreach my $k (keys %info_files_with_func) { |
| $common_func_count++ if ($info_files_with_func{$k} == scalar (@info_files)); |
| } |
| foreach my $f (@info_files) { |
| $report{$f}{"more_func"} = 0; |
| $report{$f}{"uniq_func"} = 0; |
| foreach my $func (keys(%{$report{$f}{"all_func"}})) { |
| foreach my $file (keys(%{$report{$f}{"all_func"}{$func}})) { |
| next if (!$report{$f}{"used_func"}{$func}->{$file}); |
| next if ($info_files_with_func{"$file $func"} == scalar (@info_files)); |
| |
| $report{$f}{"more_func"}++; |
| $report{$f}{"uniq_func"}++ if ($info_files_with_func{"$file $func"} == 1); |
| } |
| } |
| } |
| |
| # Colors for the html output |
| |
| my $red = "style=\"background-color:#ffb3b3\""; |
| my $yellow = "style=\"background-color:#ffffb3\""; |
| my $green = "style=\"background-color:#d9ffd9\""; |
| |
| # Open report file |
| |
| open OUT, ">$gen_report" or die "Can't open $gen_report"; |
| |
| print OUT "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n"; |
| |
| print OUT "<html lang=\"en\">\n\n"; |
| print OUT "<head>\n"; |
| print OUT " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n"; |
| print OUT " <title>$title</title>\n"; |
| print OUT " <link rel=\"stylesheet\" type=\"text/css\" href=\"$css_file\">\n" if ($css_file); |
| print OUT "</head>\n\n<body>\n$prolog"; |
| |
| print OUT " <h1>$title</h1>\n"; |
| |
| print OUT " <h2>Summary</h2>\n"; |
| # Generates a table containing the code coverage statistics per input |
| |
| print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n <tr>\n"; |
| print OUT " <th></th>\n"; |
| foreach my $f (@info_files) { |
| print OUT " <th>$f</th>\n"; |
| } |
| print OUT " <th>TOTAL</th>\n"; |
| print OUT " <th>Total count</th>\n"; |
| print OUT " </tr><tr>\n"; |
| |
| print OUT " <td><b>Functions</b></td>\n"; |
| foreach my $f (@info_files) { |
| my %st = %{$report{$f}{"stats"}}; |
| if ($st{"func_count"}) { |
| $percent = 100. * $st{"func_used"} / $st{"func_count"}; |
| |
| printf OUT " <td>%.1f%%</td>\n", $percent; |
| } else { |
| print OUT " <td>N. A.</td>\n"; |
| } |
| } |
| if ($stats{"func_count"}) { |
| $percent = 100. * $stats{"func_used"} / $stats{"func_count"}; |
| |
| printf OUT " <td>%.1f%%</td>\n", $percent; |
| } else { |
| print OUT " <td>N. A.</td>\n"; |
| } |
| print OUT " <td>" . $stats{"func_count"} . "</td>\n"; |
| print OUT " </tr><tr>\n"; |
| |
| print OUT " <td><b>Branches</b></td>\n"; |
| foreach my $f (@info_files) { |
| my %st = %{$report{$f}{"stats"}}; |
| if ($st{"branch_count"}) { |
| $percent = 100. * $st{"branch_reached"} / $st{"branch_count"}; |
| |
| printf OUT " <td>%.1f%%</td>\n", $percent; |
| } else { |
| print OUT " <td>N. A.</td>\n"; |
| } |
| } |
| if ($stats{"branch_count"}) { |
| $percent = 100. * $stats{"branch_reached"} / $stats{"branch_count"}; |
| |
| printf OUT " <td>%.1f%%</td>\n", $percent; |
| } else { |
| print OUT " <td>N. A.</td>\n"; |
| } |
| print OUT " <td>" . $stats{"branch_count"} . "</td>\n"; |
| print OUT " </tr><tr>\n"; |
| |
| print OUT " <td><b>Lines</b></td>\n"; |
| foreach my $f (@info_files) { |
| my %st = %{$report{$f}{"stats"}}; |
| |
| if ($st{"line_count"}) { |
| $percent = 100. * $st{"line_reached"} / $st{"line_count"}; |
| |
| printf OUT " <td>%.1f%%</td>\n", $percent; |
| } else { |
| print OUT " <td>N. A.</td>\n"; |
| } |
| } |
| if ($stats{"line_count"}) { |
| $percent = 100. * $stats{"line_reached"} / $stats{"line_count"}; |
| |
| printf OUT " <td>%.1f%%</td>\n", $percent; |
| } else { |
| print OUT " <td>N. A.</td>\n"; |
| } |
| print OUT " <td>" . $stats{"line_count"} . "</td>\n"; |
| |
| # If there are more than one tests per file, report them |
| my $total = scalar(keys %test_names); |
| if ($total > 1) { |
| print OUT " </tr><tr>\n"; |
| print OUT " <td><b>Number of tests</b></td>\n"; |
| foreach my $f (@info_files) { |
| my $count = scalar(keys %{$report{$f}{"test_names"}}); |
| |
| if ($count == 0) { |
| print OUT " <td $red>$count</td>\n"; |
| } elsif ($count < $total) { |
| print OUT " <td $yellow>$count</td>\n"; |
| } else { |
| print OUT " <td $green>$count</td>\n"; |
| } |
| } |
| print OUT " <td $green\>$total</td>\n"; |
| |
| } |
| print OUT " </tr>\n</table><p/>\n\n"; |
| |
| # Print function diff |
| print OUT " <h2>Differences on function coverage</h2>\n"; |
| print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n <tr>\n"; |
| print OUT " <th></th>\n"; |
| print OUT " <th>Common to all</th>\n"; |
| foreach my $f (@info_files) { |
| print OUT " <th>$f</th>\n"; |
| } |
| print OUT " <th>TOTAL</th>\n"; |
| |
| print OUT " </tr><tr>\n"; |
| print OUT " <td>#Functions per category</td>\n"; |
| |
| printf OUT " <td>%d</td>\n", $common_func_count; |
| |
| foreach my $f (@info_files) { |
| my %st = %{$report{$f}{"stats"}}; |
| if ($st{"func_count"}) { |
| printf OUT " <td>%d</td>\n", $st{"func_used"}; |
| |
| } |
| } |
| print OUT " <td>" . $stats{"func_count"} . "</td>\n"; |
| |
| print OUT " </tr><tr>\n"; |
| print OUT " <td>#functions not in common</td>\n"; |
| print OUT " <td></td>\n"; |
| foreach my $f (@info_files) { |
| my %st = %{$report{$f}{"stats"}}; |
| if ($st{"func_count"}) { |
| printf OUT " <td>%d</td>\n", $report{$f}{"more_func"}; |
| |
| } |
| } |
| print OUT " <td></td>\n"; |
| |
| print OUT " </tr><tr>\n"; |
| print OUT " <td>Unique functions</td>\n"; |
| print OUT " <td></td>\n"; |
| foreach my $f (@info_files) { |
| my %st = %{$report{$f}{"stats"}}; |
| if ($st{"func_count"}) { |
| printf OUT " <td>%d</td>\n", $report{$f}{"uniq_func"}; |
| |
| } |
| } |
| print OUT " <td></td>\n"; |
| |
| print OUT " </tr>\n</table><p/>\n\n"; |
| |
| # Print the filters applied when generating the report |
| |
| if ($filter_str ne "") { |
| printf OUT "<p>Filters: %s.</p>\n", $filter_str; |
| } else { |
| print OUT "<p>(unfiltered results)</p>"; |
| } |
| |
| if ($total > 1) { |
| print OUT "<h2>Tests coverage</h2>\n"; |
| |
| print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n <tr>\n"; |
| print OUT " <th>Test name</th>\n"; |
| foreach my $f (@info_files) { |
| print OUT " <th>$f</th>\n"; |
| } |
| |
| foreach my $t (sort keys(%test_names)) { |
| print OUT " </tr><tr>\n"; |
| printf OUT " <td>%s</td>\n", $t; |
| foreach my $f (@info_files) { |
| if (%{$report{$f}{"test_names"}}{$t}) { |
| print OUT " <td $green>YES</td>\n"; |
| } else { |
| print OUT " <td $red>NO</td>\n"; |
| } |
| } |
| } |
| print OUT "</tr></table>\n"; |
| } |
| |
| |
| # Generates a table containing per-function detailed data |
| |
| print OUT "<h2>Functions coverage</h2>\n"; |
| print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n <tr>\n"; |
| print OUT " <th>Function</th>\n"; |
| print OUT " <th>Used?</th>\n"; |
| foreach my $f (@info_files) { |
| print OUT " <th>$f</th>\n"; |
| } |
| print OUT " <th>File</th>\n"; |
| |
| foreach my $func (sort keys(%all_func)) { |
| my @keys = sort keys(%{$all_func{$func}}); |
| foreach my $file (@keys) { |
| print OUT " </tr><tr>\n"; |
| print OUT " <td>$func</td>\n"; |
| if ($used_func{$func}->{$file}) { |
| print OUT " <td $green>YES</td>\n"; |
| } else { |
| print OUT " <td $red>NO</td>\n"; |
| } |
| foreach my $f (@info_files) { |
| if ($report{$f}{"used_func"}{$func}->{$file}) { |
| print OUT " <td $green>YES</td>\n"; |
| } else { |
| print OUT " <td $red>NO</td>\n"; |
| } |
| } |
| $file =~ s,$prefix,linux/,; |
| print OUT " <td>$file</td>\n"; |
| } |
| } |
| print OUT "</tr></table>\n"; |
| |
| print OUT "$epilog</body>\n"; |
| |
| # Close the file and exit |
| |
| close OUT; |
| } |
| |
| # |
| # Argument handling |
| # |
| |
| my $print_used; |
| my $print_unused; |
| my $stat; |
| my $filter; |
| my $help; |
| my $man; |
| my $func_filters; |
| my $src_filters; |
| my $show_files; |
| my $show_lines; |
| my $only_i915; |
| my $only_drm; |
| |
| GetOptions( |
| "print-coverage|print_coverage|print|p" => \$print_used, |
| "print-unused|u" => \$print_unused, |
| "stat|statistics" => \$stat, |
| "output|o=s" => \$filter, |
| "verbose|v" => \$verbose, |
| "ignore-unused|ignore_unused" => \$ignore_unused, |
| "only-i915|only_i915" => \$only_i915, |
| "only-drm|only_drm" => \$only_drm, |
| "func-filters|f=s" => \$func_filters, |
| "include-func=s" => \@func_regexes, |
| "exclude-func=s" => \@func_exclude_regexes, |
| "source-filters|S=s" => \$src_filters, |
| "include-source=s" => \@src_regexes, |
| "exclude-source=s" => \@src_exclude_regexes, |
| "show-files|show_files" => \$show_files, |
| "show-lines|show_lines" => \$show_lines, |
| "report|r=s" => \$gen_report, |
| "css-file|css|c=s" => \$css_file, |
| "title|t=s" => \$title, |
| "html-prolog|prolog=s" => \$html_prolog, |
| "html-epilog|epilog=s" => \$html_epilog, |
| "help" => \$help, |
| "man" => \$man, |
| ) or pod2usage(2); |
| |
| pod2usage(-verbose => 2) if $man; |
| pod2usage(1) if $help; |
| |
| if ($#ARGV < 0) { |
| print "$0: no input files\n"; |
| pod2usage(1); |
| } |
| |
| # At least one action should be specified |
| pod2usage(1) if (!$print_used && !$filter && !$stat && !$print_unused && !$gen_report); |
| |
| pod2usage(1) if ($gen_report && ($print_used || $filter || $stat || $print_unused)); |
| |
| my $filter_str = ""; |
| my $has_filter; |
| my $str; |
| |
| if ($only_i915) { |
| # Please keep in sync with the documentation |
| push @src_exclude_regexes, "selftest"; |
| push @src_regexes, "drm/i915"; |
| push @src_regexes, "drm/ttm"; |
| push @src_regexes, "drm/vgem"; |
| } |
| |
| if ($only_drm) { |
| # Please keep in sync with the documentation |
| push @src_exclude_regexes, "trace.*\.h\$"; |
| push @src_exclude_regexes, "^/drm/"; |
| } |
| |
| $str = open_filter_file($func_filters, \@func_regexes, \@func_exclude_regexes); |
| if ($str) { |
| $filter_str .= "," if ($filter_str ne ""); |
| $filter_str .= " function regex ($str)"; |
| $has_filter = 1; |
| } |
| |
| $str = open_filter_file($src_filters, \@src_regexes, \@src_exclude_regexes); |
| if ($str) { |
| $filter_str .= "," if ($filter_str ne ""); |
| $filter_str .= " source regex ($str)"; |
| $has_filter = 1; |
| } |
| |
| $ignore_unused = 1 if (@func_regexes || @func_exclude_regexes); |
| |
| if ($ignore_unused) { |
| $filter_str .= "," if ($filter_str ne ""); |
| $filter_str .= " ignored source files where none of its code ran"; |
| $has_filter = 1; |
| } |
| |
| foreach my $f (@ARGV) { |
| parse_info_data($f); |
| |
| if ($gen_report) { |
| $f =~ s,.*/,,; |
| $f =~ s/\.info$//; |
| |
| gen_stats(); |
| |
| $report{$f}{"stats"} = { %stats }; |
| $report{$f}{"all_func"} = { %all_func }; |
| $report{$f}{"used_func"} = { %used_func }; |
| $report{$f}{"all_branch"} = { %all_branch }; |
| $report{$f}{"all_line"} = { %all_line }; |
| $report{$f}{"used_source"} = { %used_source }; |
| $report{$f}{"files"} = { %files }; |
| $report{$f}{"test_names"} = { %test_names }; |
| |
| %used_func = (); |
| %all_func = (); |
| %all_branch = (); |
| %all_line = (); |
| %used_source = (); |
| %files = (); |
| %test_names = (); |
| } |
| } |
| |
| $filter_str =~ s/(.*),/$1 and/ if ($filter_str ne ""); |
| |
| if ($gen_report) { |
| generate_report($filter_str); |
| exit 0; |
| } |
| |
| gen_stats(); |
| |
| die "Nothing counted. Wrong input files?" if (!$stats{"all_files"}); |
| |
| print_code_coverage($print_used, $print_unused, $show_lines); |
| |
| print_summary() if ($stat); |
| |
| if ($has_filter) { |
| my $percent = 100. * $stats{"used_files"} / $stats{"all_files"}; |
| |
| printf "Filters......:%s.\n", $filter_str; |
| printf "Source files.: %.2f%% (%d of %d total)", |
| $percent, $stats{"used_files"}, $stats{"all_files"}; |
| |
| if ($stats{"used_files"} != $stats{"filtered_files"}) { |
| my $percent_filtered = 100. * $stats{"used_files"} / $stats{"filtered_files"}; |
| |
| printf ", %.2f%% (%d of %d filtered)", |
| $percent_filtered, $stats{"used_files"}, $stats{"filtered_files"}; |
| } |
| print "\n"; |
| } else { |
| printf "Source files: %d\n", scalar keys(%files) if($stat); |
| } |
| |
| my $ntests=scalar(%test_names); |
| printf "Number of tests: %d\n", $ntests if ($ntests > 1); |
| |
| if ($show_files) { |
| for my $f(sort keys %used_source) { |
| print "\t$f\n"; |
| } |
| } |
| |
| if ($filter) { |
| write_filtered_file($filter); |
| } |
| |
| __END__ |
| |
| =head1 NAME |
| |
| Parses lcov data from .info files. |
| |
| =head1 SYNOPSIS |
| |
| code_cov_parse_info <options> [input file(s)] |
| |
| At least one of the output options should be used, e g. |
| B<--stat>, B<--print>, B<--print-unused>, B<--report> and/or B<--output>. |
| |
| Also, B<--report> can't be used together with other output options. |
| |
| =head1 OPTIONS |
| |
| =over 8 |
| |
| =item B<--stat> or B<--statistics> |
| |
| Prints code coverage statistics. |
| |
| It displays function, line, branch and file coverage percentage. |
| |
| It also reports when one or more of the filtering parameters are used. |
| |
| The statistics report is affected by the applied filters. |
| |
| =item B<--print-coverage> or B<--print_coverage> or B<--print> or B<-p> |
| |
| Prints the functions that were executed in runtime and how many times |
| they were reached. |
| |
| The function coverage report is affected by the applied filters. |
| |
| =item B<--print-unused> or B<-u> |
| |
| Prints the functions that were never reached. |
| |
| The function coverage report is affected by the applied filters. |
| |
| =item B<--report> B<[output file]> or B<-r> B<[output file]> |
| |
| Generates an html report containing per-test and total statistics. |
| |
| The function coverage report is affected by the applied filters. |
| |
| =item B<--css-file> B<[css file]> or B<--css> B<[css file]> or B<-c> B<[css file] |
| |
| Adds an optional css file to the html report. |
| Used only with B<--report>. |
| |
| =item B<--title> B<[title] or B<-t> B<[title] |
| |
| If used with B<--report>, it defines the title for the for the html report. |
| |
| If used with B<--output>, it replaces the test names with the title. This |
| is useful when merging reports from multiple tests into a summarized file. |
| If not used, the B<[output file]> will contain all test names on its |
| beginning. |
| |
| Used with B<--report> AND B<--output>. |
| |
| =item B<--html-prolog> B<[html file] or B<--prolog> B<[html file] |
| |
| Adds a prolog at the beginning of the body of the html report. |
| Used only with B<--report>. |
| |
| =item B<--html-epilog> B<[html file] or B<--epilog> B<[html file] |
| |
| Adds an epilog before the end of the body of the html report. |
| Used only with B<--report>. |
| |
| =item B<--show-lines> or B<--show_lines> |
| |
| When printing per-function code coverage data, always output the source |
| file and the line number where the function is defined. |
| |
| =item B<--output> B<[output file]> or B<-o> B<[output file]> |
| |
| Produce an output file merging all input files. |
| |
| The generated output file is affected by the applied filters. |
| |
| =item B<--only-drm> or B<--only_drm> |
| |
| Filters out includes outside the DRM subsystem, plus trace files. |
| E. g. it will exclude *.h files that match the following regular expressions: |
| |
| - .*trace.*\.h$ |
| |
| And *.h files that don't match: |
| |
| - /drm/ |
| |
| (e. g. macros and other code outside DRM subsystem) |
| |
| =item B<--only-i915> or B<--only_i915> |
| |
| Filters out C files and headers outside drm core and drm/i915. |
| |
| E. g. code coverage results will include only the files that that match |
| the following regular expressions: |
| |
| - drm/i915/ |
| - drm/ttm |
| - drm/vgem |
| |
| Excluding files that match: |
| |
| - selftest |
| |
| =item B<--func-filters> B<[filter's file]> or B<-f> B<[filter's file]> |
| |
| Use a file containing regular expressions (regex) to filter functions. |
| |
| Each line at B<[filter's file]> may contain a new regex: |
| |
| =over 4 |
| |
| - Blank lines and lines starting with B<#> will be ignored; |
| |
| - Each line of the file will be handled as a new regex; |
| |
| - If B<+regex> is used, the filter will include B<regex> to the matches; |
| |
| - If B<-regex> is used, the filter will exclude B<regex> from the matches; |
| |
| - If the line doesn't start with neither B<+> nor B<->, containing just |
| B<regex>, the filter will include B<regex> to the matches. |
| |
| - Any whitespace/tab before or after B<regex> will be ignored. |
| |
| =back |
| |
| When both include and exclude regexes are found, exclude regexes are |
| applied first and any functions that don't match the include regular |
| expressions from the B<[filter's file]> will be ignored. |
| |
| Please notice that, when this filter is used, B<--ignore-unused> will be |
| automaticaly enabled, as the final goal is to report per-function usage. |
| |
| =item B<--include-func> B<regex> |
| |
| Include B<regex> to the function filter. Can be used multiple times. |
| |
| When used together with B<--func-filters>, regexes here are handled first. |
| |
| Please notice that, when this filter is used, B<--ignore-unused> will be |
| automaticaly enabled, as the final goal is to report per-function usage. |
| |
| =item B<--exclude-func> B<regex> |
| |
| Include B<regex> to the function filter. Can be used multiple times. |
| |
| When used together with B<--func-filters>, regexes here are handled first. |
| |
| Please notice that, when this filter is used, B<--ignore-unused> will be |
| automaticaly enabled, as the final goal is to report per-function usage. |
| |
| =item B<--source-filters> B<[filter's file]> or B<-S> B<[filter's file]> |
| |
| Use a file containing regular expressions to filter source files. |
| |
| Each line of the file will be handled as a new regular expressions. |
| Blank lines and lines starting with B<#> will be ignored. |
| |
| Each line at B<[filter's file]> may contain a new regex: |
| |
| =over 4 |
| |
| - Blank lines and lines starting with B<#> will be ignored; |
| |
| - Each line of the file will be handled as a new regex; |
| |
| - If B<+regex> is used, the filter will include B<regex> to the matches; |
| |
| - If B<-regex> is used, the filter will exclude B<regex> from the matches; |
| |
| - If the line doesn't start with neither B<+> nor B<->, containing just |
| B<regex>, the filter will include B<regex> to the matches. |
| |
| - Any whitespace/tab before or after B<regex> will be ignored. |
| |
| =back |
| |
| When both include and exclude regexes are found, exclude regexes are |
| applied first and any functions that don't match the include regular |
| expressions from the B<[filter's file]> will be ignored. |
| |
| =item B<--include-src> B<regex> |
| |
| Include B<regex> to the sources filter. Can be used multiple times. |
| |
| When used together with B<--src-filters>, regexes here are handled first. |
| |
| =item B<--exclude-src> B<regex> |
| |
| Include B<regex> to the sources filter. Can be used multiple times. |
| |
| When used together with B<--src-filters>, regexes here are handled first. |
| |
| =item B<--ignore-unused> or B<--ignore_unused> |
| |
| Filters out unused C files and headers from the code coverage results. |
| |
| Sometimes, it is desired to ignore files where none of the functions on it |
| were tested. |
| |
| The rationale is that such files may contain platform-specific drivers |
| and code that will never be used, so, placing them will just bloat the |
| report and decrease the code coverage statistics. |
| |
| This option is automaticaly enabled when B<--func-filters> is used. |
| |
| =back |
| |
| =item B<--show-files> or B<--show_files> |
| |
| Shows the list of files that were used to produce the code coverage |
| results. |
| |
| =item B<--verbose> or B<-v> |
| |
| Prints the name of each parsed file. |
| |
| =item B<--help> |
| |
| Print a brief help message and exits. |
| |
| =item B<--man> |
| |
| Prints the manual page and exits. |
| |
| =back |
| |
| =head1 BUGS |
| |
| Report bugs to Mauro Carvalho Chehab <mauro.chehab@intel.com> |
| |
| =head1 COPYRIGHT |
| |
| Copyright (c) 2022 Intel Corporation |
| |
| Permission is hereby granted, free of charge, to any person obtaining a |
| copy of this software and associated documentation files (the "Software"), |
| to deal in the Software without restriction, including without limitation |
| the rights to use, copy, modify, merge, publish, distribute, sublicense, |
| and/or sell copies of the Software, and to permit persons to whom the |
| Software is furnished to do so, subject to the following conditions: |
| |
| The above copyright notice and this permission notice (including the next |
| paragraph) shall be included in all copies or substantial portions of the |
| Software. |
| |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
| THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| IN THE SOFTWARE. |
| =cut |