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