3 # Copyright (c) 2016 Samsung Electronics Co., Ltd.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
25 use Scalar::Util qw /looks_like_number/;
27 use Term::ANSIColor qw(:constants);
29 # Program to run gcov on files in patch (that are in source dirs - needs to be dali-aware).
32 # B) Remove uninteresting files
33 # C) Find matching gcno/gcda files
34 # D) Copy and rename them to match source prefix (i.e. strip library name off front)
35 # E) Generate patch output with covered/uncovered lines marked in green/red
36 # F) Generate coverage data for changed lines
37 # G) Exit status should be 0 for high coverage (90% line coverage for all new/changed lines)
38 # or 1 for low coverage
40 # Sources for conversion of gcno/gcda files:
42 # Python git-coverage (From http://stef.thewalter.net/git-coverage-useful-code-coverage.html)
44 our $repo = Git->repository();
49 #our $opt_workingtree;
56 "cached" => { "optvar"=>\$opt_cached, "desc"=>"Use index" },
57 "head" => { "optvar"=>\$opt_head, "desc"=>"Use git show" },
58 "help" => { "optvar"=>\$opt_help, "desc"=>""},
59 "quiet" => { "optvar"=>\$opt_quiet, "desc"=>""},
60 "verbose" => { "optvar"=>\$opt_verbose, "desc"=>"" });
62 my %longOptions = map { $_ => $options{$_}->{"optvar"} } keys(%options);
63 GetOptions( %longOptions ) or pod2usage(2);
64 pod2usage(1) if $opt_help;
67 ## Format per file, repeated, no linebreak
70 # --- a/<left-hand-side-file>
71 # +++ b/<right-hand-side-file>
74 # Format of each diff hunk, repeated, no linebreak
77 # [-|+]lines removed on left, added on right
91 print "Patch size: ".scalar(@$patchref)."\n" if $pd_debug;
92 for my $line (@$patchref)
96 print "State: $state $line \n" if $pd_debug;
97 # Search for a line matching "+++ b/<filename>"
98 if( $line =~ m!^\+\+\+ b/([\w-_\./]*)!)
102 print "Found File: $file\n" if $pd_debug;
105 else #elsif($state == 1)
107 # If we find a line starting with diff, the previous
108 # file's diffs have finished, store them.
109 if( $line =~ /^diff/)
111 print "State: $state $line \n" if $pd_debug;
113 # if the file had changes, store the new/modified line numbers
114 if( $file && scalar(@checklines))
116 $files{$file}->{"patch"} = [@checklines];
117 $files{$file}->{"b_lines"} = {%b_lines};
121 print("\n\n") if $pd_debug;
123 # If we find a line starting with @@, it tells us the line numbers
124 # of the old file and new file for this hunk.
125 elsif( $line =~ /^@@/)
127 print "State: $state $line \n" if $pd_debug;
129 # Find the lines in the new file (of the form "+<start>[,<length>])
130 my($start,$space,$length) = ($line =~ /\+([0-9]+)(,| )([0-9]+)?/);
131 if($length || $space eq " ")
137 push(@checklines, [$start, $length]);
146 my $last = scalar(@checklines)-1;
149 print "Checkline:" . $checklines[$last]->[0] . ", " . $checklines[$last]->[1] . "\n";
153 # If we find a line starting with "+", it belongs to the new file's patch
154 elsif( $line =~ /^\+/)
159 $line = substr($line, 1); # Remove leading +
160 $b_lines{$store_line} = $line;
166 # Store the final entry
167 $files{$file}->{"patch"} = [@checklines];
168 $files{$file}->{"b_lines"} = {%b_lines};
170 my %filter = map { $_ => $files{$_} } grep {m!^dali(-toolkit)?/!} (keys(%files));;
174 print("Filtered files:\n");
175 foreach my $file (keys(%filter))
178 $patchref = $filter{$file}->{"patch"};
179 foreach my $lineblock (@$patchref)
181 print "$lineblock->[0]($lineblock->[1]) "
192 my $filesref = shift;
193 print "\nNumber of files: " . scalar(keys(%$filesref)) . "\n";
194 for my $file (keys(%$filesref))
197 my $clref = $filesref->{$file}->{"patch"};
200 print("($cl->[0],$cl->[1]) ");
208 # Assumes test cases have been run, and "make rename_cov_data" has been executed
211 my ($name, $path, $suffix) = fileparse($file, (".c", ".cpp", ".h"));
212 my $gcno_file = $repo->wc_path() . "/build/tizen/.cov/$name.gcno";
214 # Note, will translate headers to their source's object, which
215 # may miss execution code in the headers (e.g. inlines are usually
216 # not all used in the implementation, and require getting coverage
221 my $gcno_st = stat($gcno_file);
222 my $fq_file = $repo->wc_path() . $file;
223 my $src_st = stat($fq_file);
224 if($gcno_st->ctime < $src_st->mtime)
226 print "WARNING: GCNO $gcno_file older than SRC $fq_file\n";
233 print("WARNING: No equivalent gcno file for $file\n");
242 my $filesref = shift;
243 print("get_coverage($file)\n") if $debug;
245 my $gcno_file = get_gcno_file($file);
250 print "Running gcov on $gcno_file:\n" if $debug;
251 open( my $fh, "gcov --preserve-paths $gcno_file |") || die "Can't run gcov:$!\n";
254 print $_ if $debug>=3;
256 if( m!'(.*\.gcov)'$! )
258 my $coverage_file = $1; # File has / replaced with # and .. replaced with ^
259 my $source_file = $coverage_file;
260 $source_file =~ s!\^!..!g; # Change ^ to ..
261 $source_file =~ s!\#!/!g; # change #'s to /s
262 $source_file =~ s!.gcov$!!; # Strip off .gcov suffix
264 print "Matching $file against $source_file\n" if $debug >= 3;
265 # Only want the coverage files matching source file:
266 if(index( $source_file, $file ) > 0 )
268 $gcovfile = $coverage_file;
277 if($gcovfiles{$gcovfile} == undef)
279 # Only parse a gcov file once
280 $gcovfiles{$gcovfile}->{"seen"}=1;
282 print "Getting coverage data from $gcovfile\n" if $debug;
284 open( FH, "< $gcovfile" ) || die "Can't open $gcovfile for reading:$!\n";
287 my ($cov, $line, @code ) = split( /:/, $_ );
288 $cov =~ s/^\s+//; # Strip leading space
290 my $code=join(":", @code);
293 # There is no coverage data for these executable lines
294 $gcovfiles{$gcovfile}->{"uncovered"}->{$line}++;
295 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
297 elsif( $cov ne "-" && looks_like_number($cov) && $cov > 0 )
299 $gcovfiles{$gcovfile}->{"covered"}->{$line}=$cov;
300 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
304 # All other lines are not executable.
305 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
310 $filesref->{$file}->{"coverage"} = $gcovfiles{$gcovfile}; # store hashref
314 # No gcov output - the gcno file produced no coverage of the source/header
315 # Probably means that there is no coverage for the file (with the given
316 # test case - there may be some somewhere, but for the sake of speed, don't
322 # Run the git diff command to get the patch, then check the coverage
323 # output for the patch.
326 my ($fh, $c) = $repo->command_output_pipe(@_);
333 $repo->command_close_pipe($fh, $c);
335 # @patch has slurped diff for all files...
336 my $filesref = parse_diff ( \@patch );
337 show_patch_lines($filesref) if $debug;
339 print "Checking coverage:\n" if $debug;
342 chdir ".cov" || die "Can't find $cwd/.cov:$!\n";
344 for my $file (keys(%$filesref))
346 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
347 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
349 get_coverage($file, $filesref);
357 sub calc_patch_coverage_percentage
359 my $filesref = shift;
360 my $total_covered_lines = 0;
361 my $total_uncovered_lines = 0;
363 foreach my $file (keys(%$filesref))
365 my $covered_lines = 0;
366 my $uncovered_lines = 0;
368 my $patchref = $filesref->{$file}->{"patch"};
369 my $coverage_ref = $filesref->{$file}->{"coverage"};
372 for my $patch (@$patchref)
374 for(my $i = 0; $i < $patch->[1]; $i++ )
376 my $line = $i + $patch->[0];
377 if($coverage_ref->{"covered"}->{$line})
380 $total_covered_lines++;
382 if($coverage_ref->{"uncovered"}->{$line})
385 $total_uncovered_lines++;
389 $coverage_ref->{"covered_lines"} = $covered_lines;
390 $coverage_ref->{"uncovered_lines"} = $uncovered_lines;
391 my $total = $covered_lines + $uncovered_lines;
395 $percent = $covered_lines / $total;
397 $coverage_ref->{"percent_covered"} = 100 * $percent;
400 my $total_exec = $total_covered_lines + $total_uncovered_lines;
402 if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
409 my $filesref = shift;
410 foreach my $file (keys(%$filesref))
412 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
413 next if($path !~ /^dali/);
415 my $patchref = $filesref->{$file}->{"patch"};
416 my $b_lines_ref = $filesref->{$file}->{"b_lines"};
417 my $coverage_ref = $filesref->{$file}->{"coverage"};
418 print BOLD, "$file ";
422 if( $coverage_ref->{"covered_lines"} > 0
424 $coverage_ref->{"uncovered_lines"} > 0 )
426 print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET;
431 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
435 print "No coverage found";
439 for my $patch (@$patchref)
441 my $hunkstr="Hunk: " . $patch->[0];
442 if( $patch->[1] > 1 )
444 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
446 print BOLD, "$hunkstr\n", RESET;
447 for(my $i = 0; $i < $patch->[1]; $i++ )
449 my $line = $i + $patch->[0];
450 printf "%-6s ", $line;
455 if($coverage_ref->{"covered"}->{$line})
459 elsif($coverage_ref->{"uncovered"}->{$line})
467 my $src=$coverage_ref->{"src"}->{$line};
469 print $color, "$src\n", RESET;
473 # We don't have coverage data, so print it from the patch instead.
474 my $src = $b_lines_ref->{$line};
483 ################################################################################
485 ################################################################################
488 chdir $repo->wc_path();
490 `make rename_cov_data`;
492 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
494 my $status = $repo->command("status", "-s");
497 # There are no changes in the index or working tree. Use the last patch instead
498 push @cmd, ('HEAD~1','HEAD');
500 elsif($opt_cached) # TODO: Remove this option. Instead, need full diff
502 push @cmd, "--cached";
506 my $filesref = run_diff(@cmd);
508 my $percent = calc_patch_coverage_percentage($filesref);
511 patch_output($filesref);
517 printf("Percentage of change covered: $color %5.2f%\n" . RESET, $percent);
530 patch-coverage.pl - Determine if patch coverage is below 90%
533 Calculates how well the most recent patch is covered (either the
534 patch that is in the index+working directory, or HEAD).
541 Use index files if there is nothing in the working tree
547 Don't generate any output
550 0 if the coverage of source files is > 90%, otherwise 1