File Coverage

blib/lib/Test2/Plugin/Cover.pm
Criterion Covered Total %
statement 143 162 88.2
branch 54 74 72.9
condition 16 30 53.3
subroutine 27 33 81.8
pod 18 20 90.0
total 258 319 80.8


line stmt bran cond sub pod time code
1             package Test2::Plugin::Cover;
2 7     7   2739 use strict;
  7         34  
  7         165  
3 7     7   30 use warnings;
  7         11  
  7         157  
4              
5 7     7   2898 use Test2::API qw/test2_add_callback_exit context/;
  7         347271  
  7         460  
6 7     7   4576 use Path::Tiny qw/path/;
  7         74541  
  7         330  
7 7     7   3537 use Storable qw/dclone/;
  7         18350  
  7         445  
8 7     7   42 use Carp qw/croak/;
  7         12  
  7         248  
9 7     7   45 use File::Spec();
  7         11  
  7         905  
10              
11             my $SEP = File::Spec->catfile('', '');
12              
13             our $VERSION = '0.000027';
14              
15             # Directly modifying this is a bad idea, but for the XS to work it needs to be
16             # a package var, not a lexical.
17             our $FROM = '*';
18             my $FROM_MODIFIED = 0;
19             my $FROM_MANAGER;
20              
21             our ($ENABLED, $ROOT, $LOAD_ROOT, %REPORT, @OPENS, $TRACE_OPENS);
22             BEGIN {
23 7     7   33 $TRACE_OPENS = 0;
24 7         11 $ENABLED = 0;
25 7         25 $LOAD_ROOT = "" . path('.')->realpath;
26 7         1449 $ROOT = $LOAD_ROOT;
27             }
28              
29             my %FILTER;
30              
31 7     7   41 use XSLoader;
  7         11  
  7         11297  
