#!/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 %used_func; my %all_func; my %all_branch; my %all_line; my %used_source; my %record; my %files; my @func_regexes; my %test_names; my @src_regexes; my $verbose = 0; my $ignore_unused = 0; my $only_i915 = 0; my $only_drm = 0; my $skip_func = 0; sub is_function_excluded($) { return 0 if (!@func_regexes); my $func = shift; foreach my $r (@func_regexes) { return 0 if ($func =~ m/$r/); } return 1; } sub filter_file($) { my $s = shift; if ($only_drm) { # Please keep --only-drm doc updated with any changes her if ($s =~ m/\.h$/) { if ($s =~ m/trace/ || !($s =~ m/drm/)) { return 1; } } } if ($only_i915) { # Please keep --only-i915 doc updated with any changes here if ($s =~ m/selftest/) { return 1; } # Please keep --only-i915 doc updated with any changes here if (!($s =~ m#drm/i915/# || $s =~ m#drm/ttm# || $s =~ m#drm/vgem#)) { return 1; } } return 0 if (!@src_regexes); my $func = shift; 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 () { # TN: if (m/^TN:(.*)/) { if ($1 ne $cur_test) { $cur_test = $1; $test_names{$cur_test} = 1; } $source = $before_sf; $func = $before_sf; next; } # SF: 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:, if (m/^FN:(-?\d+),(.*)/) { 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:, if (m/^FNDA:(-?\d+),(.*)/) { 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: if (m/^FNF:(-?\d+)/) { $record{$source}{$func}{fnf} = $1; next; } # FNH: if (m/^FNH:(-?\d+)/) { my $hits = $1; if ($record{$source}{$func}{fnh} < $hits) { $record{$source}{$func}{fnh} = $hits; } next; } # Branch coverage # BRDA:,,, 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: if (m/^BRF:(-?\d+)/) { $record{$source}{brf} = $1; next; } # BRH: if (m/^BRH:(-?\d+)/) { my $hits = $1; if ($record{$source}{$func}{brh} < $hits) { $record{$source}{$func}{brh} = $hits; } next; } # Line coverage # DA:,[,] 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: if (m/^LF:(-?\d+)/) { $record{$source}{$func}{lf} = $1; next; } # LH: if (m/^LH:(-?\d+)/) { my $hits = $1; if ($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 = ""; foreach my $testname(sort keys %test_names) { $filtered .= "TN:$testname\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"; } } } } sub print_summary() { # Output per-line coverage statistics my $line_count = 0; my $line_reached = 0; foreach my $source (keys(%all_line)) { next if (!$used_source{$source}); foreach my $where (keys(%{$all_line{$source}})) { $line_count++; $line_reached++ if ($all_line{$source}{$where} != 0); } } if ($line_count) { my $percent = 100. * $line_reached / $line_count; printf " lines......: %.1f%% (%d of %d lines)\n", $percent, $line_reached, $line_count; } else { print "No line coverage data.\n"; } # Output per-function coverage statistics my $func_count = 0; my $func_used = 0; foreach my $func (keys(%all_func)) { foreach my $file (keys(%{$all_func{$func}})) { $func_count++; if ($used_func{$func}) { if ($used_func{$func}->{$file}) { $func_used++; } } } } if ($func_count) { my $percent = 100. * $func_used / $func_count; printf " functions..: %.1f%% (%d of %d functions)\n", $percent, $func_used, $func_count; } else { print "No functions reported. Wrong filters?\n"; return; } # Output per-branch coverage statistics my $branch_count = 0; my $branch_reached = 0; foreach my $source (keys(%all_branch)) { next if (!$used_source{$source}); foreach my $where (keys(%{$all_branch{$source}})) { $branch_count++; $branch_reached++ if ($all_branch{$source}{$where} != 0); } } if ($branch_count) { my $percent = 100. * $branch_reached / $branch_count; printf " branches...: %.1f%% (%d of %d branches)\n", $percent, $branch_reached, $branch_count; } else { print "No branch coverage data.\n"; } } # # 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; 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, "source-filters|S=s" => \$src_filters, "show-files|show_files" => \$show_files, "show-lines|show_lines" => \$show_lines, "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); my $filter_str = ""; my $has_filter; if ($func_filters) { open IN, $func_filters or die "Can't open $func_filters"; while () { s/^\s+//; s/\s+$//; next if (m/^#/ || m/^$/); push @func_regexes, qr /$_/; } close IN; } if ($src_filters) { open IN, $src_filters or die "Can't open $src_filters"; while () { s/^\s+//; s/\s+$//; next if (m/^#/ || m/^$/); push @src_regexes, qr /$_/; } close IN; } $ignore_unused = 1 if (@func_regexes); if ($only_i915) { $filter_str = " non-i915 files"; $has_filter = 1; } if ($only_drm) { $filter_str .= "," if ($filter_str ne ""); $filter_str .= " non-drm headers"; $has_filter = 1; } if (@func_regexes) { $filter_str .= "," if ($filter_str ne ""); $filter_str .= " unmatched functions"; foreach my $r (@func_regexes) { $filter_str .= " m/$r/"; } $has_filter = 1; } if (@src_regexes) { $filter_str .= "," if ($filter_str ne ""); $filter_str .= " unmatched source files"; foreach my $r (@src_regexes) { $filter_str .= " m/$r/"; } $has_filter = 1; } if ($ignore_unused) { $filter_str .= "," if ($filter_str ne ""); $filter_str .= " source files where none of its code ran"; $has_filter = 1; } foreach my $f (@ARGV) { parse_info_data($f); } print_code_coverage($print_used, $print_unused, $show_lines); print_summary() if ($stat); my $all_files = scalar keys(%files); die "Nothing counted. Wrong input files?" if (!$all_files); if ($has_filter) { my $all_files = scalar keys(%files); my $filtered_files = scalar keys(%record); my $used_files = scalar keys(%used_source); my $percent = 100. * $used_files / $all_files; $filter_str =~ s/(.*),/$1 and/; printf "Ignored......:%s.\n", $filter_str; printf "Source files.: %.2f%% (%d of %d total)", $percent, $used_files, $all_files; if ($used_files != $filtered_files) { my $percent_filtered = 100. * $used_files / $filtered_files; printf ", %.2f%% (%d of %d filtered)", $percent_filtered, $used_files, $filtered_files; } print "\n"; } else { printf "Source files: %d\n", scalar keys(%files) if($stat); } 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 [input file(s)] At least one of the options B<--stat>, B<--print> and/or B<--output> should be used. =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<--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 =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]> Take into account only the code coverage for the functions that match the regular expressions contained at the B<[filter's file]>. When this filter is used, B<--ignore-unused> will be automaticaly enabled, as the final goal is to report per-function usage, and not per-file. =item B<--source-filters> B<[filter's file]> or B<-S> B<[filter's file]> Takes into account only the code coverage for the source files that match the regular expressions contained at the B<[filter's file]>. =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 =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