File Coverage

blib/lib/Test2/Plugin/Cover.pm
Criterion Covered Total %
statement 142 161 88.2
branch 53 74 71.6
condition 16 30 53.3
subroutine 27 33 81.8
pod 18 20 90.0
total 256 318 80.5


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