[dali_1.4.36] Merge branch 'devel/master'
[platform/core/uifw/dali-toolkit.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 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                     last;
270                 }
271             }
272         }
273         close($fh);
274
275         if($gcovfile)
276         {
277             if($gcovfiles{$gcovfile} == undef)
278             {
279                 # Only parse a gcov file once
280                 $gcovfiles{$gcovfile}->{"seen"}=1;
281
282                 print "Getting coverage data from $gcovfile\n" if $debug;
283
284                 open( FH, "< $gcovfile" ) || die "Can't open $gcovfile for reading:$!\n";
285                 while(<FH>)
286                 {
287                     my ($cov, $line, @code ) = split( /:/, $_ );
288                     $cov =~ s/^\s+//; # Strip leading space
289                     $line =~ s/^\s+//;
290                     my $code=join(":", @code);
291                     if($cov =~ /\#/)
292                     {
293                         # There is no coverage data for these executable lines
294                         $gcovfiles{$gcovfile}->{"uncovered"}->{$line}++;
295                         $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
296                     }
297                     elsif( $cov ne "-" && looks_like_number($cov) && $cov > 0 )
298                     {
299                         $gcovfiles{$gcovfile}->{"covered"}->{$line}=$cov;
300                         $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
301                     }
302                     else
303                     {
304                         # All other lines are not executable.
305                         $gcovfiles{$gcovfile}->{"src"}->{$line}=$code;
306                     }
307                 }
308                 close( FH );
309             }
310             $filesref->{$file}->{"coverage"} = $gcovfiles{$gcovfile}; # store hashref
311         }
312         else
313         {
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
317             # check (yet).
318         }
319     }
320 }
321
322 # Run the git diff command to get the patch, then check the coverage
323 # output for the patch.
324 sub run_diff
325 {
326     #print "run_diff(" . join(" ", @_) . ")\n";
327     my ($fh, $c) = $repo->command_output_pipe(@_);
328     our @patch=();
329     while(<$fh>)
330     {
331         chomp;
332         push @patch, $_;
333     }
334     $repo->command_close_pipe($fh, $c);
335
336     print "Patch size: " . scalar(@patch) . "\n" if $debug;
337
338     # @patch has slurped diff for all files...
339     my $filesref = parse_diff ( \@patch );
340     show_patch_lines($filesref) if $debug;
341
342     print "Checking coverage:\n" if $debug;
343
344     my $cwd=getcwd();
345     chdir ".cov" || die "Can't find $cwd/.cov:$!\n";
346
347     for my $file (keys(%$filesref))
348     {
349         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
350         next if($path !~ /^dali/);
351         if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
352         {
353             get_coverage($file, $filesref);
354         }
355     }
356     chdir $cwd;
357     return $filesref;
358 }
359
360 sub calc_patch_coverage_percentage
361 {
362     my $filesref = shift;
363     my $total_covered_lines = 0;
364     my $total_uncovered_lines = 0;
365
366     foreach my $file (keys(%$filesref))
367     {
368         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
369         next if($path !~ /^dali/);
370
371         my $covered_lines = 0;
372         my $uncovered_lines = 0;
373
374         my $patchref = $filesref->{$file}->{"patch"};
375         my $coverage_ref = $filesref->{$file}->{"coverage"};
376         if( $coverage_ref )
377         {
378             for my $patch (@$patchref)
379             {
380                 for(my $i = 0; $i < $patch->[1]; $i++ )
381                 {
382                     my $line = $i + $patch->[0];
383                     if($coverage_ref->{"covered"}->{$line})
384                     {
385                         $covered_lines++;
386                         $total_covered_lines++;
387                     }
388                     if($coverage_ref->{"uncovered"}->{$line})
389                     {
390                         $uncovered_lines++;
391                         $total_uncovered_lines++;
392                     }
393                 }
394             }
395             $coverage_ref->{"covered_lines"} = $covered_lines;
396             $coverage_ref->{"uncovered_lines"} = $uncovered_lines;
397             my $total = $covered_lines + $uncovered_lines;
398             my $percent = 0;
399             if($total > 0)
400             {
401                 $percent = $covered_lines / $total;
402             }
403             $coverage_ref->{"percent_covered"} = 100 * $percent;
404         }
405     }
406     my $total_exec = $total_covered_lines + $total_uncovered_lines;
407     my $percent = 0;
408     if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
409
410     return [ $total_exec, $percent ];
411 }
412
413 sub patch_output
414 {
415     my $filesref = shift;
416     foreach my $file (keys(%$filesref))
417     {
418         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
419         next if($path !~ /^dali/);
420
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  ";
425
426         if($coverage_ref)
427         {
428             if( $coverage_ref->{"covered_lines"} > 0
429                 ||
430                 $coverage_ref->{"uncovered_lines"} > 0 )
431             {
432                 print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET;
433             }
434         }
435         else
436         {
437             if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
438             {
439                 print RED;
440             }
441             print "No coverage found";
442         }
443         print RESET "\n";
444
445         for my $patch (@$patchref)
446         {
447             my $hunkstr="Hunk: " . $patch->[0];
448             if( $patch->[1] > 1 )
449             {
450                 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
451             }
452             print BOLD, "$hunkstr\n",  RESET;
453             for(my $i = 0; $i < $patch->[1]; $i++ )
454             {
455                 my $line = $i + $patch->[0];
456                 printf "%-6s  ", $line;
457
458                 if($coverage_ref)
459                 {
460                     my $color;
461                     if($coverage_ref->{"covered"}->{$line})
462                     {
463                         $color=GREEN;
464                     }
465                     elsif($coverage_ref->{"uncovered"}->{$line})
466                     {
467                         $color=BOLD . RED;
468                     }
469                     else
470                     {
471                         $color=BLACK;
472                     }
473                     my $src=$coverage_ref->{"src"}->{$line};
474                     chomp($src);
475                     print $color, "$src\n", RESET;
476                 }
477                 else
478                 {
479                     # We don't have coverage data, so print it from the patch instead.
480                     my $src = $b_lines_ref->{$line};
481                     print "$src\n";
482                 }
483             }
484         }
485     }
486 }
487
488
489 sub patch_html_output
490 {
491     my $filesref = shift;
492
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");
499
500     my $body = HTML::Element->new('body');
501     $body->attr('bgcolor', "white");
502
503     foreach my $file (sort(keys(%$filesref)))
504     {
505         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
506         next if($path !~ /^dali/);
507
508         my $patchref = $filesref->{$file}->{"patch"};
509         my $b_lines_ref = $filesref->{$file}->{"b_lines"};
510         my $coverage_ref = $filesref->{$file}->{"coverage"};
511
512         my $header = HTML::Element->new('h2');
513         $header->push_content($file);
514         $body->push_content($header);
515         $body->push_content("\n");
516         if($coverage_ref)
517         {
518             if( $coverage_ref->{"covered_lines"} > 0
519                 ||
520                 $coverage_ref->{"uncovered_lines"} > 0 )
521             {
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);
527
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);
534             }
535             else
536             {
537                 #print "coverage ref exists for $file:\n" . Data::Dumper::Dumper($coverage_ref) . "\n";
538             }
539         }
540         else
541         {
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")
545             {
546                 $span->attr('style', "color:red;");
547             }
548             $span->push_content("No coverage found");
549             $para->push_content($span);
550             $body->push_content($para);
551         }
552
553         for my $patch (@$patchref)
554         {
555             my $hunkstr="Hunk: " . $patch->[0];
556             if( $patch->[1] > 1 )
557             {
558                 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
559             }
560
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);
567
568             my $codeHunk = HTML::Element->new('pre');
569             for(my $i = 0; $i < $patch->[1]; $i++ )
570             {
571                 my $line = $i + $patch->[0];
572                 my $num_line_digits=log($line)/log(10);
573                 for $i (0..(6-$num_line_digits-1))
574                 {
575                     $codeHunk->push_content(" ");
576                 }
577
578                 $codeHunk->push_content("$line  ");
579
580                 my $srcLine = HTML::Element->new('span');
581                 if($coverage_ref)
582                 {
583                     my $color;
584
585                     if($coverage_ref->{"covered"}->{$line})
586                     {
587                         $srcLine->attr('style', "color:green;");
588                     }
589                     elsif($coverage_ref->{"uncovered"}->{$line})
590                     {
591                         $srcLine->attr('style', "color:red;font-weight:bold;");
592                     }
593                     else
594                     {
595                         $srcLine->attr('style', "color:black;font-weight:normal;");
596                     }
597                     my $src=$coverage_ref->{"src"}->{$line};
598                     chomp($src);
599                     $srcLine->push_content($src);
600                 }
601                 else
602                 {
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);
607                 }
608                 $codeHunk->push_content($srcLine, "\n");
609             }
610             $body->push_content($codeHunk, "\n");
611         }
612     }
613     $body->push_content(HTML::Element->new('hr'));
614     $html->push_content($body, "\n");
615
616     open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n";
617
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">
621 EOH
622 ;
623     print $filehandle $html->as_HTML();
624     close $filehandle;
625 }
626
627
628 ################################################################################
629 ##                                    MAIN                                    ##
630 ################################################################################
631
632 my $cwd = getcwd();
633 chdir $repo->wc_path();
634 chdir "build/tizen";
635 `make rename_cov_data`;
636
637 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
638
639 my $status = $repo->command("status", "-s");
640 if( $status eq "" && !scalar(@ARGV))
641 {
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');
645 }
646 else
647 {
648     # detect if there are only cached changes or only working tree changes
649     my $cached = 0;
650     my $working = 0;
651     for my $fstat ( split(/\n/, $status) )
652     {
653         if(substr( $fstat, 0, 1 ) ne " "){ $cached++; }
654         if(substr( $fstat, 1, 1 ) ne " "){ $working++; }
655     }
656     if($cached > 0 )
657     {
658         if($working == 0)
659         {
660             push @cmd, "--cached";
661         }
662         else
663         {
664             die "Both cached & working files - cannot get correct patch from git\n";
665             # Would have to diff from separate clone.
666         }
667     }
668 }
669
670 push @cmd, @ARGV;
671 my $filesref = run_diff(@cmd);
672
673 chdir $cwd;
674
675 # Check how many actual source files there are in the patch
676 my $filecount = 0;
677 foreach my $file (keys(%$filesref))
678 {
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");
682     $filecount++;
683 }
684 if( $filecount == 0 )
685 {
686     print "No source files found\n";
687     exit 0;    # Exit with no error.
688 }
689
690 my $percentref = calc_patch_coverage_percentage($filesref);
691 if($percentref->[0] == 0)
692 {
693     print "No coverable lines found\n";
694     exit 0;
695 }
696 my $percent = $percentref->[1];
697
698 my $color=BOLD RED;
699 if($opt_output)
700 {
701     print "Outputing to $opt_output\n" if $debug;
702     patch_html_output($filesref);
703 }
704 elsif( ! $opt_quiet )
705 {
706     patch_output($filesref);
707     if($percent>=90)
708     {
709         $color=GREEN;
710     }
711     print RESET;
712 }
713
714 printf("Percentage of change covered: %5.2f%\n", $percent);
715
716 exit($percent<90);
717
718
719 __END__
720
721 =head1 NAME
722
723 patch-coverage
724
725 =head1 SYNOPSIS
726
727 patch-coverage.pl - Determine if patch coverage is below 90%
728
729 =head1 DESCRIPTION
730 Calculates how well the most recent patch is covered (either the
731 patch that is in the index+working directory, or HEAD).
732
733 =head1 OPTIONS
734
735 =over 28
736
737 =item B<-c|--cached>
738 Use index files if there is nothing in the working tree
739
740 =item B<   --help>
741 This help
742
743 =item B<-q|--quiet>
744 Don't generate any output
745
746 =head1 RETURN STATUS
747 0 if the coverage of source files is > 90%, otherwise 1
748
749 =head1 EXAMPLES
750
751
752 =cut