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.
27 use Scalar::Util qw /looks_like_number/;
29 use Term::ANSIColor qw(:constants);
31 # Program to run gcov on files in patch (that are in source dirs - needs to be dali-aware).
34 # B) Remove uninteresting files
35 # C) Find matching gcno/gcda files
36 # D) Copy and rename them to match source prefix (i.e. strip library name off front)
37 # E) Generate patch output with covered/uncovered lines marked in green/red
38 # F) Generate coverage data for changed lines
39 # G) Exit status should be 0 for high coverage (90% line coverage for all new/changed lines)
40 # or 1 for low coverage
42 # Sources for conversion of gcno/gcda files:
44 # Python git-coverage (From http://stef.thewalter.net/git-coverage-useful-code-coverage.html)
46 our $repo = Git->repository();
56 "cached" => { "optvar"=>\$opt_cached, "desc"=>"Use index" },
57 "output:s" => { "optvar"=>\$opt_output, "desc"=>"Generate html output"},
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 #print "run_diff(" . join(" ", @_) . ")\n";
327 my ($fh, $c) = $repo->command_output_pipe(@_);
334 $repo->command_close_pipe($fh, $c);
336 print "Patch size: " . scalar(@patch) . "\n" if $debug;
338 # @patch has slurped diff for all files...
339 my $filesref = parse_diff ( \@patch );
340 show_patch_lines($filesref) if $debug;
342 print "Checking coverage:\n" if $debug;
345 chdir ".cov" || die "Can't find $cwd/.cov:$!\n";
347 for my $file (keys(%$filesref))
349 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
350 next if($path !~ /^dali/);
351 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
353 get_coverage($file, $filesref);
360 sub calc_patch_coverage_percentage
362 my $filesref = shift;
363 my $total_covered_lines = 0;
364 my $total_uncovered_lines = 0;
366 foreach my $file (keys(%$filesref))
368 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
369 next if($path !~ /^dali/);
371 my $covered_lines = 0;
372 my $uncovered_lines = 0;
374 my $patchref = $filesref->{$file}->{"patch"};
375 my $coverage_ref = $filesref->{$file}->{"coverage"};
378 for my $patch (@$patchref)
380 for(my $i = 0; $i < $patch->[1]; $i++ )
382 my $line = $i + $patch->[0];
383 if($coverage_ref->{"covered"}->{$line})
386 $total_covered_lines++;
388 if($coverage_ref->{"uncovered"}->{$line})
391 $total_uncovered_lines++;
395 $coverage_ref->{"covered_lines"} = $covered_lines;
396 $coverage_ref->{"uncovered_lines"} = $uncovered_lines;
397 my $total = $covered_lines + $uncovered_lines;
401 $percent = $covered_lines / $total;
403 $coverage_ref->{"percent_covered"} = 100 * $percent;
406 my $total_exec = $total_covered_lines + $total_uncovered_lines;
408 if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
410 return [ $total_exec, $percent ];
415 my $filesref = shift;
416 foreach my $file (keys(%$filesref))
418 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
419 next if($path !~ /^dali/);
421 my $patchref = $filesref->{$file}->{"patch"};
422 my $b_lines_ref = $filesref->{$file}->{"b_lines"};
423 my $coverage_ref = $filesref->{$file}->{"coverage"};
424 print BOLD, "$file ";
428 if( $coverage_ref->{"covered_lines"} > 0
430 $coverage_ref->{"uncovered_lines"} > 0 )
432 print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET;
437 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
441 print "No coverage found";
445 for my $patch (@$patchref)
447 my $hunkstr="Hunk: " . $patch->[0];
448 if( $patch->[1] > 1 )
450 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
452 print BOLD, "$hunkstr\n", RESET;
453 for(my $i = 0; $i < $patch->[1]; $i++ )
455 my $line = $i + $patch->[0];
456 printf "%-6s ", $line;
461 if($coverage_ref->{"covered"}->{$line})
465 elsif($coverage_ref->{"uncovered"}->{$line})
473 my $src=$coverage_ref->{"src"}->{$line};
475 print $color, "$src\n", RESET;
479 # We don't have coverage data, so print it from the patch instead.
480 my $src = $b_lines_ref->{$line};
489 sub patch_html_output
491 my $filesref = shift;
493 my $html = HTML::Element->new('html');
494 my $head = HTML::Element->new('head');
495 my $title = HTML::Element->new('title');
496 $title->push_content("Patch Coverage");
497 $head->push_content($title, "\n");
498 $html->push_content($head, "\n");
500 my $body = HTML::Element->new('body');
501 $body->attr('bgcolor', "white");
503 foreach my $file (sort(keys(%$filesref)))
505 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
506 next if($path !~ /^dali/);
508 my $patchref = $filesref->{$file}->{"patch"};
509 my $b_lines_ref = $filesref->{$file}->{"b_lines"};
510 my $coverage_ref = $filesref->{$file}->{"coverage"};
512 my $header = HTML::Element->new('h2');
513 $header->push_content($file);
514 $body->push_content($header);
515 $body->push_content("\n");
518 if( $coverage_ref->{"covered_lines"} > 0
520 $coverage_ref->{"uncovered_lines"} > 0 )
522 my $para = HTML::Element->new('p');
523 my $covered = HTML::Element->new('span');
524 $covered->attr('style', "color:green;");
525 $covered->push_content("Covered: " . $coverage_ref->{"covered_lines"} );
526 $para->push_content($covered);
528 my $para2 = HTML::Element->new('p');
529 my $uncovered = HTML::Element->new('span');
530 $uncovered->attr('style', "color:red;");
531 $uncovered->push_content("Uncovered: " . $coverage_ref->{"uncovered_lines"} );
532 $para2->push_content($uncovered);
533 $body->push_content($para, $para2);
537 #print "coverage ref exists for $file:\n" . Data::Dumper::Dumper($coverage_ref) . "\n";
542 my $para = HTML::Element->new('p');
543 my $span = HTML::Element->new('span');
544 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
546 $span->attr('style', "color:red;");
548 $span->push_content("No coverage found");
549 $para->push_content($span);
550 $body->push_content($para);
553 for my $patch (@$patchref)
555 my $hunkstr="Hunk: " . $patch->[0];
556 if( $patch->[1] > 1 )
558 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
561 my $para = HTML::Element->new('p');
562 my $span = HTML::Element->new('span');
563 $span->attr('style', "font-weight:bold;");
564 $span->push_content($hunkstr);
565 $para->push_content($span);
566 $body->push_content($para);
568 my $codeHunk = HTML::Element->new('pre');
569 for(my $i = 0; $i < $patch->[1]; $i++ )
571 my $line = $i + $patch->[0];
572 my $num_line_digits=log($line)/log(10);
573 for $i (0..(6-$num_line_digits-1))
575 $codeHunk->push_content(" ");
578 $codeHunk->push_content("$line ");
580 my $srcLine = HTML::Element->new('span');
585 if($coverage_ref->{"covered"}->{$line})
587 $srcLine->attr('style', "color:green;");
589 elsif($coverage_ref->{"uncovered"}->{$line})
591 $srcLine->attr('style', "color:red;font-weight:bold;");
595 $srcLine->attr('style', "color:black;font-weight:normal;");
597 my $src=$coverage_ref->{"src"}->{$line};
599 $srcLine->push_content($src);
603 # We don't have coverage data, so print it from the patch instead.
604 my $src = $b_lines_ref->{$line};
605 $srcLine->attr('style', "color:black;font-weight:normal;");
606 $srcLine->push_content($src);
608 $codeHunk->push_content($srcLine, "\n");
610 $body->push_content($codeHunk, "\n");
613 $body->push_content(HTML::Element->new('hr'));
614 $html->push_content($body, "\n");
616 open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n";
618 print $filehandle <<EOH;
619 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
620 "http://www.w3.org/TR/REC-html40/loose.dtd">
623 print $filehandle $html->as_HTML();
628 ################################################################################
630 ################################################################################
633 chdir $repo->wc_path();
635 `make rename_cov_data`;
637 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
639 my $status = $repo->command("status", "-s");
640 if( $status eq "" && !scalar(@ARGV))
642 # There are no changes in the index or working tree, and
643 # no diff arguments to append. Use the last patch instead.
644 push @cmd, ('HEAD~1','HEAD');
648 # detect if there are only cached changes or only working tree changes
651 for my $fstat ( split(/\n/, $status) )
653 if(substr( $fstat, 0, 1 ) ne " "){ $cached++; }
654 if(substr( $fstat, 1, 1 ) ne " "){ $working++; }
660 push @cmd, "--cached";
664 die "Both cached & working files - cannot get correct patch from git\n";
665 # Would have to diff from separate clone.
671 my $filesref = run_diff(@cmd);
675 # Check how many actual source files there are in the patch
677 foreach my $file (keys(%$filesref))
679 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
680 next if($path !~ /^dali/);
681 next if($suffix ne ".cpp" && $suffix ne ".c" && $suffix ne ".h");
684 if( $filecount == 0 )
686 print "No source files found\n";
687 exit 0; # Exit with no error.
690 my $percentref = calc_patch_coverage_percentage($filesref);
691 if($percentref->[0] == 0)
693 print "No coverable lines found\n";
696 my $percent = $percentref->[1];
701 print "Outputing to $opt_output\n" if $debug;
702 patch_html_output($filesref);
704 elsif( ! $opt_quiet )
706 patch_output($filesref);
714 printf("Percentage of change covered: %5.2f%\n", $percent);
727 patch-coverage.pl - Determine if patch coverage is below 90%
730 Calculates how well the most recent patch is covered (either the
731 patch that is in the index+working directory, or HEAD).
738 Use index files if there is nothing in the working tree
744 Don't generate any output
747 0 if the coverage of source files is > 90%, otherwise 1