Notify multiline hint to IMF context
[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 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_head;
49 #our $opt_workingtree;
50 #our $opt_diff=1;
51 our $opt_help;
52 our $opt_verbose;
53 our $opt_quiet;
54
55 my %options = (
56     "cached"       => { "optvar"=>\$opt_cached, "desc"=>"Use index" },
57     "head"         => { "optvar"=>\$opt_head, "desc"=>"Use git show" },
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     my ($fh, $c) = $repo->command_output_pipe(@_);
327     our @patch=();
328     while(<$fh>)
329     {
330         chomp;
331         push @patch, $_;
332     }
333     $repo->command_close_pipe($fh, $c);
334
335     # @patch has slurped diff for all files...
336     my $filesref = parse_diff ( \@patch );
337     show_patch_lines($filesref) if $debug;
338
339     print "Checking coverage:\n" if $debug;
340
341     my $cwd=getcwd();
342     chdir ".cov" || die "Can't find $cwd/.cov:$!\n";
343
344     for my $file (keys(%$filesref))
345     {
346         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
347         if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
348         {
349             get_coverage($file, $filesref);
350         }
351     }
352     chdir $cwd;
353     return $filesref;
354 }
355
356
357 sub calc_patch_coverage_percentage
358 {
359     my $filesref = shift;
360     my $total_covered_lines = 0;
361     my $total_uncovered_lines = 0;
362
363     foreach my $file (keys(%$filesref))
364     {
365         my $covered_lines = 0;
366         my $uncovered_lines = 0;
367
368         my $patchref = $filesref->{$file}->{"patch"};
369         my $coverage_ref = $filesref->{$file}->{"coverage"};
370         if( $coverage_ref )
371         {
372             for my $patch (@$patchref)
373             {
374                 for(my $i = 0; $i < $patch->[1]; $i++ )
375                 {
376                     my $line = $i + $patch->[0];
377                     if($coverage_ref->{"covered"}->{$line})
378                     {
379                         $covered_lines++;
380                         $total_covered_lines++;
381                     }
382                     if($coverage_ref->{"uncovered"}->{$line})
383                     {
384                         $uncovered_lines++;
385                         $total_uncovered_lines++;
386                     }
387                 }
388             }
389             $coverage_ref->{"covered_lines"} = $covered_lines;
390             $coverage_ref->{"uncovered_lines"} = $uncovered_lines;
391             my $total = $covered_lines + $uncovered_lines;
392             my $percent = 0;
393             if($total > 0)
394             {
395                 $percent = $covered_lines / $total;
396             }
397             $coverage_ref->{"percent_covered"} = 100 * $percent;
398         }
399     }
400     my $total_exec = $total_covered_lines + $total_uncovered_lines;
401     my $percent = 0;
402     if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
403
404     return $percent;
405 }
406
407 sub patch_output
408 {
409     my $filesref = shift;
410     foreach my $file (keys(%$filesref))
411     {
412         my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
413         next if($path !~ /^dali/);
414
415         my $patchref = $filesref->{$file}->{"patch"};
416         my $b_lines_ref = $filesref->{$file}->{"b_lines"};
417         my $coverage_ref = $filesref->{$file}->{"coverage"};
418         print BOLD, "$file  ";
419
420         if($coverage_ref)
421         {
422             if( $coverage_ref->{"covered_lines"} > 0
423                 ||
424                 $coverage_ref->{"uncovered_lines"} > 0 )
425             {
426                 print GREEN, "Covered: " . $coverage_ref->{"covered_lines"}, RED, " Uncovered: " . $coverage_ref->{"uncovered_lines"}, RESET;
427             }
428         }
429         else
430         {
431             if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
432             {
433                 print RED;
434             }
435             print "No coverage found";
436         }
437         print RESET "\n";
438
439         for my $patch (@$patchref)
440         {
441             my $hunkstr="Hunk: " . $patch->[0];
442             if( $patch->[1] > 1 )
443             {
444                 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
445             }
446             print BOLD, "$hunkstr\n",  RESET;
447             for(my $i = 0; $i < $patch->[1]; $i++ )
448             {
449                 my $line = $i + $patch->[0];
450                 printf "%-6s  ", $line;
451
452                 if($coverage_ref)
453                 {
454                     my $color;
455                     if($coverage_ref->{"covered"}->{$line})
456                     {
457                         $color=GREEN;
458                     }
459                     elsif($coverage_ref->{"uncovered"}->{$line})
460                     {
461                         $color=BOLD . RED;
462                     }
463                     else
464                     {
465                         $color=BLACK;
466                     }
467                     my $src=$coverage_ref->{"src"}->{$line};
468                     chomp($src);
469                     print $color, "$src\n", RESET;
470                 }
471                 else
472                 {
473                     # We don't have coverage data, so print it from the patch instead.
474                     my $src = $b_lines_ref->{$line};
475                     print "$src\n";
476                 }
477             }
478         }
479     }
480 }
481
482
483 ################################################################################
484 ##                                    MAIN                                    ##
485 ################################################################################
486
487 my $cwd = getcwd();
488 chdir $repo->wc_path();
489 chdir "build/tizen";
490 `make rename_cov_data`;
491
492 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
493
494 my $status = $repo->command("status", "-s");
495 if( $status eq "" )
496 {
497     # There are no changes in the index or working tree. Use the last patch instead
498     push @cmd, ('HEAD~1','HEAD');
499 }
500 elsif($opt_cached) # TODO: Remove this option. Instead, need full diff
501 {
502     push @cmd, "--cached";
503 }
504
505 push @cmd, @ARGV;
506 my $filesref = run_diff(@cmd);
507
508 my $percent = calc_patch_coverage_percentage($filesref);
509 if( ! $opt_quiet )
510 {
511     patch_output($filesref);
512     my $color=BOLD RED;
513     if($percent>=90)
514     {
515         $color=GREEN;
516     }
517     printf("Percentage of change covered: $color %5.2f%\n" . RESET, $percent);
518 }
519 exit($percent<90);
520
521
522 __END__
523
524 =head1 NAME
525
526 patch-coverage
527
528 =head1 SYNOPSIS
529
530 patch-coverage.pl - Determine if patch coverage is below 90%
531
532 =head1 DESCRIPTION
533 Calculates how well the most recent patch is covered (either the
534 patch that is in the index+working directory, or HEAD).
535
536 =head1 OPTIONS
537
538 =over 28
539
540 =item B<-c|--cached>
541 Use index files if there is nothing in the working tree
542
543 =item B<   --help>
544 This help
545
546 =item B<-q|--quiet>
547 Don't generate any output
548
549 =head1 RETURN STATUS
550 0 if the coverage of source files is > 90%, otherwise 1
551
552 =head1 EXAMPLES
553
554
555 =cut