[3.0] Updated patch-coverage tool to catch fewer errors
[platform/core/uifw/dali-adaptor.git] / automated-tests / patch-coverage.pl
1 #!/usr/bin/perl
2 #
3 # Copyright (c) 2016 Samsung Electronics Co., Ltd.
4 #
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
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
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.
16 #
17
18 use strict;
19 use Git;
20 use Getopt::Long;
21 use Error qw(:try);
22 use Pod::Usage;
23 use File::Basename;
24 use File::stat;
25 use Scalar::Util qw /looks_like_number/;
26 use Cwd qw /getcwd/;
27 use Term::ANSIColor qw(:constants);
28
29 # Program to run gcov on files in patch (that are in source dirs - needs to be dali-aware).
30
31 # A) Get patch
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
39
40 # Sources for conversion of gcno/gcda files:
41 # ~/bin/lcov
42 # Python git-coverage (From http://stef.thewalter.net/git-coverage-useful-code-coverage.html)
43
44 our $repo = Git->repository();
45 our $debug=0;
46 our $pd_debug=0;
47 our $opt_cached;
48 our $opt_help;
49 our $opt_output;
50 our $opt_quiet;
51 our $opt_verbose;
52
53 my %options = (
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"=>"" });
59
60 my %longOptions = map { $_ => $options{$_}->{"optvar"} } keys(%options);
61 GetOptions( %longOptions ) or pod2usage(2);
62 pod2usage(1) if $opt_help;
63
64
65 ## Format per file, repeated, no linebreak
66 # <diffcmd>
67 # index c1..c2 c3
68 # --- a/<left-hand-side-file>
69 # +++ b/<right-hand-side-file>
70 # <diff hunks>
71
72 # Format of each diff hunk, repeated, no linebreak
73 # @@ <ranges> @@ line
74 # 3 lines of context
75 # [-|+]lines removed on left, added on right
76 # 3 lines of context
77 #
78 # output:
79 sub parse_diff
80 {
81     my $patchref = shift;
82     my $file="";
83     my @checklines=();
84     my %b_lines=();
85     my $state = 0;
86     my $store_line=-1;
87     my %files=();
88
89     print "Patch size: ".scalar(@$patchref)."\n" if $pd_debug;
90     for my $line (@$patchref)
91     {
92         if($state == 0)
93         {
94             print "State: $state  $line  \n" if $pd_debug;
95             # Search for a line matching "+++ b/<filename>"
96             if( $line =~ m!^\+\+\+ b/([\w-_\./]*)!)
97             {
98                 $file = $1;
99                 $state = 1 ;
100                 print "Found File: $file\n" if $pd_debug;
101             }
102         }
103         else #elsif($state == 1)
104         {
105             # If we find a line starting with diff, the previous
106             # file's diffs have finished, store them.
107             if( $line =~ /^diff/)
108             {
109                 print "State: $state  $line  \n" if $pd_debug;
110                 $state = 0;
111                 # if the file had changes, store the new/modified line numbers
112                 if( $file && scalar(@checklines))
113                 {
114                     $files{$file}->{"patch"} = [@checklines];
115                     $files{$file}->{"b_lines"} = {%b_lines};
116                     @checklines=();
117                     %b_lines=();
118                 }
119                 print("\n\n") if $pd_debug;
120             }
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 =~ /^@@/)
124             {
125                 print "State: $state  $line  \n" if $pd_debug;
126
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 " ")
130                 {
131                     if( $space eq " " )
132                     {
133                         $length=1;
134                     }
135                     push(@checklines, [$start, $length]);
136                     $store_line=$start;
137                 }
138                 else
139                 {
140                     $store_line = -1;
141                 }
142                 if($pd_debug)
143                 {
144                     my $last = scalar(@checklines)-1;
145                     if( $last >= 0 )
146                     {
147                         print "Checkline:" . $checklines[$last]->[0] . ", " . $checklines[$last]->[1] . "\n";
148                     }
149                 }
150             }
151             # If we find a line starting with "+", it belongs to the new file's patch
152             elsif( $line =~ /^\+/)
153             {
154                if($store_line >= 0)
155                {
156                    chomp;
157                    $line = substr($line, 1); # Remove leading +
158                    $b_lines{$store_line} = $line;
159                    $store_line++;
160                }
161             }
162         }
163     }
164     # Store the final entry
165     $files{$file}->{"patch"} = [@checklines];
166     $files{$file}->{"b_lines"} = {%b_lines};
167
168     my %filter = map { $_ => $files{$_} } grep {m!^dali(-toolkit)?/!} (keys(%files));;
169
170     if($pd_debug)
171     {
172         print("Filtered files:\n");
173         foreach my $file (keys(%filter))
174         {
175             print("$file: ");
176             $patchref = $filter{$file}->{"patch"};
177             foreach my $lineblock (@$patchref)
178             {
179                 print "$lineblock->[0]($lineblock->[1]) "
180             }
181             print ( "\n");
182         }
183     }
184
185     return {%filter};
186 }
187
188 sub show_patch_lines
189 {
190     my $filesref = shift;
191     print "\nNumber of files: " . scalar(keys(%$filesref)) . "\n";
192     for my $file (keys(%$filesref))
193     {
194         print("$file:");
195         my $clref = $filesref->{$file}->{"patch"};
196         for my $cl (@$clref)
197         {
198             print("($cl->[0],$cl->[1]) ");
199         }
200         print("\n");
201     }
202 }
203
204 sub get_gcno_file
205 {
206     # Assumes test cases have been run, and "make rename_cov_data" has been executed
207
208     my $file = shift;
209     my ($name, $path, $suffix) = fileparse($file, (".c", ".cpp", ".h"));
210     my $gcno_file = $repo->wc_path() . "/build/tizen/.cov/$name.gcno";
211
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
215     # from test cases.
216
217     if( -f $gcno_file )
218     {
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)
223         {
224             print "WARNING: GCNO $gcno_file older than SRC $fq_file\n";
225             $gcno_file="";
226         }
227
228     }
229     else
230     {
231         print("WARNING: No equivalent gcno file for $file\n");
232     }
233     return $gcno_file;
234 }
235
236 our %gcovfiles=();
237 sub get_coverage
238 {
239     my $file = shift;
240     my $filesref = shift;
241     print("get_coverage($file)\n") if $debug;
242
243     my $gcno_file = get_gcno_file($file);
244     my @gcov_files = ();
245     my $gcovfile;
246     if( $gcno_file )
247     {
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";
250         while( <$fh> )
251         {
252             print $_ if $debug>=3;
253             chomp;
254             if( m!'(.*\.gcov)'$! )
255             {
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
261
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 )
265                 {
266                     $gcovfile = $coverage_file;
267                     last;
268                 }
269             }
270         }
271         close($fh);
272
273         if($gcovfile)
274         {
275             if($gcovfiles{$gcovfile} == undef)
276             {
277                 # Only parse a gcov file once
278                 $gcovfiles{$gcovfile}->{"seen"}=1;
279
280                 print "Getting coverage data from $gcovfile\n" if $debug;
281
282                 open( FH, "< $gcovfile" ) || die "Can't open $gcovfile for reading:$!\n";
283                 while(<FH>)
284                 {
285                     my ($cov, $line, @code ) = split( /:/, $_ );
286                     $cov =~ s/^\s+//; # Strip leading space
287                     $line =~ s/^\s+//;
288                     my $code=join(":", @code);
289                     if($cov =~ /\#/)
290                     {
291                         # There is no coverage data for these executable lines
292                         $gcovfiles{$gcovfile}->{"uncovered"}->{$line}++;
293                         $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
294                     }
295                     elsif( $cov ne "-" && looks_like_number($cov) && $cov > 0 )
296                     {
297                         $gcovfiles{$gcovfile}->{"covered"}->{$line}=$cov;
298                         $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
299                     }
300                     else
301                     {
302                         # All other lines are not executable.
303                         $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
304                     }
305                 }
306                 close( FH );
307             }
308             $filesref->{$file}->{"coverage"} = $gcovfiles{$gcovfile}; # store hashref
309         }
310         else
311         {
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
315             # check (yet).
316         }
317     }
318 }
319
320 # Run the git diff command to get the patch, then check the coverage
321 # output for the patch.
322 sub run_diff
323 {
324     #print "run_diff(" . join(" ", @_) . ")\n";
325     my ($fh, $c) = $repo->command_output_pipe(@_);
326     our @patch=();
327     while(<$fh>)
328     {
329         chomp;
330         push @patch, $_;
331     }
332     $repo->command_close_pipe($fh, $c);
333
334     print "Patch size: " . scalar(@patch) . "\n" if $debug;
335
336     # @patch has slurped diff for all files...
337     my $filesref = parse_diff ( \@patch );
338     show_patch_lines($filesref) if $debug;
339
340     print "Checking coverage:\n" if $debug;
341
342     my $cwd=getcwd();
343     chdir ".cov" || die "Can't find $cwd/.cov:$!\n";
344
345     for my $file (keys(%$filesref))
346     {
347         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
348         next if($path !~ /^dali/);
349         if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
350         {
351             get_coverage($file, $filesref);
352         }
353     }
354     chdir $cwd;
355     return $filesref;
356 }
357
358 sub calc_patch_coverage_percentage
359 {
360     my $filesref = shift;
361     my $total_covered_lines = 0;
362     my $total_uncovered_lines = 0;
363
364     foreach my $file (keys(%$filesref))
365     {
366         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
367         next if($path !~ /^dali/);
368
369         my $covered_lines = 0;
370         my $uncovered_lines = 0;
371
372         my $patchref = $filesref->{$file}->{"patch"};
373         my $coverage_ref = $filesref->{$file}->{"coverage"};
374         if( $coverage_ref )
375         {
376             for my $patch (@$patchref)
377             {
378                 for(my $i = 0; $i < $patch->[1]; $i++ )
379                 {
380                     my $line = $i + $patch->[0];
381                     if($coverage_ref->{"covered"}->{$line})
382                     {
383                         $covered_lines++;
384                         $total_covered_lines++;
385                     }
386                     if($coverage_ref->{"uncovered"}->{$line})
387                     {
388                         $uncovered_lines++;
389                         $total_uncovered_lines++;
390                     }
391                 }
392             }
393             $coverage_ref->{"covered_lines"} = $covered_lines;
394             $coverage_ref->{"uncovered_lines"} = $uncovered_lines;
395             my $total = $covered_lines + $uncovered_lines;
396             my $percent = 0;
397             if($total > 0)
398             {
399                 $percent = $covered_lines / $total;
400             }
401             $coverage_ref->{"percent_covered"} = 100 * $percent;
402         }
403     }
404     my $total_exec = $total_covered_lines + $total_uncovered_lines;
405     my $percent = 0;
406     if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
407
408     return [ $total_exec, $percent ];
409 }
410
411 sub patch_output
412 {
413     my $filesref = shift;
414     foreach my $file (keys(%$filesref))
415     {
416         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
417         next if($path !~ /^dali/);
418
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  ";
423
424         if($coverage_ref)
425         {
426             if( $coverage_ref->{"covered_lines"} > 0
427                 ||
428                 $coverage_ref->{"uncovered_lines"} > 0 )
429             {
430                 print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET;
431             }
432         }
433         else
434         {
435             if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
436             {
437                 print RED;
438             }
439             print "No coverage found";
440         }
441         print RESET "\n";
442
443         for my $patch (@$patchref)
444         {
445             my $hunkstr="Hunk: " . $patch->[0];
446             if( $patch->[1] > 1 )
447             {
448                 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
449             }
450             print BOLD, "$hunkstr\n",  RESET;
451             for(my $i = 0; $i < $patch->[1]; $i++ )
452             {
453                 my $line = $i + $patch->[0];
454                 printf "%-6s  ", $line;
455
456                 if($coverage_ref)
457                 {
458                     my $color;
459                     if($coverage_ref->{"covered"}->{$line})
460                     {
461                         $color=GREEN;
462                     }
463                     elsif($coverage_ref->{"uncovered"}->{$line})
464                     {
465                         $color=BOLD . RED;
466                     }
467                     else
468                     {
469                         $color=BLACK;
470                     }
471                     my $src=$coverage_ref->{"src"}->{$line};
472                     chomp($src);
473                     print $color, "$src\n", RESET;
474                 }
475                 else
476                 {
477                     # We don't have coverage data, so print it from the patch instead.
478                     my $src = $b_lines_ref->{$line};
479                     print "$src\n";
480                 }
481             }
482         }
483     }
484 }
485
486
487 sub patch_html_output
488 {
489     my $filesref = shift;
490
491     open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n";
492
493     my $OUTPUT_FH = select;
494     select $filehandle;
495     print <<EOH;
496 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
497 "http://www.w3.org/TR/REC-html40/loose.dtd">
498 <html>
499 <head>
500 <title>Patch Coverage</title>
501 </head>
502 <body bgcolor="white">
503 EOH
504
505     foreach my $file (keys(%$filesref))
506     {
507         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
508         next if($path !~ /^dali/);
509
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";
514
515         if($coverage_ref)
516         {
517             if( $coverage_ref->{"covered_lines"} > 0
518                 ||
519                 $coverage_ref->{"uncovered_lines"} > 0 )
520             {
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>";
525             }
526         }
527         else
528         {
529             print "<p>";
530             my $span=0;
531             if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
532             {
533                 print "<span style=\"color:red;\">";
534                 $span=1;
535             }
536             print "No coverage found";
537             print "</span>" if $span;
538         }
539         print "</p>";
540
541         for my $patch (@$patchref)
542         {
543             my $hunkstr="Hunk: " . $patch->[0];
544             if( $patch->[1] > 1 )
545             {
546                 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
547             }
548             print "<p style=\"font-weight:bold;\">" . $hunkstr . "</p>";
549
550             print "<pre>";
551             for(my $i = 0; $i < $patch->[1]; $i++ )
552             {
553                 my $line = $i + $patch->[0];
554                 my $num_line_digits=log($line)/log(10);
555                 for $i (0..(6-$num_line_digits-1))
556                 {
557                     print " ";
558                 }
559                 print "$line  ";
560
561                 if($coverage_ref)
562                 {
563                     my $color;
564                     if($coverage_ref->{"covered"}->{$line})
565                     {
566                         print("<span style=\"color:green;\">");
567                     }
568                     elsif($coverage_ref->{"uncovered"}->{$line})
569                     {
570                         print("<span style=\"color:red;font-weight:bold;\">");
571                     }
572                     else
573                     {
574                         #print("<span style=\"color:black;font-weight:normal;\">");
575                     }
576                     my $src=$coverage_ref->{"src"}->{$line};
577                     chomp($src);
578                     #print $color, "$src\n", RESET;
579                     print "$src</span>\n";
580                 }
581                 else
582                 {
583                     # We don't have coverage data, so print it from the patch instead.
584                     my $src = $b_lines_ref->{$line};
585                     print "$src\n";
586                 }
587             }
588             print "<\pre>\n";
589         }
590     }
591
592     print $filehandle "<hr>\n</body>\n</html>\n";
593     close $filehandle;
594     select $OUTPUT_FH;
595 }
596
597
598 ################################################################################
599 ##                                    MAIN                                    ##
600 ################################################################################
601
602 my $cwd = getcwd();
603 chdir $repo->wc_path();
604 chdir "build/tizen";
605 `make rename_cov_data`;
606
607 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
608
609 my $status = $repo->command("status", "-s");
610 if( $status eq "" && !scalar(@ARGV))
611 {
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');
615 }
616 else
617 {
618     # detect if there are only cached changes or only working tree changes
619     my $cached = 0;
620     my $working = 0;
621     for my $fstat ( split(/\n/, $status) )
622     {
623         if(substr( $fstat, 0, 1 ) ne " "){ $cached++; }
624         if(substr( $fstat, 1, 1 ) ne " "){ $working++; }
625     }
626     if($cached > 0 )
627     {
628         if($working == 0)
629         {
630             push @cmd, "--cached";
631         }
632         else
633         {
634             die "Both cached & working files - cannot get correct patch from git\n";
635             # Would have to diff from separate clone.
636         }
637     }
638 }
639
640 push @cmd, @ARGV;
641 my $filesref = run_diff(@cmd);
642
643 chdir $cwd;
644
645 # Check how many actual source files there are in the patch
646 my $filecount = 0;
647 foreach my $file (keys(%$filesref))
648 {
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");
652     $filecount++;
653 }
654 if( $filecount == 0 )
655 {
656     print "No source files found\n";
657     exit 0;    # Exit with no error.
658 }
659
660 my $percentref = calc_patch_coverage_percentage($filesref);
661 if($percentref->[0] == 0)
662 {
663     print "No coverable lines found\n";
664     exit 0;
665 }
666 my $percent = $percentref->[1];
667
668 my $color=BOLD RED;
669 if($opt_output)
670 {
671     print "Outputing to $opt_output\n" if $debug;
672     patch_html_output($filesref);
673 }
674 elsif( ! $opt_quiet )
675 {
676     patch_output($filesref);
677     if($percent>=90)
678     {
679         $color=GREEN;
680     }
681     print RESET;
682 }
683
684 printf("Percentage of change covered: %5.2f%\n", $percent);
685
686 exit($percent<90);
687
688
689 __END__
690
691 =head1 NAME
692
693 patch-coverage
694
695 =head1 SYNOPSIS
696
697 patch-coverage.pl - Determine if patch coverage is below 90%
698
699 =head1 DESCRIPTION
700 Calculates how well the most recent patch is covered (either the
701 patch that is in the index+working directory, or HEAD).
702
703 =head1 OPTIONS
704
705 =over 28
706
707 =item B<-c|--cached>
708 Use index files if there is nothing in the working tree
709
710 =item B<   --help>
711 This help
712
713 =item B<-q|--quiet>
714 Don't generate any output
715
716 =head1 RETURN STATUS
717 0 if the coverage of source files is > 90%, otherwise 1
718
719 =head1 EXAMPLES
720
721
722 =cut