3 # Copyright (c) 2020 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;
269 # Some header files do not produce an equivalent gcov file so we shouldn't parse them
270 if(($source_file =~ /\.h$/) && (! -e $gcovfile))
272 print "Omitting Header: $source_file\n" if $debug;
283 if($gcovfiles{$gcovfile} == undef)
285 # Only parse a gcov file once
286 $gcovfiles{$gcovfile}->{"seen"}=1;
288 print "Getting coverage data from $gcovfile\n" if $debug;
290 open( FH, "< $gcovfile" ) || die "Can't open $gcovfile for reading:$!\n";
293 my ($cov, $line, @code ) = split( /:/, $_ );
294 $cov =~ s/^\s+//; # Strip leading space
296 my $code=join(":", @code);
299 # There is no coverage data for these executable lines
300 $gcovfiles{$gcovfile}->{"uncovered"}->{$line}++;
301 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
303 elsif( $cov ne "-" && looks_like_number($cov) && $cov > 0 )
305 $gcovfiles{$gcovfile}->{"covered"}->{$line}=$cov;
306 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
310 # All other lines are not executable.
311 $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
316 $filesref->{$file}->{"coverage"} = $gcovfiles{$gcovfile}; # store hashref
320 # No gcov output - the gcno file produced no coverage of the source/header
321 # Probably means that there is no coverage for the file (with the given
322 # test case - there may be some somewhere, but for the sake of speed, don't
328 # Run the git diff command to get the patch, then check the coverage
329 # output for the patch.
332 #print "run_diff(" . join(" ", @_) . ")\n";
333 my ($fh, $c) = $repo->command_output_pipe(@_);
340 $repo->command_close_pipe($fh, $c);
342 print "Patch size: " . scalar(@patch) . "\n" if $debug;
344 # @patch has slurped diff for all files...
345 my $filesref = parse_diff ( \@patch );
346 show_patch_lines($filesref) if $debug;
348 print "Checking coverage:\n" if $debug;
351 chdir ".cov" || die "Can't find $cwd/.cov:$!\n";
353 for my $file (keys(%$filesref))
355 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
356 next if($path !~ /^dali/);
357 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
359 get_coverage($file, $filesref);
366 sub calc_patch_coverage_percentage
368 my $filesref = shift;
369 my $total_covered_lines = 0;
370 my $total_uncovered_lines = 0;
372 foreach my $file (keys(%$filesref))
374 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
375 next if($path !~ /^dali/);
377 my $covered_lines = 0;
378 my $uncovered_lines = 0;
380 my $patchref = $filesref->{$file}->{"patch"};
381 my $coverage_ref = $filesref->{$file}->{"coverage"};
384 for my $patch (@$patchref)
386 for(my $i = 0; $i < $patch->[1]; $i++ )
388 my $line = $i + $patch->[0];
389 if($coverage_ref->{"covered"}->{$line})
392 $total_covered_lines++;
394 if($coverage_ref->{"uncovered"}->{$line})
397 $total_uncovered_lines++;
401 $coverage_ref->{"covered_lines"} = $covered_lines;
402 $coverage_ref->{"uncovered_lines"} = $uncovered_lines;
403 my $total = $covered_lines + $uncovered_lines;
407 $percent = $covered_lines / $total;
409 $coverage_ref->{"percent_covered"} = 100 * $percent;
412 my $total_exec = $total_covered_lines + $total_uncovered_lines;
414 if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
416 return [ $total_exec, $percent ];
421 my $filesref = shift;
422 foreach my $file (keys(%$filesref))
424 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
425 next if($path !~ /^dali/);
427 my $patchref = $filesref->{$file}->{"patch"};
428 my $b_lines_ref = $filesref->{$file}->{"b_lines"};
429 my $coverage_ref = $filesref->{$file}->{"coverage"};
430 print BOLD, "$file ";
434 if( $coverage_ref->{"covered_lines"} > 0
436 $coverage_ref->{"uncovered_lines"} > 0 )
438 print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET;
443 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
447 print "No coverage found";
451 for my $patch (@$patchref)
453 my $hunkstr="Hunk: " . $patch->[0];
454 if( $patch->[1] > 1 )
456 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
458 print BOLD, "$hunkstr\n", RESET;
459 for(my $i = 0; $i < $patch->[1]; $i++ )
461 my $line = $i + $patch->[0];
462 printf "%-6s ", $line;
467 if($coverage_ref->{"covered"}->{$line})
471 elsif($coverage_ref->{"uncovered"}->{$line})
479 my $src=$coverage_ref->{"src"}->{$line};
481 print $color, "$src\n", RESET;
485 # We don't have coverage data, so print it from the patch instead.
486 my $src = $b_lines_ref->{$line};
495 sub patch_html_output
497 my $filesref = shift;
499 my $html = HTML::Element->new('html');
500 my $head = HTML::Element->new('head');
501 my $title = HTML::Element->new('title');
502 $title->push_content("Patch Coverage");
503 $head->push_content($title, "\n");
504 $html->push_content($head, "\n");
506 my $body = HTML::Element->new('body');
507 $body->attr('bgcolor', "white");
509 foreach my $file (sort(keys(%$filesref)))
511 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
512 next if($path !~ /^dali/);
514 my $patchref = $filesref->{$file}->{"patch"};
515 my $b_lines_ref = $filesref->{$file}->{"b_lines"};
516 my $coverage_ref = $filesref->{$file}->{"coverage"};
518 my $header = HTML::Element->new('h2');
519 $header->push_content($file);
520 $body->push_content($header);
521 $body->push_content("\n");
524 if( $coverage_ref->{"covered_lines"} > 0
526 $coverage_ref->{"uncovered_lines"} > 0 )
528 my $para = HTML::Element->new('p');
529 my $covered = HTML::Element->new('span');
530 $covered->attr('style', "color:green;");
531 $covered->push_content("Covered: " . $coverage_ref->{"covered_lines"} );
532 $para->push_content($covered);
534 my $para2 = HTML::Element->new('p');
535 my $uncovered = HTML::Element->new('span');
536 $uncovered->attr('style', "color:red;");
537 $uncovered->push_content("Uncovered: " . $coverage_ref->{"uncovered_lines"} );
538 $para2->push_content($uncovered);
539 $body->push_content($para, $para2);
543 #print "coverage ref exists for $file:\n" . Data::Dumper::Dumper($coverage_ref) . "\n";
548 my $para = HTML::Element->new('p');
549 my $span = HTML::Element->new('span');
550 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
552 $span->attr('style', "color:red;");
554 $span->push_content("No coverage found");
555 $para->push_content($span);
556 $body->push_content($para);
559 for my $patch (@$patchref)
561 my $hunkstr="Hunk: " . $patch->[0];
562 if( $patch->[1] > 1 )
564 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
567 my $para = HTML::Element->new('p');
568 my $span = HTML::Element->new('span');
569 $span->attr('style', "font-weight:bold;");
570 $span->push_content($hunkstr);
571 $para->push_content($span);
572 $body->push_content($para);
574 my $codeHunk = HTML::Element->new('pre');
575 for(my $i = 0; $i < $patch->[1]; $i++ )
577 my $line = $i + $patch->[0];
578 my $num_line_digits=log($line)/log(10);
579 for $i (0..(6-$num_line_digits-1))
581 $codeHunk->push_content(" ");
584 $codeHunk->push_content("$line ");
586 my $srcLine = HTML::Element->new('span');
591 if($coverage_ref->{"covered"}->{$line})
593 $srcLine->attr('style', "color:green;");
595 elsif($coverage_ref->{"uncovered"}->{$line})
597 $srcLine->attr('style', "color:red;font-weight:bold;");
601 $srcLine->attr('style', "color:black;font-weight:normal;");
603 my $src=$coverage_ref->{"src"}->{$line};
605 $srcLine->push_content($src);
609 # We don't have coverage data, so print it from the patch instead.
610 my $src = $b_lines_ref->{$line};
611 $srcLine->attr('style', "color:black;font-weight:normal;");
612 $srcLine->push_content($src);
614 $codeHunk->push_content($srcLine, "\n");
616 $body->push_content($codeHunk, "\n");
619 $body->push_content(HTML::Element->new('hr'));
620 $html->push_content($body, "\n");
622 open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n";
624 print $filehandle <<EOH;
625 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
626 "http://www.w3.org/TR/REC-html40/loose.dtd">
629 print $filehandle $html->as_HTML();
634 ################################################################################
636 ################################################################################
639 chdir $repo->wc_path();
641 `make rename_cov_data`;
643 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
645 my $status = $repo->command("status", "-s");
646 if( $status eq "" && !scalar(@ARGV))
648 # There are no changes in the index or working tree, and
649 # no diff arguments to append. Use the last patch instead.
650 push @cmd, ('HEAD~1','HEAD');
654 # detect if there are only cached changes or only working tree changes
657 for my $fstat ( split(/\n/, $status) )
659 if(substr( $fstat, 0, 1 ) ne " "){ $cached++; }
660 if(substr( $fstat, 1, 1 ) ne " "){ $working++; }
666 push @cmd, "--cached";
670 die "Both cached & working files - cannot get correct patch from git\n";
671 # Would have to diff from separate clone.
677 my $filesref = run_diff(@cmd);
681 # Check how many actual source files there are in the patch
683 foreach my $file (keys(%$filesref))
685 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
686 next if($path !~ /^dali/);
687 next if($suffix ne ".cpp" && $suffix ne ".c" && $suffix ne ".h");
690 if( $filecount == 0 )
692 print "No source files found\n";
693 exit 0; # Exit with no error.
696 my $percentref = calc_patch_coverage_percentage($filesref);
697 if($percentref->[0] == 0)
699 print "No coverable lines found\n";
702 my $percent = $percentref->[1];
707 print "Outputing to $opt_output\n" if $debug;
708 patch_html_output($filesref);
710 elsif( ! $opt_quiet )
712 patch_output($filesref);
720 printf("Percentage of change covered: %5.2f%\n", $percent);
733 patch-coverage.pl - Determine if patch coverage is below 90%
736 Calculates how well the most recent patch is covered (either the
737 patch that is in the index+working directory, or HEAD).
744 Use index files if there is nothing in the working tree
750 Don't generate any output
753 0 if the coverage of source files is > 90%, otherwise 1