32             XSLoader::load(__PACKAGE__, $VERSION);
33              
34             1;
35              
36             my $IMPORTED = 0;
37             sub import {
38 8     8   89 my $class = shift;
39 8         17 my %params = @_;
40              
41 8 100       26 if ($params{disabled}) {
42 1         3 $class->disable;
43 1         18 return;
44             }
45              
46 7         19 $class->enable;
47              
48 7 50       20 return if $params{no_event};
49              
50 7 50       18 if ($IMPORTED++) {
51 0 0       0 croak "$class has already been imported, too late to add params" if keys %params;
52 0         0 return;
53             }
54              
55 7         15 $class->reload;
56              
57 7         20 my $ran = 0;
58 7 50       20 $ROOT = "" . $params{root} if $params{root};
59 7 100   14   28 my $callback = sub { return if $ran++; $class->report(%params, ctx => $_[0], root => $ROOT) };
  14         1533  
  7         53  
60              
61 7         46 test2_add_callback_exit($callback);
62              
63             # Fallback if we fork.
64 6 50   6   100709 eval 'END { local $?; $callback->() }; 1' or die $@;
  6         89  
  7         659  
65             }
66              
67             sub reload {
68 7   33 7 1 17 $ROOT = $LOAD_ROOT // "" . path('.')->realpath;
69 7 50       288 %FILTER = map {-f $_ ? ($_ => 1) : ()} $0, __FILE__, File::Spec->rel2abs($0), File::Spec->rel2abs(__FILE__);
  28         384  
70             }
71              
72 4     4 1 17 sub enabled { $ENABLED }
73 9     9 1 14 sub enable { $ENABLED = 1 }
74 2     2 1 5 sub disable { $ENABLED = 0 }
75              
76             sub full_reset {
77 0     0 1 0 reset_from();
78 0         0 reset_coverage();
79             }
80              
81             sub reset_from {
82 0     0 1 0 $FROM = '*';
83 0         0 $FROM_MODIFIED = 0;
84 0         0 $FROM_MANAGER = undef;
85             }
86              
87             sub reset_coverage {
88 10     10 1 25376 %REPORT = ();
89             }
90              
91 0     0 0 0 sub set_root { $ROOT = "" . pop };
92              
93 0   0 0 1 0 sub get_from { $FROM //= '*' }
94 6     6 1 5791 sub set_from { $FROM_MODIFIED++; $FROM = pop }
  6         13  
95 1     1 1 26 sub clear_from { $FROM = '*' }
96 0 0   0 1 0 sub was_from_modified { $FROM_MODIFIED ? 1 : 0 }
97 0     0 1 0 sub set_from_manager { $FROM_MODIFIED++; $FROM_MANAGER = pop }
  0         0  
98              
99             sub touch_data_file {
100 3     3 1 17 my $class = shift;
101 3         5 my ($file, $from) = @_;
102 3 50       8 croak "A file is required" unless $file;
103 3   33     10 $from //= $FROM;
104              
105 3         7 $REPORT{$file}{'<>'}{$from} = $from;
106 3         6 return;
107             }
108              
109             sub touch_source_file {
110 6     6 1 29 my $class = shift;
111 6         10 my ($file, $subs, $from) = @_;
112 6 50       8 croak "A file is required" unless $file;
113              
114 6   100     14 $subs //= ['*'];
115 6 100       14 $subs = [$subs] unless 'ARRAY' eq ref($subs);
116              
117 6   33     15 $from //= $FROM;
118              
119 6         17 $REPORT{$file}{$_}{$from} = $from for @$subs;
120              
121 6         12 return;
122             }
123              
124             sub filter {
125 90     90 1 118 my $class = shift;
126 90         213 my ($file, %params) = @_;
127              
128 90   33     320 my $root = $params{root} // path('.')->realpath;
129 90         263 $root = path($root);
130              
131 90 50       1868 my $path = $INC{$file} ? path($INC{$file}) : path($file);
132 90 100       1779 $path = $path->realpath if $path->exists;
133              
134 90 100       11586 return () unless $root->subsumes($file);
135              
136 41         2678 return $path->relative($root)->stringify();
137             }
138              
139             sub extract {
140 131     131 1 6537 my $class = shift;
141 131         199 my ($file) = @_;
142              
143             # If we opened a file with 2-arg open
144 131         432 $file =~ s/^[\+\-]?(?:>{1,2}|<|\|)[\+\-]?//;
145              
146             # Sometimes things get nested and we need to extract and then extract again...
147 131         172 while (1) {
148             # No hope :-(
149 199 100       439 return if $file =~ m/^\(eval( \d+\)?)$/;
150              
151             # Easy
152 195 100       2273 return $file if -e $file;
153              
154 125         283 my $start = $file;
155              
156             # Moose like to write "blah blah (defined at filename line 123)"
157 125 100       286 $file = $1 if $file =~ m/(?:defined|declared) (?:at|in) (.+) at line \d+/;
158 125 100       255 $file = $1 if $file =~ m/(?:defined|declared) (?:at|in) (.+) line \d+/;
159 125 100       418 $file = $1 if $file =~ m/\(eval \d+\)\[(.+):\d+\]/;
160 125 100       221 $file = $1 if $file =~ m/\((.+)\) line \d+/;
161 125 100       202 $file = $1 if $file =~ m/\((.+)\) at line \d+/;
162              
163             # Extracted everything away
164 125 50       182 return unless $file;
165              
166             # Not going to change anymore
167 125 100       248 last if $file eq $start;
168             }
169              
170             # These characters are rare in file names, but common in calls where files
171             # could not be determined, so we probably failed to extract. If this
172             # assumption is wrong for someone they can write a custom extract, this is
173             # not a bug.
174 57 100       268 return if $file =~ m/([\[\]\(\)]|->|\beval\b)/;
175              
176             # If we have a foo.bar pattern, or a string that contains this platforms
177             # file separator we will condifer it a valid file.
178 50 100 100     370 return $file if $file =~ m/\S+\.\S+$/i || $file =~ m/\Q$SEP\E/;
179              
180 3         12 return;
181             }
182              
183             my %HIDDEN_SUBS = (
184             '__ANON__' => 1,
185             'eval' => 1,
186             );
187              
188             my %SPECIAL_SUBS = (
189             'BEGIN' => 1,
190             'CHECK' => 1,
191             'END' => 1,
192             'INIT' => 1,
193             'UNITCHECK' => 1,
194             );
195              
196             sub files {
197 7     7 1 3564 my $class = shift;
198 7         18 my %params = @_;
199              
200 7         23 my $report = $class->_process(%params);
201              
202 7         61 return [sort keys %$report];
203             }
204              
205             sub data {
206 14     14 0 8450 my $class = shift;
207 14         42 my %params = @_;
208              
209 14         44 my $report = $class->_process(%params);
210              
211 14         41 my $out = {};
212              
213 14         47 for my $file (keys %$report) {
214 21   50     42 my $rval = $report->{$file} // next;
215 21         32 my $oval = $out->{$file} = {};
216              
217 21         35 for my $sub (keys %$rval) {
218 24 50       43 next if $HIDDEN_SUBS{$sub};
219              
220 24 100       34 my $key = $SPECIAL_SUBS{$sub} ? '*' : $sub;
221 24         32 my @add = map { $rval->{$sub}->{$_} } keys %{$rval->{$sub}};
  33         61  
  24         42  
222              
223 24 50       38 if ($oval->{$key}) {
224 0         0 my %seen;
225 0         0 $oval->{$key} = [ sort grep { !$seen{$_}++ } @{$oval->{$key}}, @add ];
  0         0  
  0         0  
226             }
227             else {
228 24         64 $oval->{$key} = [ sort @add ];
229             }
230             }
231             }
232              
233 14         68 return $out;
234             }
235              
236             sub report {
237 7     7 1 25 my $class = shift;
238 7         39 my %params = @_;
239              
240 7         61 my $data = $class->data(%params);
241 7         39 my $details = "This test covered " . scalar(keys %$data) . " source files.";
242 7 100       53 my $type = $FROM_MODIFIED ? 'split' : 'flat';
243              
244 7   33     66 my $ctx = $params{ctx} // context();
245             my $event = $ctx->send_ev2(
246             about => {package => __PACKAGE__, details => $details},
247              
248             coverage => {
249             files => $data,
250             details => $details,
251             test_type => $type,
252             from_manager => $FROM_MANAGER,
253             },
254              
255 7         688 info => [{tag => 'COVERAGE', details => $details, debug => $params{verbose}}],
256             );
257 7 50       3133 $ctx->release unless $params{ctx};
258              
259 7         228 return $event;
260             }
261              
262             sub _process {
263 21     21   46 my $class = shift;
264 21         39 my %params = @_;
265              
266 21         95 my $filter = $class->can('filter');
267 21         60 my $extract = $class->can('extract');
268              
269 21         1292 my $clone = dclone(\%REPORT);
270 21         50 my %report;
271              
272 21         68 for my $raw (keys %$clone) {
273 126 50       4070 next unless $raw;
274 126 100       242 next if $FILTER{$raw};
275              
276 100   100     234 my $file = $class->$extract($raw, %params) // next;
277 97 100       270 next if $FILTER{$file};
278              
279 90   100     204 my $path = $class->$filter($file, %params) // next;
280 41 50       9674 next if $FILTER{$path};
281              
282 41         63 my $from = $clone->{$raw};
283              
284             # Merge
285 41   50     144 my $into = $report{$path} //= {};
286              
287 41         125 for my $sub (keys %$from) {
288 47 50       78 if ($into->{$sub}) {
289 0         0 $into->{$sub} = {%{dclone($into->{$sub})}, %{$from->{$sub}}};
  0         0  
  0         0  
290             }
291             else {
292 47 50       756 $into->{$sub} = dclone($from->{$sub}) if $from->{$sub};
293             }
294             }
295             }
296              
297 21         408 return \%report;
298             }
299              
300             1;
301              
302             __END__