1 | #!/usr/bin/perl |
---|
2 | |
---|
3 | use strict; |
---|
4 | use warnings; |
---|
5 | |
---|
6 | use Cwd; |
---|
7 | |
---|
8 | my $verbose = 0; |
---|
9 | |
---|
10 | my $showCoverageForAll; # 1=all; 0=files containing tests only |
---|
11 | my $showCoverageForFilesMatching; # regexpr filter for covered files |
---|
12 | my $sortBySectionSize; # 0=by location; 1=by section size |
---|
13 | |
---|
14 | sub setup() { |
---|
15 | sub env_defined_non_empty($) { |
---|
16 | my ($varname) = @_; |
---|
17 | my $value = $ENV{$varname}; |
---|
18 | if (not defined $value) { die "environment variable $varname is undefined"; } |
---|
19 | if ($value eq '') { die "environment variable $varname is empty"; } |
---|
20 | chomp($value); |
---|
21 | $value =~ s/^\s+//go; |
---|
22 | $value =~ s/\s+$//go; |
---|
23 | |
---|
24 | if ($value =~ /^'(.*)'$/o) { $value = $1; } |
---|
25 | return $value; |
---|
26 | } |
---|
27 | |
---|
28 | my $SORT_COVERAGE = env_defined_non_empty('SORT_COVERAGE'); |
---|
29 | my $RESTRICT_COVERAGE = env_defined_non_empty('RESTRICT_COVERAGE'); |
---|
30 | |
---|
31 | print "SORT_COVERAGE='$SORT_COVERAGE'\n"; |
---|
32 | print "RESTRICT_COVERAGE='$RESTRICT_COVERAGE'\n"; |
---|
33 | |
---|
34 | $showCoverageForAll = 1; |
---|
35 | $showCoverageForFilesMatching = qr/.*/; |
---|
36 | |
---|
37 | if ($RESTRICT_COVERAGE eq 'NO') { |
---|
38 | ; |
---|
39 | } |
---|
40 | elsif ($RESTRICT_COVERAGE eq 'MODULE') { |
---|
41 | my $RESTRICT_MODULE = env_defined_non_empty('RESTRICT_MODULE'); |
---|
42 | print "RESTRICT_MODULE='$RESTRICT_MODULE'"; |
---|
43 | if ($RESTRICT_MODULE eq '.') { |
---|
44 | print " (restricting to tested modules)\n"; |
---|
45 | $showCoverageForAll = 0; |
---|
46 | } |
---|
47 | else { |
---|
48 | print "\n"; |
---|
49 | $showCoverageForFilesMatching = qr/$RESTRICT_MODULE/; |
---|
50 | } |
---|
51 | } |
---|
52 | else { |
---|
53 | $showCoverageForFilesMatching = qr/$RESTRICT_COVERAGE/; |
---|
54 | } |
---|
55 | |
---|
56 | if ($SORT_COVERAGE eq 'LOCATION') { $sortBySectionSize = 0; } |
---|
57 | elsif ($SORT_COVERAGE eq 'SIZE') { $sortBySectionSize = 1; } |
---|
58 | else { die "SORT_COVERAGE '$SORT_COVERAGE' is unknown"; } |
---|
59 | } |
---|
60 | |
---|
61 | # -------------------------------------------------------------------------------- |
---|
62 | |
---|
63 | my %code = (); # key=lineno, value=code (valid for recently parsed lines with type '#') |
---|
64 | |
---|
65 | # -------------------------------------------------------------------------------- |
---|
66 | |
---|
67 | my ($loclen,$msglen) = (0,0); |
---|
68 | |
---|
69 | sub reset_trim() { |
---|
70 | ($loclen,$msglen) = (0,0); |
---|
71 | } |
---|
72 | |
---|
73 | sub trim($\$) { |
---|
74 | my ($str, $len_r) = @_; |
---|
75 | my $len = length($str); |
---|
76 | if ($len > $$len_r) { $$len_r = $len; } |
---|
77 | else { |
---|
78 | $str = sprintf("%-*s", $$len_r, $str); |
---|
79 | } |
---|
80 | return $str; |
---|
81 | } |
---|
82 | |
---|
83 | sub print_trimmed($$$$) { |
---|
84 | my ($source,$lineno,$msg,$code) = @_; |
---|
85 | |
---|
86 | my $loc = $source.':'.$lineno.':'; |
---|
87 | $loc .= ' ' if ($lineno<1000); |
---|
88 | $loc .= ' ' if ($lineno<100); |
---|
89 | |
---|
90 | $loc = trim($loc, $loclen); |
---|
91 | $msg = trim($msg, $msglen); |
---|
92 | |
---|
93 | $code =~ s/^\s*//go; |
---|
94 | |
---|
95 | print $loc.' '.$msg.' | '.$code."\n"; |
---|
96 | } |
---|
97 | |
---|
98 | sub print_annotated_message($$$) { |
---|
99 | my ($source,$lineno,$msg) = @_; |
---|
100 | print_trimmed($source, $lineno, $msg, $code{$lineno}); |
---|
101 | } |
---|
102 | |
---|
103 | # -------------------------------------------------------------------------------- |
---|
104 | |
---|
105 | sub parseCoveredLines($\@) { |
---|
106 | my ($gcov, $covered_lines_r) = @_; |
---|
107 | |
---|
108 | my ($lines,$covered,$tests_seen) = (0,0,0); |
---|
109 | open(GCOV,'<'.$gcov) || die "gcov2msg.pl: can't read '$gcov' (Reason: $!)"; |
---|
110 | my $line; |
---|
111 | while (defined ($line = <GCOV>)) { |
---|
112 | if (not $line =~ /^\s*([^\s:][^:]*):\s*([^\s:][^:]*):(.*)$/o) { die "can't parse '$line'"; } |
---|
113 | my ($counter,$lineno,$code) = ($1,$2,$3); |
---|
114 | if ($lineno>0) { |
---|
115 | if ($counter eq '-') { |
---|
116 | $$covered_lines_r[$lineno] = '-'; |
---|
117 | } |
---|
118 | elsif ($counter eq '#####') { |
---|
119 | if ($code =~ /NEED_NO_COV/) { # handle like there was no code here |
---|
120 | $$covered_lines_r[$lineno] = '-'; |
---|
121 | } |
---|
122 | else { |
---|
123 | $lines++; |
---|
124 | $$covered_lines_r[$lineno] = '#'; |
---|
125 | $code{$lineno} = $code; |
---|
126 | } |
---|
127 | } |
---|
128 | else { |
---|
129 | if ($counter =~ /^[0-9]+$/) { |
---|
130 | $lines++; |
---|
131 | $covered++; |
---|
132 | $$covered_lines_r[$lineno] = '+'; |
---|
133 | } |
---|
134 | else { |
---|
135 | die "Invalid counter '$counter' (expected number)"; |
---|
136 | } |
---|
137 | } |
---|
138 | |
---|
139 | if ($code =~ /^void\s+TEST_.*()/g) { |
---|
140 | $tests_seen++; |
---|
141 | } |
---|
142 | } |
---|
143 | } |
---|
144 | close(GCOV); |
---|
145 | |
---|
146 | return ($lines,$covered,$tests_seen); |
---|
147 | } |
---|
148 | |
---|
149 | sub next_uncovered_section_after(\@$$) { |
---|
150 | my ($covered_lines_r,$lines,$after_line) = @_; |
---|
151 | |
---|
152 | my $line = $after_line+1; |
---|
153 | while ($line<$lines) { |
---|
154 | my $type = $$covered_lines_r[$line]; |
---|
155 | if ($type eq '#') { |
---|
156 | my ($first,$last,$loc) = ($line, $line, 0); |
---|
157 | |
---|
158 | LINE: while (1) { |
---|
159 | if ($type eq '+') { last LINE; } # covered -> stop |
---|
160 | if ($type eq '#') { |
---|
161 | $loc++; |
---|
162 | $last = $line; |
---|
163 | } |
---|
164 | ++$line; |
---|
165 | if ($line>=$lines) { last LINE; } |
---|
166 | $type = $$covered_lines_r[$line]; |
---|
167 | } |
---|
168 | |
---|
169 | return ($first,$last,$loc); |
---|
170 | } |
---|
171 | ++$line; |
---|
172 | } |
---|
173 | return (undef,undef,undef); |
---|
174 | |
---|
175 | } |
---|
176 | |
---|
177 | sub collect_gcov_data($$) { |
---|
178 | my ($source,$gcov) = @_; |
---|
179 | |
---|
180 | reset_trim(); |
---|
181 | |
---|
182 | my $cov = $gcov; |
---|
183 | $cov =~ s/\.gcov$/\.cov/g; |
---|
184 | if ($cov eq $gcov) { die "Invalid gcov name '$gcov'"; } |
---|
185 | |
---|
186 | if (not -f $gcov) { |
---|
187 | print "No such file '$gcov' (assuming it belongs to a standard header)\n"; |
---|
188 | return; |
---|
189 | } |
---|
190 | |
---|
191 | my @covered_lines = (); |
---|
192 | my ($lines,$covered,$tests_seen) = parseCoveredLines($gcov,@covered_lines); |
---|
193 | my $size = scalar(@covered_lines); |
---|
194 | |
---|
195 | my $percent = 100*$covered/$lines; |
---|
196 | $percent = int($percent*10)/10; |
---|
197 | |
---|
198 | my $source_name = $source; |
---|
199 | if ($source =~ /\/([^\/]+)$/) { $source_name = $1; } |
---|
200 | |
---|
201 | if ($covered==$lines) { |
---|
202 | print "Full test-coverage for $source_name\n"; |
---|
203 | unlink($gcov); |
---|
204 | } |
---|
205 | else { |
---|
206 | my $summary = "lines=$lines covered=$covered (coverage=$percent%)"; |
---|
207 | |
---|
208 | $verbose==0 || print "collect_gcov_data($gcov): $summary\n"; |
---|
209 | $covered>0 || die "Argh.. collected data for completely uncovered file '$source'"; |
---|
210 | |
---|
211 | if ($tests_seen==0 and $showCoverageForAll==0) { print "$source_name defines no tests. $summary\n"; } |
---|
212 | elsif (not $source_name =~ $showCoverageForFilesMatching) { print "Skipping $source_name by mask. $summary\n"; } |
---|
213 | else { |
---|
214 | my $line = 0; |
---|
215 | my @sections = (); |
---|
216 | |
---|
217 | SECTION: while (1) { |
---|
218 | my ($first,$last,$loc) = next_uncovered_section_after(@covered_lines, $size, $line); |
---|
219 | if (not defined $first) { last SECTION; } |
---|
220 | push @sections, [$first,$last,$loc]; |
---|
221 | $line = $last; |
---|
222 | } |
---|
223 | |
---|
224 | if ($sortBySectionSize==1) { @sections = sort { $$a[2] <=> $$b[2]; } @sections; } |
---|
225 | |
---|
226 | foreach my $sec_r (@sections) { |
---|
227 | my ($first,$last,$loc) = ($$sec_r[0],$$sec_r[1], $$sec_r[2]); |
---|
228 | if ($first==$last) { |
---|
229 | print_annotated_message($source, $first, 'Uncovered line'); |
---|
230 | } |
---|
231 | else { |
---|
232 | print_annotated_message($source, $first, "[start] $loc uncovered lines"); |
---|
233 | print_annotated_message($source, $last, '[end]'); |
---|
234 | } |
---|
235 | } |
---|
236 | |
---|
237 | if ($percent<90) { print "$source_name:0: Warning: Summary $summary\n"; } |
---|
238 | else { print "Summary $source_name: $summary\n"; } |
---|
239 | |
---|
240 | rename($gcov,$cov) || die "Failed to rename '$gcov' -> '$cov' (Reason: $!)"; |
---|
241 | } |
---|
242 | } |
---|
243 | } |
---|
244 | |
---|
245 | # -------------------------------------------------------------------------------- |
---|
246 | |
---|
247 | my @known_source_ext = qw/cxx cpp c/; |
---|
248 | |
---|
249 | sub find_gcda_files($) { |
---|
250 | my ($dir) = @_; |
---|
251 | my @gcda = (); |
---|
252 | opendir(DIR,$dir) || die "can't read directory '$dir' (Reason: $!)"; |
---|
253 | foreach (readdir(DIR)) { |
---|
254 | if ($_ =~ /\.gcda$/o) { push @gcda, $_; } |
---|
255 | } |
---|
256 | closedir(DIR); |
---|
257 | return @gcda; |
---|
258 | } |
---|
259 | sub gcda2code($\@) { |
---|
260 | my ($gcda, $srcdirs_r) = @_; |
---|
261 | |
---|
262 | if (not $gcda =~ /\.gcda$/o) { |
---|
263 | die "wrong file in gcda2code: '$gcda'"; |
---|
264 | } |
---|
265 | my $base = $`; |
---|
266 | foreach my $dir (@$srcdirs_r) { |
---|
267 | foreach (@known_source_ext) { |
---|
268 | my $name = $base.'.'.$_; |
---|
269 | my $full = $dir.'/'.$name; |
---|
270 | if (-f $full) { |
---|
271 | return [$name,$dir]; |
---|
272 | } |
---|
273 | } |
---|
274 | } |
---|
275 | die "Failed to find code file for '$gcda'"; |
---|
276 | } |
---|
277 | |
---|
278 | sub die_usage($) { |
---|
279 | my ($err) = @_; |
---|
280 | print("Usage: gcov2msg.pl [options] directory\n". |
---|
281 | "Options: --srcdirs=dir,dir,dir set sourcedirectories (default is 'directory')\n". |
---|
282 | " --builddir=dir set dir from which build was done (default is 'directory')\n"); |
---|
283 | die "Error: $err\n"; |
---|
284 | } |
---|
285 | |
---|
286 | sub main() { |
---|
287 | my $args = scalar(@ARGV); |
---|
288 | if ($args<1) { die_usage("Missing argument\n"); } |
---|
289 | |
---|
290 | setup(); |
---|
291 | |
---|
292 | my $dir; |
---|
293 | my @srcdirs; |
---|
294 | my $builddir = undef; |
---|
295 | { |
---|
296 | my $srcdirs = undef; |
---|
297 | |
---|
298 | while ($ARGV[0] =~ /^--/) { |
---|
299 | if ($ARGV[0] =~ /^--srcdirs=/) { |
---|
300 | $srcdirs = $'; |
---|
301 | shift @ARGV; |
---|
302 | } |
---|
303 | elsif ($ARGV[0] =~ /^--builddir=/) { |
---|
304 | $builddir = $'; |
---|
305 | shift @ARGV; |
---|
306 | } |
---|
307 | } |
---|
308 | $dir = $ARGV[0]; |
---|
309 | if (not -d $dir) { die "No such directory '$dir'\n"; } |
---|
310 | |
---|
311 | if (not defined $builddir) { $builddir = $dir; } |
---|
312 | if (not defined $srcdirs) { $srcdirs = $dir; } |
---|
313 | @srcdirs = split(',', $srcdirs); |
---|
314 | } |
---|
315 | |
---|
316 | my @gcda = find_gcda_files($dir); |
---|
317 | my %gcda2code = map { $_ => gcda2code($_,@srcdirs); } @gcda; # value=[name,srcdir] |
---|
318 | |
---|
319 | my $olddir = cwd(); |
---|
320 | chdir($dir) || die "can't cd to '$dir' (Reason: $!)\n";; |
---|
321 | |
---|
322 | eval { |
---|
323 | foreach (sort @gcda) { |
---|
324 | my $cs_ref = $gcda2code{$_}; |
---|
325 | my ($code,$srcdir) = @$cs_ref; |
---|
326 | |
---|
327 | my $fullcode = $srcdir.'/'.$code; |
---|
328 | my $objSwitch = ''; |
---|
329 | |
---|
330 | if ($srcdir ne $dir) { |
---|
331 | $objSwitch = " -o '$dir'"; |
---|
332 | } |
---|
333 | |
---|
334 | if ($builddir ne $dir) { |
---|
335 | chdir($builddir) || die "can't cd to '$builddir' (Reason: $!)\n";; |
---|
336 | } |
---|
337 | my $cmd = "gcov '$fullcode' $objSwitch"; |
---|
338 | |
---|
339 | $verbose==0 || print "[Action: $cmd]\n"; |
---|
340 | |
---|
341 | open(CMD,$cmd.'|') || die "failed to fork '$cmd' (Reason: $!)"; |
---|
342 | |
---|
343 | if ($builddir ne $dir) { |
---|
344 | chdir($dir) || die "can't cd to '$dir' (Reason: $!)\n";; |
---|
345 | } |
---|
346 | |
---|
347 | my ($file,$percent,$lines,$source,$gcov) = (undef,undef,undef,undef,undef); |
---|
348 | |
---|
349 | foreach (<CMD>) { |
---|
350 | chomp; |
---|
351 | if ($_ eq '') { ; } # ignore empty lines |
---|
352 | elsif (/^File '(.*)'$/o) { $file = $1; } |
---|
353 | elsif (/^Lines executed:([0-9.]+)% of ([0-9]+)$/o) { |
---|
354 | ($percent,$lines) = ($1,$2); |
---|
355 | } |
---|
356 | elsif (/^([^:]+):creating '(.*)'$/o) { |
---|
357 | ($source,$gcov) = ($1,$2); |
---|
358 | |
---|
359 | if ($percent>0 and $lines>0) { |
---|
360 | if ($source =~ /^\/usr\/include/o) { |
---|
361 | # print "Skipping '$gcov'\n"; |
---|
362 | } |
---|
363 | else { |
---|
364 | my $fullgcov = $gcov; |
---|
365 | if ($dir ne $builddir) { |
---|
366 | $fullgcov = $builddir.'/'.$gcov; |
---|
367 | } |
---|
368 | collect_gcov_data($source,$fullgcov); |
---|
369 | } |
---|
370 | } |
---|
371 | if (-f $gcov) { unlink($gcov); } |
---|
372 | |
---|
373 | ($file,$percent,$lines,$source,$gcov) = (undef,undef,undef,undef,undef); |
---|
374 | } |
---|
375 | else { |
---|
376 | die "can't parse line '$_'"; |
---|
377 | } |
---|
378 | } |
---|
379 | close(CMD) || die "failed to execute '$cmd' (Reason: $! exitcode=$?)"; |
---|
380 | |
---|
381 | -f $_ || die "No such file '$_'"; |
---|
382 | unlink($_); |
---|
383 | } |
---|
384 | }; |
---|
385 | if ($@) { |
---|
386 | my $err = $@; |
---|
387 | chdir($olddir) || print "Failed to resume old working dir '$olddir' (Reason: $!)\n"; |
---|
388 | die "Error: $err\n"; |
---|
389 | } |
---|
390 | } |
---|
391 | |
---|
392 | main(); |
---|