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();
54 "cached" => { "optvar"=>\$opt_cached, "desc"=>"Use index" },
55 "output:s" => { "optvar"=>\$opt_output, "desc"=>"Generate html output"},
56 "help" => { "optvar"=>\$opt_help, "desc"=>""},
57 "quiet" => { "optvar"=>\$opt_quiet, "desc"=>""},
58 "verbose" => { "optvar"=>\$opt_verbose, "desc"=>"" });
60 my %longOptions = map { $_ => $options{$_}->{"optvar"} } keys(%options);
61 GetOptions( %longOptions ) or pod2usage(2);
62 pod2usage(1) if $opt_help;
65 ## Format per file, repeated, no linebreak
68 # --- a/<left-hand-side-file>
69 # +++ b/<right-hand-side-file>
72 # Format of each diff hunk, repeated, no linebreak
75 # [-|+]lines removed on left, added on right
89 print "Patch size: ".scalar(@$patchref)."\n" if $pd_debug;
90 for my $line (@$patchref)
94 print "State: $state $line \n" if $pd_debug;
95 # Search for a line matching "+++ b/<filename>"
96 if( $line =~ m!^\+\+\+ b/([\w-_\./]*)!)
100 print "Found File: $file\n" if $pd_debug;
103 else #elsif($state == 1)
105 # If we find a line starting with diff, the previous
106 # file's diffs have finished, store them.
107 if( $line =~ /^diff/)
109 print "State: $state $line \n" if $pd_debug;
111 # if the file had changes, store the new/modified line numbers
112 if( $file && scalar(@checklines))
114 $files{$file}->{"patch"} = [@checklines];
115 $files{$file}->{"b_lines"} = {%b_lines};
119 print("\n\n") if $pd_debug;
121 # If we find a line starting with @@, it tells us the line numbers
122 # of the old file and new file for this hunk.
123 elsif( $line =~ /^@@/)
125 print "State: $state $line \n" if $pd_debug;
127 # Find the lines in the new file (of the form "+<start>[,<length>])
128 my($start,$space,$length) = ($line =~ /\+([0-9]+)(,| )([0-9]+)?/);
129 if($length || $space eq " ")
135 push(@checklines, [$start, $length]);
144 my $last = scalar(@checklines)-1;
147 print "Checkline:" . $checklines[$last]->[0] . ", " . $checklines[$last]->[1] . "\n";
151 # If we find a line starting with "+", it belongs to the new file's patch
152 elsif( $line =~ /^\+/)
157 $line = substr($line, 1); # Remove leading +
158 $b_lines{$store_line} = $line;
164 # Store the final entry
165 $files{$file}->{"patch"} = [@checklines];
166 $files{$file}->{"b_lines"} = {%b_lines};
168 my %filter = map { $_ => $files{$_} } grep {m!^dali(-toolkit)?/!} (keys(%files));;
172 print("Filtered files:\n");
173 foreach my $file (keys(%filter))
176 $patchref = $filter{$file}->{"patch"};
177 foreach my $lineblock (@$patchref)
179 print "$lineblock->[0]($lineblock->[1]) "
190 my $filesref = shift;
191 print "\nNumber of files: " . scalar(keys(%$filesref)) . "\n";
192 for my $file (keys(%$filesref))
195 my $clref = $filesref->{$file}->{"patch"};
198 print("($cl->[0],$cl->[1]) ");
206 # Assumes test cases have been run, and "make rename_cov_data" has been executed
209 my ($name, $path, $suffix) = fileparse($file, (".c", ".cpp", ".h"));
210 my $gcno_file = $repo->wc_path() . "/build/tizen/.cov/$name.gcno";
212 # Note, will translate headers to their source's object, which
213 # may miss execution code in the headers (e.g. inlines are usually
214 # not all used in the implementation, and require getting coverage
219 my $gcno_st = stat($gcno_file);
220 my $fq_file = $repo->wc_path() . $file;
221 my $src_st = stat($fq_file);
222 if($gcno_st->ctime < $src_st->mtime)
224 print "WARNING: GCNO $gcno_file older than SRC $fq_file\n";
231 print("WARNING: No equivalent gcno file for $file\n");
240 my $filesref = shift;
241 print("get_coverage($file)\n") if $debug;
243 my $gcno_file = get_gcno_file($file);
248 print "Running gcov on $gcno_file:\n" if $debug;
249 open( my $fh, "gcov --preserve-paths $gcno_file |") || die "Can't run gcov:$!\n";
252 print $_ if $debug>=3;
254 if( m!'(.*\.gcov)'$! )
256 my $coverage_file = $1; # File has / replaced with # and .. replaced with ^
257 my $source_file = $coverage_file;
258 $source_file =~ s!\^!..!g; # Change ^ to ..
259 $source_file =~ s!\#!/!g; # change #'s to /s
260 $source_file =~ s!.gcov$!!; # Strip off .gcov suffix
262 print "Matching $file against $source_file\n" if $debug >= 3;
263 # Only want the coverage files matching source file:
264 if(index( $source_file, $file ) > 0 )
266 $gcovfile = $coverage_file;
275 if($gcovfiles{$gcovfile} == undef)
277 # Only parse a gcov file once
278 $gcovfiles{$gcovfile}->{"seen"}=1;
280 print "Getting coverage data from $gcovfile\n" if $debug;
282 open( FH, "< $gcovfile" ) || die "Can't open $gcovfile for reading:$!\n";
285 my ($cov, $line, @code ) = split( /:/, $_ );
286 $cov =~ s/^\s+//; # Strip leading space
288 my $code=join(":", @code);
291 # There is no coverage data for these executable lines
292 $gcovfiles{$gcovfile}->{"uncovered"}->{$line}++;
293 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
295 elsif( $cov ne "-" && looks_like_number($cov) && $cov > 0 )
297 $gcovfiles{$gcovfile}->{"covered"}->{$line}=$cov;
298 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
302 # All other lines are not executable.
303 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
308 $filesref->{$file}->{"coverage"} = $gcovfiles{$gcovfile}; # store hashref
312 # No gcov output - the gcno file produced no coverage of the source/header
313 # Probably means that there is no coverage for the file (with the given
314 # test case - there may be some somewhere, but for the sake of speed, don't
320 # Run the git diff command to get the patch, then check the coverage
321 # output for the patch.
324 #print "run_diff(" . join(" ", @_) . ")\n";
325 my ($fh, $c) = $repo->command_output_pipe(@_);
332 $repo->command_close_pipe($fh, $c);
334 print "Patch size: " . scalar(@patch) . "\n" if $debug;
336 # @patch has slurped diff for all files...
337 my $filesref = parse_diff ( \@patch );
338 show_patch_lines($filesref) if $debug;
340 print "Checking coverage:\n" if $debug;
343 chdir ".cov" || die "Can't find $cwd/.cov:$!\n";
345 for my $file (keys(%$filesref))
347 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
348 next if($path !~ /^dali/);
349 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
351 get_coverage($file, $filesref);
358 sub calc_patch_coverage_percentage
360 my $filesref = shift;
361 my $total_covered_lines = 0;
362 my $total_uncovered_lines = 0;
364 foreach my $file (keys(%$filesref))
366 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
367 next if($path !~ /^dali/);
369 my $covered_lines = 0;
370 my $uncovered_lines = 0;
372 my $patchref = $filesref->{$file}->{"patch"};
373 my $coverage_ref = $filesref->{$file}->{"coverage"};
376 for my $patch (@$patchref)
378 for(my $i = 0; $i < $patch->[1]; $i++ )
380 my $line = $i + $patch->[0];
381 if($coverage_ref->{"covered"}->{$line})
384 $total_covered_lines++;
386 if($coverage_ref->{"uncovered"}->{$line})
389 $total_uncovered_lines++;
393 $coverage_ref->{"covered_lines"} = $covered_lines;
394 $coverage_ref->{"uncovered_lines"} = $uncovered_lines;
395 my $total = $covered_lines + $uncovered_lines;
399 $percent = $covered_lines / $total;
401 $coverage_ref->{"percent_covered"} = 100 * $percent;
404 my $total_exec = $total_covered_lines + $total_uncovered_lines;
406 if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
408 return [ $total_exec, $percent ];
413 my $filesref = shift;
414 foreach my $file (keys(%$filesref))
416 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
417 next if($path !~ /^dali/);
419 my $patchref = $filesref->{$file}->{"patch"};
420 my $b_lines_ref = $filesref->{$file}->{"b_lines"};
421 my $coverage_ref = $filesref->{$file}->{"coverage"};
422 print BOLD, "$file ";
426 if( $coverage_ref->{"covered_lines"} > 0
428 $coverage_ref->{"uncovered_lines"} > 0 )
430 print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET;
435 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
439 print "No coverage found";
443 for my $patch (@$patchref)
445 my $hunkstr="Hunk: " . $patch->[0];
446 if( $patch->[1] > 1 )
448 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
450 print BOLD, "$hunkstr\n", RESET;
451 for(my $i = 0; $i < $patch->[1]; $i++ )
453 my $line = $i + $patch->[0];
454 printf "%-6s ", $line;
459 if($coverage_ref->{"covered"}->{$line})
463 elsif($coverage_ref->{"uncovered"}->{$line})
471 my $src=$coverage_ref->{"src"}->{$line};
473 print $color, "$src\n", RESET;
477 # We don't have coverage data, so print it from the patch instead.
478 my $src = $b_lines_ref->{$line};
487 sub patch_html_output
489 my $filesref = shift;
491 open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n";
493 my $OUTPUT_FH = select;
496 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
497 "http://www.w3.org/TR/REC-html40/loose.dtd">
500 <title>Patch Coverage</title>
502 <body bgcolor="white">
505 foreach my $file (keys(%$filesref))
507 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
508 next if($path !~ /^dali/);
510 my $patchref = $filesref->{$file}->{"patch"};
511 my $b_lines_ref = $filesref->{$file}->{"b_lines"};
512 my $coverage_ref = $filesref->{$file}->{"coverage"};
513 print "<h2>$file</h2>\n";
517 if( $coverage_ref->{"covered_lines"} > 0
519 $coverage_ref->{"uncovered_lines"} > 0 )
521 print "<p style=\"color:green;\">Covered: " .
522 $coverage_ref->{"covered_lines"} . "<p>" .
523 "<p style=\"color:red;\">Uncovered: " .
524 $coverage_ref->{"uncovered_lines"} . "</span></p>";
531 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
533 print "<span style=\"color:red;\">";
536 print "No coverage found";
537 print "</span>" if $span;
541 for my $patch (@$patchref)
543 my $hunkstr="Hunk: " . $patch->[0];
544 if( $patch->[1] > 1 )
546 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
548 print "<p style=\"font-weight:bold;\">" . $hunkstr . "</p>";
551 for(my $i = 0; $i < $patch->[1]; $i++ )
553 my $line = $i + $patch->[0];
554 my $num_line_digits=log($line)/log(10);
555 for $i (0..(6-$num_line_digits-1))
564 if($coverage_ref->{"covered"}->{$line})
566 print("<span style=\"color:green;\">");
568 elsif($coverage_ref->{"uncovered"}->{$line})
570 print("<span style=\"color:red;font-weight:bold;\">");
574 #print("<span style=\"color:black;font-weight:normal;\">");
576 my $src=$coverage_ref->{"src"}->{$line};
578 #print $color, "$src\n", RESET;
579 print "$src</span>\n";
583 # We don't have coverage data, so print it from the patch instead.
584 my $src = $b_lines_ref->{$line};
592 print $filehandle "<hr>\n</body>\n</html>\n";
598 ################################################################################
600 ################################################################################
603 chdir $repo->wc_path();
605 `make rename_cov_data`;
607 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
609 my $status = $repo->command("status", "-s");
610 if( $status eq "" && !scalar(@ARGV))
612 # There are no changes in the index or working tree, and
613 # no diff arguments to append. Use the last patch instead.
614 push @cmd, ('HEAD~1','HEAD');
618 # detect if there are only cached changes or only working tree changes
621 for my $fstat ( split(/\n/, $status) )
623 if(substr( $fstat, 0, 1 ) ne " "){ $cached++; }
624 if(substr( $fstat, 1, 1 ) ne " "){ $working++; }
630 push @cmd, "--cached";
634 die "Both cached & working files - cannot get correct patch from git\n";
635 # Would have to diff from separate clone.
641 my $filesref = run_diff(@cmd);
645 # Check how many actual source files there are in the patch
647 foreach my $file (keys(%$filesref))
649 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
650 next if($path !~ /^dali/);
651 next if($suffix ne ".cpp" && $suffix ne ".c" && $suffix ne ".h");
654 if( $filecount == 0 )
656 print "No source files found\n";
657 exit 0; # Exit with no error.
660 my $percentref = calc_patch_coverage_percentage($filesref);
661 if($percentref->[0] == 0)
663 print "No coverable lines found\n";
666 my $percent = $percentref->[1];
671 print "Outputing to $opt_output\n" if $debug;
672 patch_html_output($filesref);
674 elsif( ! $opt_quiet )
676 patch_output($filesref);
684 printf("Percentage of change covered: %5.2f%\n", $percent);
697 patch-coverage.pl - Determine if patch coverage is below 90%
700 Calculates how well the most recent patch is covered (either the
701 patch that is in the index+working directory, or HEAD).
708 Use index files if there is nothing in the working tree
714 Don't generate any output
717 0 if the coverage of source files is > 90%, otherwise 1