3 # Copyright (c) 2020 Samsung Electronics Co., Ltd.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
25 use Scalar::Util qw /looks_like_number/;
27 use Term::ANSIColor qw(:constants);
30 # Dali specific program to run lcov and parse output for files in patch
32 # A) Generate lcov output from lib & test cases
33 # B) Get patch using git diff
34 # C) Generate patch output with covered/uncovered lines marked in green/red
35 # D) Generate coverage data for changed lines
36 # E) Exit status should be 0 for high coverage (90% line coverage for all new/changed lines)
37 # or 1 for low coverage
39 # Sources for conversion of gcno/gcda files:
40 # /usr/bin/lcov & genhtml
41 # Python git-coverage (From http://stef.thewalter.net/git-coverage-useful-code-coverage.html)
44 sub read_info_file($);
45 sub get_info_entry($);
46 sub set_info_entry($$$$$$$$$;$$$$$$);
47 sub combine_info_entries($$$);
48 sub combine_info_files($$);
49 sub compress_brcount($);
51 sub db_to_brcount($;$);
52 sub brcount_db_combine($$$);
56 our $repo = Git->repository();
60 our %info_data; # Hash containing all data from .info files
62 # Settings from genhtml:
63 our $func_coverage; # If set, generate function coverage statistics
64 our $no_func_coverage; # Disable func_coverage
65 our $br_coverage; # If set, generate branch coverage statistics
66 our $no_br_coverage; # Disable br_coverage
67 our $sort = 1; # If set, provide directory listings with sorted entries
68 our $no_sort; # Disable sort
70 # Branch data combination types
74 # Command line options
82 "cached" => { "optvar"=>\$opt_cached, "desc"=>"Use index" },
83 "output:s" => { "optvar"=>\$opt_output, "desc"=>"Generate html output"},
84 "help" => { "optvar"=>\$opt_help, "desc"=>""},
85 "quiet" => { "optvar"=>\$opt_quiet, "desc"=>""},
86 "verbose" => { "optvar"=>\$opt_verbose, "desc"=>"Also output coverage" });
88 my %longOptions = map { $_ => $options{$_}->{"optvar"} } keys(%options);
89 GetOptions( %longOptions ) or pod2usage(2);
90 pod2usage(1) if $opt_help;
95 # read_info_file(info_filename)
97 # Read in the contents of the .info file specified by INFO_FILENAME. Data will
98 # be returned as a reference to a hash containing the following mappings:
100 # %result: for each filename found in file -> \%data
102 # %data: "test" -> \%testdata
103 # "sum" -> \%sumcount
104 # "func" -> \%funcdata
105 # "found" -> $lines_found (number of instrumented lines found in file)
106 # "hit" -> $lines_hit (number of executed lines in file)
107 # "f_found" -> $fn_found (number of instrumented functions found in file)
108 # "f_hit" -> $fn_hit (number of executed functions in file)
109 # "b_found" -> $br_found (number of instrumented branches found in file)
110 # "b_hit" -> $br_hit (number of executed branches in file)
111 # "check" -> \%checkdata
112 # "testfnc" -> \%testfncdata
113 # "sumfnc" -> \%sumfnccount
114 # "testbr" -> \%testbrdata
115 # "sumbr" -> \%sumbrcount
117 # %testdata : name of test affecting this file -> \%testcount
118 # %testfncdata: name of test affecting this file -> \%testfnccount
119 # %testbrdata: name of test affecting this file -> \%testbrcount
121 # %testcount : line number -> execution count for a single test
122 # %testfnccount: function name -> execution count for a single test
123 # %testbrcount : line number -> branch coverage data for a single test
124 # %sumcount : line number -> execution count for all tests
125 # %sumfnccount : function name -> execution count for all tests
126 # %sumbrcount : line number -> branch coverage data for all tests
127 # %funcdata : function name -> line number
128 # %checkdata : line number -> checksum of source code line
129 # $brdata : vector of items: block, branch, taken
131 # Note that .info file sections referring to the same file and test name
132 # will automatically be combined by adding all execution counts.
134 # Note that if INFO_FILENAME ends with ".gz", it is assumed that the file
135 # is compressed using GZIP. If available, GUNZIP will be used to decompress
140 sub read_info_file($)
142 my $tracefile = $_[0]; # Name of tracefile
143 my %result; # Resulting hash: file -> data
144 my $data; # Data handle for current entry
156 my $line; # Current line read from .info file
157 my $testname; # Current test name
158 my $filename; # Current filename
159 my $hitcount; # Count for lines hit
160 my $count; # Execution count of current line
161 my $negative; # If set, warn about negative counts
162 my $changed_testname; # If set, warn about changed testname
163 my $line_checksum; # Checksum of current line
164 my $notified_about_relative_paths;
165 local *INFO_HANDLE; # Filehandle for .info file
167 info("Reading data file $tracefile\n");
169 # Check if file exists and is readable
173 die("ERROR: cannot read file $tracefile!\n");
176 # Check if this is really a plain file
179 die("ERROR: not a plain file: $tracefile!\n");
182 # Check for .gz extension
183 if ($tracefile =~ /\.gz$/)
185 # Check for availability of GZIP tool
186 system_no_output(1, "gunzip" ,"-h")
187 and die("ERROR: gunzip command not available!\n");
189 # Check integrity of compressed file
190 system_no_output(1, "gunzip", "-t", $tracefile)
191 and die("ERROR: integrity check failed for ".
192 "compressed file $tracefile!\n");
194 # Open compressed file
195 open(INFO_HANDLE, "-|", "gunzip -c '$tracefile'")
196 or die("ERROR: cannot start gunzip to decompress ".
197 "file $tracefile!\n");
201 # Open decompressed file
202 open(INFO_HANDLE, "<", $tracefile)
203 or die("ERROR: cannot read file $tracefile!\n");
207 while (<INFO_HANDLE>)
215 /^TN:([^,]*)(,diff)?/ && do
217 # Test name information found
218 $testname = defined($1) ? $1 : "";
219 if ($testname =~ s/\W/_/g)
221 $changed_testname = 1;
223 $testname .= $2 if (defined($2));
229 # Filename information found
230 # Retrieve data for new entry
231 $filename = File::Spec->rel2abs($1, $root);
233 if (!File::Spec->file_name_is_absolute($1) &&
234 !$notified_about_relative_paths)
236 info("Resolved relative source file ".
237 "path \"$1\" with CWD to ".
239 $notified_about_relative_paths = 1;
242 $data = $result{$filename};
243 ($testdata, $sumcount, $funcdata, $checkdata,
244 $testfncdata, $sumfnccount, $testbrdata,
246 get_info_entry($data);
248 if (defined($testname))
250 $testcount = $testdata->{$testname};
251 $testfnccount = $testfncdata->{$testname};
252 $testbrcount = $testbrdata->{$testname};
263 /^DA:(\d+),(-?\d+)(,[^,\s]+)?/ && do
265 # Fix negative counts
266 $count = $2 < 0 ? 0 : $2;
271 # Execution count found, add to structure
273 $sumcount->{$1} += $count;
275 # Add test-specific counts
276 if (defined($testname))
278 $testcount->{$1} += $count;
281 # Store line checksum if available
284 $line_checksum = substr($3, 1);
286 # Does it match a previous definition
287 if (defined($checkdata->{$1}) &&
291 die("ERROR: checksum mismatch ".
292 "at $filename:$1\n");
295 $checkdata->{$1} = $line_checksum;
300 /^FN:(\d+),([^,]+)/ && do
302 last if (!$func_coverage);
304 # Function data found, add to structure
305 $funcdata->{$2} = $1;
307 # Also initialize function call data
308 if (!defined($sumfnccount->{$2})) {
309 $sumfnccount->{$2} = 0;
311 if (defined($testname))
313 if (!defined($testfnccount->{$2})) {
314 $testfnccount->{$2} = 0;
320 /^FNDA:(\d+),([^,]+)/ && do
322 last if (!$func_coverage);
323 # Function call count found, add to structure
325 $sumfnccount->{$2} += $1;
327 # Add test-specific counts
328 if (defined($testname))
330 $testfnccount->{$2} += $1;
335 /^BRDA:(\d+),(\d+),(\d+),(\d+|-)/ && do {
336 # Branch coverage data found
337 my ($line, $block, $branch, $taken) =
340 last if (!$br_coverage);
341 $sumbrcount->{$line} .=
342 "$block,$branch,$taken:";
344 # Add test-specific counts
345 if (defined($testname)) {
346 $testbrcount->{$line} .=
347 "$block,$branch,$taken:";
352 /^end_of_record/ && do
354 # Found end of section marker
357 # Store current section data
358 if (defined($testname))
360 $testdata->{$testname} =
362 $testfncdata->{$testname} =
364 $testbrdata->{$testname} =
368 set_info_entry($data, $testdata,
369 $sumcount, $funcdata,
370 $checkdata, $testfncdata,
374 $result{$filename} = $data;
385 # Calculate lines_found and lines_hit for each file
386 foreach $filename (keys(%result))
388 $data = $result{$filename};
390 ($testdata, $sumcount, undef, undef, $testfncdata,
391 $sumfnccount, $testbrdata, $sumbrcount) =
392 get_info_entry($data);
394 # Filter out empty files
395 if (scalar(keys(%{$sumcount})) == 0)
397 delete($result{$filename});
400 # Filter out empty test cases
401 foreach $testname (keys(%{$testdata}))
403 if (!defined($testdata->{$testname}) ||
404 scalar(keys(%{$testdata->{$testname}})) == 0)
406 delete($testdata->{$testname});
407 delete($testfncdata->{$testname});
411 $data->{"found"} = scalar(keys(%{$sumcount}));
414 foreach (keys(%{$sumcount}))
416 if ($sumcount->{$_} > 0) { $hitcount++; }
419 $data->{"hit"} = $hitcount;
421 # Get found/hit values for function call data
422 $data->{"f_found"} = scalar(keys(%{$sumfnccount}));
425 foreach (keys(%{$sumfnccount})) {
426 if ($sumfnccount->{$_} > 0) {
430 $data->{"f_hit"} = $hitcount;
432 # Combine branch data for the same branches
433 (undef, $data->{"b_found"}, $data->{"b_hit"}) = compress_brcount($sumbrcount);
434 foreach $testname (keys(%{$testbrdata})) {
435 compress_brcount($testbrdata->{$testname});
439 if (scalar(keys(%result)) == 0)
441 die("ERROR: no valid records found in tracefile $tracefile\n");
445 warn("WARNING: negative counts found in tracefile ".
448 if ($changed_testname)
450 warn("WARNING: invalid characters removed from testname in ".
451 "tracefile $tracefile\n");
457 sub print_simplified_info
459 for my $key (keys(%info_data))
462 my $sumcountref = $info_data{$key}->{"sum"};
463 for my $line (sort{$a<=>$b}(keys(%$sumcountref)))
465 print "L $line: $sumcountref->{$line}\n";
472 # get_info_entry(hash_ref)
474 # Retrieve data from an entry of the structure generated by read_info_file().
475 # Return a list of references to hashes:
476 # (test data hash ref, sum count hash ref, funcdata hash ref, checkdata hash
477 # ref, testfncdata hash ref, sumfnccount hash ref, lines found, lines hit,
478 # functions found, functions hit)
481 sub get_info_entry($)
483 my $testdata_ref = $_[0]->{"test"};
484 my $sumcount_ref = $_[0]->{"sum"};
485 my $funcdata_ref = $_[0]->{"func"};
486 my $checkdata_ref = $_[0]->{"check"};
487 my $testfncdata = $_[0]->{"testfnc"};
488 my $sumfnccount = $_[0]->{"sumfnc"};
489 my $testbrdata = $_[0]->{"testbr"};
490 my $sumbrcount = $_[0]->{"sumbr"};
491 my $lines_found = $_[0]->{"found"};
492 my $lines_hit = $_[0]->{"hit"};
493 my $fn_found = $_[0]->{"f_found"};
494 my $fn_hit = $_[0]->{"f_hit"};
495 my $br_found = $_[0]->{"b_found"};
496 my $br_hit = $_[0]->{"b_hit"};
498 return ($testdata_ref, $sumcount_ref, $funcdata_ref, $checkdata_ref,
499 $testfncdata, $sumfnccount, $testbrdata, $sumbrcount,
500 $lines_found, $lines_hit, $fn_found, $fn_hit,
507 # set_info_entry(hash_ref, testdata_ref, sumcount_ref, funcdata_ref,
508 # checkdata_ref, testfncdata_ref, sumfcncount_ref,
509 # testbrdata_ref, sumbrcount_ref[,lines_found,
510 # lines_hit, f_found, f_hit, $b_found, $b_hit])
512 # Update the hash referenced by HASH_REF with the provided data references.
515 sub set_info_entry($$$$$$$$$;$$$$$$)
517 my $data_ref = $_[0];
519 $data_ref->{"test"} = $_[1];
520 $data_ref->{"sum"} = $_[2];
521 $data_ref->{"func"} = $_[3];
522 $data_ref->{"check"} = $_[4];
523 $data_ref->{"testfnc"} = $_[5];
524 $data_ref->{"sumfnc"} = $_[6];
525 $data_ref->{"testbr"} = $_[7];
526 $data_ref->{"sumbr"} = $_[8];
528 if (defined($_[9])) { $data_ref->{"found"} = $_[9]; }
529 if (defined($_[10])) { $data_ref->{"hit"} = $_[10]; }
530 if (defined($_[11])) { $data_ref->{"f_found"} = $_[11]; }
531 if (defined($_[12])) { $data_ref->{"f_hit"} = $_[12]; }
532 if (defined($_[13])) { $data_ref->{"b_found"} = $_[13]; }
533 if (defined($_[14])) { $data_ref->{"b_hit"} = $_[14]; }
538 # combine_info_entries(entry_ref1, entry_ref2, filename)
540 # Combine .info data entry hashes referenced by ENTRY_REF1 and ENTRY_REF2.
541 # Return reference to resulting hash.
543 sub combine_info_entries($$$)
545 my $entry1 = $_[0]; # Reference to hash containing first entry
555 my $entry2 = $_[1]; # Reference to hash containing second entry
565 my %result; # Hash containing combined entry
567 my $result_sumcount = {};
569 my $result_testfncdata;
570 my $result_sumfnccount;
571 my $result_testbrdata;
572 my $result_sumbrcount;
581 my $filename = $_[2];
584 ($testdata1, $sumcount1, $funcdata1, $checkdata1, $testfncdata1,
585 $sumfnccount1, $testbrdata1, $sumbrcount1) = get_info_entry($entry1);
586 ($testdata2, $sumcount2, $funcdata2, $checkdata2, $testfncdata2,
587 $sumfnccount2, $testbrdata2, $sumbrcount2) = get_info_entry($entry2);
590 # $checkdata1 = merge_checksums($checkdata1, $checkdata2, $filename);
593 # $result_funcdata = merge_func_data($funcdata1, $funcdata2, $filename);
595 # # Combine function call count data
596 # $result_testfncdata = add_testfncdata($testfncdata1, $testfncdata2);
597 # ($result_sumfnccount, $fn_found, $fn_hit) =
598 # add_fnccount($sumfnccount1, $sumfnccount2);
600 # # Combine branch coverage data
601 # $result_testbrdata = add_testbrdata($testbrdata1, $testbrdata2);
602 # ($result_sumbrcount, $br_found, $br_hit) =
603 # combine_brcount($sumbrcount1, $sumbrcount2, $BR_ADD);
606 foreach $testname (keys(%{$testdata1}))
608 if (defined($testdata2->{$testname}))
610 # testname is present in both entries, requires
612 ($result_testdata{$testname}) =
613 add_counts($testdata1->{$testname},
614 $testdata2->{$testname});
618 # testname only present in entry1, add to result
619 $result_testdata{$testname} = $testdata1->{$testname};
622 # update sum count hash
623 ($result_sumcount, $lines_found, $lines_hit) =
624 add_counts($result_sumcount,
625 $result_testdata{$testname});
628 foreach $testname (keys(%{$testdata2}))
630 # Skip testnames already covered by previous iteration
631 if (defined($testdata1->{$testname})) { next; }
633 # testname only present in entry2, add to result hash
634 $result_testdata{$testname} = $testdata2->{$testname};
636 # update sum count hash
637 ($result_sumcount, $lines_found, $lines_hit) =
638 add_counts($result_sumcount,
639 $result_testdata{$testname});
642 # Calculate resulting sumcount
645 set_info_entry(\%result, \%result_testdata, $result_sumcount,
646 $result_funcdata, $checkdata1, $result_testfncdata,
647 $result_sumfnccount, $result_testbrdata,
648 $result_sumbrcount, $lines_found, $lines_hit,
649 $fn_found, $fn_hit, $br_found, $br_hit);
656 # combine_info_files(info_ref1, info_ref2)
658 # Combine .info data in hashes referenced by INFO_REF1 and INFO_REF2. Return
659 # reference to resulting hash.
661 sub combine_info_files($$)
663 my %hash1 = %{$_[0]};
664 my %hash2 = %{$_[1]};
667 foreach $filename (keys(%hash2))
669 if ($hash1{$filename})
671 # Entry already exists in hash1, combine them
673 combine_info_entries($hash1{$filename},
679 # Entry is unique in both hashes, simply add to
681 $hash1{$filename} = $hash2{$filename};
690 # add_counts(data1_ref, data2_ref)
692 # DATA1_REF and DATA2_REF are references to hashes containing a mapping
694 # line number -> execution count
696 # Return a list (RESULT_REF, LINES_FOUND, LINES_HIT) where RESULT_REF
697 # is a reference to a hash containing the combined mapping in which
698 # execution counts are added.
702 my $data1_ref = $_[0]; # Hash 1
703 my $data2_ref = $_[1]; # Hash 2
704 my %result; # Resulting hash
705 my $line; # Current line iteration scalar
706 my $data1_count; # Count of line in hash1
707 my $data2_count; # Count of line in hash2
708 my $found = 0; # Total number of lines found
709 my $hit = 0; # Number of lines with a count > 0
711 foreach $line (keys(%$data1_ref))
713 $data1_count = $data1_ref->{$line};
714 $data2_count = $data2_ref->{$line};
716 # Add counts if present in both hashes
717 if (defined($data2_count)) { $data1_count += $data2_count; }
719 # Store sum in %result
720 $result{$line} = $data1_count;
723 if ($data1_count > 0) { $hit++; }
726 # Add lines unique to data2_ref
727 foreach $line (keys(%$data2_ref))
729 # Skip lines already in data1_ref
730 if (defined($data1_ref->{$line})) { next; }
732 # Copy count from data2_ref
733 $result{$line} = $data2_ref->{$line};
736 if ($result{$line} > 0) { $hit++; }
739 return (\%result, $found, $hit);
744 sub compress_brcount($)
749 $db = brcount_to_db($brcount);
750 return db_to_brcount($db, $brcount);
754 # brcount_to_db(brcount)
756 # Convert brcount data to the following format:
758 # db: line number -> block hash
759 # block hash: block number -> branch hash
760 # branch hash: branch number -> taken value
769 # Add branches to database
770 foreach $line (keys(%{$brcount})) {
771 my $brdata = $brcount->{$line};
773 foreach my $entry (split(/:/, $brdata)) {
774 my ($block, $branch, $taken) = split(/,/, $entry);
775 my $old = $db->{$line}->{$block}->{$branch};
777 if (!defined($old) || $old eq "-") {
779 } elsif ($taken ne "-") {
783 $db->{$line}->{$block}->{$branch} = $old;
792 # db_to_brcount(db[, brcount])
794 # Convert branch coverage data back to brcount format. If brcount is specified,
795 # the converted data is directly inserted in brcount.
798 sub db_to_brcount($;$)
800 my ($db, $brcount) = @_;
805 # Convert database back to brcount format
806 foreach $line (sort({$a <=> $b} keys(%{$db}))) {
807 my $ldata = $db->{$line};
811 foreach $block (sort({$a <=> $b} keys(%{$ldata}))) {
812 my $bdata = $ldata->{$block};
815 foreach $branch (sort({$a <=> $b} keys(%{$bdata}))) {
816 my $taken = $bdata->{$branch};
819 $br_hit++ if ($taken ne "-" && $taken > 0);
820 $brdata .= "$block,$branch,$taken:";
823 $brcount->{$line} = $brdata;
826 return ($brcount, $br_found, $br_hit);
831 # brcount_db_combine(db1, db2, op)
833 # db1 := db1 op db2, where
834 # db1, db2: brcount data as returned by brcount_to_db
835 # op: one of $BR_ADD and BR_SUB
837 sub brcount_db_combine($$$)
839 my ($db1, $db2, $op) = @_;
841 foreach my $line (keys(%{$db2})) {
842 my $ldata = $db2->{$line};
844 foreach my $block (keys(%{$ldata})) {
845 my $bdata = $ldata->{$block};
847 foreach my $branch (keys(%{$bdata})) {
848 my $taken = $bdata->{$branch};
849 my $new = $db1->{$line}->{$block}->{$branch};
851 if (!defined($new) || $new eq "-") {
853 } elsif ($taken ne "-") {
854 if ($op == $BR_ADD) {
856 } elsif ($op == $BR_SUB) {
858 $new = 0 if ($new < 0);
862 $db1->{$line}->{$block}->{$branch} = $new;
879 # Format per file, repeated, no linebreak
882 # --- a/<left-hand-side-file>
883 # +++ b/<right-hand-side-file>
886 # Format of each diff hunk, repeated, no linebreak
887 # @@ <ranges> @@ line
889 # [-|+]lines removed on left, added on right
893 # <dali-source-file>: source / header files in dali/dali-toolkit
894 # \%filter: <dali-source-file> -> \%filedata
895 # %filedata: "patch" -> \@checklines
896 # "b_lines" -> \%b_lines
897 # @checklines: vector of \[start, length] # line numbers of new/modified lines
898 # %b_lines: <line-number> -> patch line in b-file.
901 my $patchref = shift;
909 print "Patch size: ".scalar(@$patchref)."\n" if $pd_debug;
910 for my $line (@$patchref)
914 print "State: $state $line \n" if $pd_debug;
915 # Search for a line matching "+++ b/<filename>"
916 if( $line =~ m!^\+\+\+ b/([\w-_\./]*)!)
920 print "Found File: $file\n" if $pd_debug;
923 else #elsif($state == 1)
925 # If we find a line starting with diff, the previous
926 # file's diffs have finished, store them.
927 if( $line =~ /^diff/)
929 print "State: $state $line \n" if $pd_debug;
931 # if the file had changes, store the new/modified line numbers
932 if( $file && scalar(@checklines))
934 $files{$file}->{"patch"} = [@checklines];
935 $files{$file}->{"b_lines"} = {%b_lines};
939 print("\n\n") if $pd_debug;
941 # If we find a line starting with @@, it tells us the line numbers
942 # of the old file and new file for this hunk.
943 elsif( $line =~ /^@@/)
945 print "State: $state $line \n" if $pd_debug;
947 # Find the lines in the new file (of the form "+<start>[,<length>])
948 my($start,$space,$length) = ($line =~ /\+([0-9]+)(,| )([0-9]+)?/);
949 if($length || $space eq " ")
955 push(@checklines, [$start, $length]);
964 my $last = scalar(@checklines)-1;
967 print "Checkline:" . $checklines[$last]->[0] . ", " . $checklines[$last]->[1] . "\n";
971 # If we find a line starting with "+", it belongs to the new file's patch
972 elsif( $line =~ /^\+/)
977 $line = substr($line, 1); # Remove leading +
978 $b_lines{$store_line} = $line;
984 # Store the final entry
985 $files{$file}->{"patch"} = [@checklines];
986 $files{$file}->{"b_lines"} = {%b_lines};
988 my %filter = map { $_ => $files{$_} } grep {m!^dali(-toolkit|-scene3d)?/!} (keys(%files));
992 print("Filtered files:\n");
993 foreach my $file (keys(%filter))
996 $patchref = $filter{$file}->{"patch"};
997 foreach my $lineblock (@$patchref)
999 print "$lineblock->[0]($lineblock->[1]) "
1005 return {%filter};#copy? - test and fixme
1008 sub show_patch_lines
1010 my $filesref = shift;
1011 print "\nNumber of files: " . scalar(keys(%$filesref)) . "\n";
1012 for my $file (keys(%$filesref))
1015 my $clref = $filesref->{$file}->{"patch"};
1016 for my $cl (@$clref)
1018 print("($cl->[0],$cl->[1]) ");
1025 # Run the git diff command to get the patch
1026 # Output - see parse_diff
1029 my ($fh, $c) = $repo->command_output_pipe(@_);
1036 $repo->command_close_pipe($fh, $c);
1038 print "Patch size: " . scalar(@patch) . "\n" if $debug;
1040 # @patch has slurped diff for all files...
1041 my $filesref = parse_diff ( \@patch );
1042 show_patch_lines($filesref) if $debug>1;
1047 sub calc_patch_coverage_percentage
1049 my $filesref = shift;
1050 my $total_covered_lines = 0;
1051 my $total_uncovered_lines = 0;
1053 foreach my $file (keys(%$filesref))
1055 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
1056 next if($path !~ /^dali/);
1058 my $covered_lines = 0;
1059 my $uncovered_lines = 0;
1061 my $patchref = $filesref->{$file}->{"patch"};
1063 my $abs_filename = File::Spec->rel2abs($file, $root);
1064 my $sumcountref = $info_data{$abs_filename}->{"sum"};
1068 print("File: $abs_filename\n");
1069 print Dumper($info_data{$abs_filename});
1075 for my $patch (@$patchref)
1077 for(my $i = 0; $i < $patch->[1]; $i++ )
1079 my $line = $i + $patch->[0];
1080 if(exists($sumcountref->{$line}))
1082 if( $sumcountref->{$line} > 0 )
1085 $total_covered_lines++;
1090 $total_uncovered_lines++;
1095 $filesref->{$file}->{"covered_lines"} = $covered_lines;
1096 $filesref->{$file}->{"uncovered_lines"} = $uncovered_lines;
1097 my $total = $covered_lines + $uncovered_lines;
1101 $percent = $covered_lines / $total;
1103 $filesref->{$file}->{"percent_covered"} = 100 * $percent;
1107 print "Can't find coverage data for $abs_filename\n";
1110 my $total_exec = $total_covered_lines + $total_uncovered_lines;
1112 if($total_exec > 0) { $percent = 100 * $total_covered_lines / $total_exec; }
1114 return [ $total_exec, $percent, $total_covered_lines ];
1121 my $filesref = shift;
1122 foreach my $file (keys(%$filesref))
1124 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
1125 next if($path !~ /^dali/);
1127 my $fileref = $filesref->{$file};
1128 my $patchref = $fileref->{"patch"};
1129 my $b_lines_ref = $fileref->{"b_lines"};
1131 my $abs_filename = File::Spec->rel2abs($file, $root);
1132 my $sumcountref = $info_data{$abs_filename}->{"sum"};
1134 print BOLD, "$file ";
1138 if( $fileref->{"covered_lines"} > 0
1140 $fileref->{"uncovered_lines"} > 0 )
1142 print GREEN, "Covered: " . $fileref->{"covered_lines"}, RED, " Uncovered: " . $fileref->{"uncovered_lines"}, RESET;
1147 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
1151 print "No coverage found";
1155 for my $patch (@$patchref)
1157 my $hunkstr="Hunk: " . $patch->[0];
1158 if( $patch->[1] > 1 )
1160 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
1162 print BOLD, "$hunkstr\n", RESET;
1163 for(my $i = 0; $i < $patch->[1]; $i++ )
1165 my $line = $i + $patch->[0];
1166 printf "%-6s ", $line;
1171 if(exists($sumcountref->{$line}))
1173 if($sumcountref->{$line} > 0)
1186 my $src = $b_lines_ref->{$line};
1187 print $color, "$src\n", RESET;
1191 my $src = $b_lines_ref->{$line};
1201 sub patch_html_output
1203 my $filesref = shift;
1205 open( my $filehandle, ">", $opt_output ) || die "Can't open $opt_output for writing:$!\n";
1207 my $OUTPUT_FH = select;
1210 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
1211 "http://www.w3.org/TR/REC-html40/loose.dtd">
1214 <title>Patch Coverage</title>
1216 <body bgcolor="white">
1219 foreach my $file (keys(%$filesref))
1221 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
1222 next if($path !~ /^dali/);
1224 my $fileref = $filesref->{$file};
1225 my $patchref = $fileref->{"patch"};
1226 my $b_lines_ref = $fileref->{"b_lines"};
1228 my $abs_filename = File::Spec->rel2abs($file, $root);
1229 my $sumcountref = $info_data{$abs_filename}->{"sum"};
1231 print "<h2>$file</h2>\n";
1235 if( $fileref->{"covered_lines"} > 0
1237 $fileref->{"uncovered_lines"} > 0 )
1239 print "<p style=\"color:green;\">Covered: " .
1240 $fileref->{"covered_lines"} . "<p>" .
1241 "<p style=\"color:red;\">Uncovered: " .
1242 $fileref->{"uncovered_lines"} . "</span></p>";
1249 if($suffix eq ".cpp" || $suffix eq ".c" || $suffix eq ".h")
1251 print "<span style=\"color:red;\">";
1254 print "No coverage found";
1255 print "</span>" if $span;
1259 for my $patch (@$patchref)
1261 my $hunkstr="Hunk: " . $patch->[0];
1262 if( $patch->[1] > 1 )
1264 $hunkstr .= " - " . ($patch->[0]+$patch->[1]-1);
1266 print "<p style=\"font-weight:bold;\">" . $hunkstr . "</p>";
1269 for(my $i = 0; $i < $patch->[1]; $i++ )
1271 my $line = $i + $patch->[0];
1272 my $num_line_digits=log($line)/log(10);
1273 for $i (0..(6-$num_line_digits-1))
1282 if(exists($sumcountref->{$line}))
1284 if($sumcountref->{$line} > 0)
1286 print("<span style=\"color:green;\">");
1290 print("<span style=\"color:red;font-weight:bold;\">");
1295 print("<span style=\"color:black;font-weight:normal;\">");
1297 my $src=$b_lines_ref->{$line};
1299 print "$src</span>\n";
1303 my $src = $b_lines_ref->{$line};
1311 print $filehandle "<hr>\n</body>\n</html>\n";
1317 ################################################################################
1319 ################################################################################
1322 # Generate git diff command
1323 my @cmd=('--no-pager','diff','--no-ext-diff','-U0','--no-color');
1324 my $status = $repo->command("status", "-s");
1326 if( $status eq "" && !scalar(@ARGV))
1328 # There are no changes in the index or working tree, and
1329 # no diff arguments to append. Use the last patch instead.
1330 push @cmd, ('HEAD~1','HEAD');
1334 # detect if there are only cached changes or only working tree changes
1337 for my $fstat ( split(/\n/, $status) )
1339 if(substr( $fstat, 0, 1 ) ne " "){ $cached++; }
1340 if(substr( $fstat, 1, 1 ) ne " "){ $working++; }
1346 push @cmd, "--cached";
1350 die "Error: Both cached & working files - cannot get correct patch from git\nRun git add first.";
1351 # Would have to diff from separate clone.
1358 # Before executing the diff, run the coverage.sh script. This is done here so that the
1359 # error condition above happens straight away, rather than after spewing out lots of information.
1361 my $cwd = getcwd(); # expect this to be automated-tests folder
1362 # execute coverage.sh, generating build/tizen/dali.info from lib, and
1363 # *.dir/dali.info. Don't generate html
1364 printf("Running coverage.sh\n");
1365 my $coverage_output=`./coverage.sh -n`;
1369 our %info_data; # Hash of all data from .info files
1370 my @info_files = split(/\n/, `find . -name dali.info`);
1373 # Read in all specified .info files
1374 foreach (@info_files)
1376 %new_info = %{read_info_file($_)};
1378 # Combine %new_info with %info_data
1379 %info_data = %{combine_info_files(\%info_data, \%new_info)};
1383 # Execute diff & coverage from root directory
1384 my $filesref = run_diff(@cmd);
1388 # Check how many actual source files there are in the patch
1390 foreach my $file (keys(%$filesref))
1392 my ($name, $path, $suffix) = fileparse($file, qr{\.[^.]*$});
1393 next if($path !~ /^dali/);
1394 next if($suffix ne ".cpp" && $suffix ne ".c" && $suffix ne ".h");
1397 if( $filecount == 0 )
1399 print "Warning: No source files found\n";
1400 exit 0; # Exit with no error.
1403 #print_simplified_info() if $debug;
1407 print "Info keys:\n";
1408 for my $key (keys(%info_data))
1415 my $percentref = calc_patch_coverage_percentage($filesref);
1416 if($percentref->[0] == 0)
1418 print "Warning: No coverable lines found\n";
1421 my $percent = $percentref->[1];
1423 printf(join("\n", grep { $_ !~ /^Remov/ } split(/\n/,$coverage_output))) if $opt_verbose;
1424 #printf($coverage_output) if $opt_verbose;
1429 print "Outputing to $opt_output\n" if $debug;
1430 patch_html_output($filesref);
1432 elsif( ! $opt_quiet )
1434 patch_output($filesref);
1442 printf("\n\n=========================\nPatch coverage output:\n=========================\n");
1443 printf("Line Coverage: %d/%d\n", $percentref->[2], $percentref->[0]);
1444 printf("Percentage of change covered: %5.2f%\n", $percent);
1457 patch-coverage.pl - Determine if patch coverage is below 90%
1460 Calculates how well the most recent patch is covered (either the
1461 patch that is in the index+working directory, or HEAD).
1467 =item B<-c|--cached>
1468 Use index files if there is nothing in the working tree
1474 Don't generate any output
1476 =head1 RETURN STATUS
1477 0 if the coverage of source files is > 90%, otherwise 1