File Coverage

blib/lib/Test2/Plugin/Cover.pm
Criterion Covered Total %
statement 128 147 87.0
branch 49 68 72.0
condition 12 22 54.5
subroutine 25 31 80.6
pod 16 18 88.8
total 230 286 80.4


line stmt bran cond sub pod time code
1             package Test2::Plugin::Cover;
2 6     6   2817 use strict;
  6         35  
  6         196  
3 6     6   31 use warnings;
  6         12  
  6         169  
4              
5 6     6   2897 use Test2::API qw/test2_add_callback_exit context/;
  6         353836  
  6         524  
6 6     6   4726 use Path::Tiny qw/path/;
  6         86672  
  6         394  
7 6     6   3963 use Storable qw/dclone/;
  6         19833  
  6         395  
8 6     6   43 use Carp qw/croak/;
  6         20  
  6         257  
9 6     6   34 use File::Spec();
  6         14  
  6         728  
10              
11             my $SEP = File::Spec->catfile('', '');
12              
13             our $VERSION = '0.000025';
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 6     6   21 $ENABLED = 0;
24 6         40 $LOAD_ROOT = "" . path('.')->realpath;
25 6         1479 $ROOT = $LOAD_ROOT;
26             }
27              
28             my %FILTER;
29              
30 6     6   39 use XSLoader;
  6         12  
  6         10728  
31             XSLoader::load(__PACKAGE__, $VERSION);
32              
33             1;
34              
35             my $IMPORTED = 0;
36             sub import {
37 7     7   43 my $class = shift;
38 7         17 my %params = @_;
39              
40 7 100       35 if ($params{disabled}) {
41 1         4 $class->disable;
42 1         2 return;
43             }
44              
45 6         21 $class->enable;
46              
47 6 50       24 return if $params{no_event};
48              
49 6 50       17 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 6         18 $class->reload;
55              
56 6         21 my $ran = 0;
57 6 50       20 $ROOT = "" . $params{root} if $params{root};
58 6 100   12   61 my $callback = sub { return if $ran++; $class->report(%params, ctx => $_[0], root => $ROOT) };
  12         1522  
  6         67  
59              
60 6         41 test2_add_callback_exit($callback);
61              
62             # Fallback if we fork.
63 5 50   5   118840 eval 'END { local $?; $callback->() }; 1' or die $@;
  5         58  
  6         715  
64             }
65              
66             sub reload {
67 6   33 6 1 19 $ROOT = $LOAD_ROOT // "" . path('.')->realpath;
68 6 50       307 %FILTER = map {-f $_ ? ($_ => 1) : ()} $0, __FILE__, File::Spec->rel2abs($0), File::Spec->rel2abs(__FILE__);
  24         391  
69             }
70              
71 4     4 1 37 sub enabled { $ENABLED }
72 8     8 1 18 sub enable { $ENABLED = 1 }
73 2     2 1 7 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 8     8 1 22375 %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 4     4 1 235 sub set_from { $FROM_MODIFIED++; $FROM = pop }
  4         10  
94 1     1 1 23 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 filter {
99 68     68 1 114 my $class = shift;
100 68         189 my ($file, %params) = @_;
101              
102 68   33     255 my $root = $params{root} // path('.')->realpath;
103 68         217 $root = path($root);
104              
105 68 50       1841 my $path = $INC{$file} ? path($INC{$file}) : path($file);
106 68 100       2108 $path = $path->realpath if $path->exists;
107              
108 68 100       11896 return () unless $root->subsumes($file);
109              
110 21         1859 return $path->relative($root)->stringify();
111             }
112              
113             sub extract {
114 108     108 1 8066 my $class = shift;
115 108         214 my ($file) = @_;
116              
117             # If we opened a file with 2-arg open
118 108         421 $file =~ s/^[\+\-]?(?:>{1,2}|<|\|)[\+\-]?//;
119              
120             # Sometimes things get nested and we need to extract and then extract again...
121 108         202 while (1) {
122             # No hope :-(
123 173 100       475 return if $file =~ m/^\(eval( \d+\)?)$/;
124              
125             # Easy
126 169 100       2334 return $file if -e $file;
127              
128 104         267 my $start = $file;
129              
130             # Moose like to write "blah blah (defined at filename line 123)"
131 104 100       272 $file = $1 if $file =~ m/(?:defined|declared) (?:at|in) (.+) at line \d+/;
132 104 100       264 $file = $1 if $file =~ m/(?:defined|declared) (?:at|in) (.+) line \d+/;
133 104 100       462 $file = $1 if $file =~ m/\(eval \d+\)\[(.+):\d+\]/;
134 104 100       247 $file = $1 if $file =~ m/\((.+)\) line \d+/;
135 104 100       200 $file = $1 if $file =~ m/\((.+)\) at line \d+/;
136              
137             # Extracted everything away
138 104 50       199 return unless $file;
139              
140             # Not going to change anymore
141 104 100       240 last if $file eq $start;
142             }
143              
144             # These characters are rare in file names, but common in calls where files
145             # could not be determined, so we probably failed to extract. If this
146             # assumption is wrong for someone they can write a custom extract, this is
147             # not a bug.
148 39 100       240 return if $file =~ m/([\[\]\(\)]|->|\beval\b)/;
149              
150             # If we have a foo.bar pattern, or a string that contains this platforms
151             # file separator we will condifer it a valid file.
152 32 100 100     330 return $file if $file =~ m/\S+\.\S+$/i || $file =~ m/\Q$SEP\E/;
153              
154 3         14 return;
155             }
156              
157             my %HIDDEN_SUBS = (
158             '__ANON__' => 1,
159             'eval' => 1,
160             );
161              
162             my %SPECIAL_SUBS = (
163             'BEGIN' => 1,
164             'CHECK' => 1,
165             'END' => 1,
166             'INIT' => 1,
167             'UNITCHECK' => 1,
168             );
169              
170             sub files {
171 6     6 1 842 my $class = shift;
172 6         18 my %params = @_;
173              
174 6         20 my $report = $class->_process(%params);
175              
176 6         66 return [sort keys %$report];
177             }
178              
179             sub data {
180 12     12 0 7807 my $class = shift;
181 12         35 my %params = @_;
182              
183 12         41 my $report = $class->_process(%params);
184              
185 12         38 my $out = {};
186              
187 12         44 for my $file (keys %$report) {
188 11   50     29 my $rval = $report->{$file} // next;
189 11         20 my $oval = $out->{$file} = {};
190              
191 11         26 for my $sub (keys %$rval) {
192 11 50       24 next if $HIDDEN_SUBS{$sub};
193              
194 11 50       27 my $key = $SPECIAL_SUBS{$sub} ? '*' : $sub;
195 11         14 my @add = map { $rval->{$sub}->{$_} } keys %{$rval->{$sub}};
  20         47  
  11         29  
196              
197 11 50       25 if ($oval->{$key}) {
198 0         0 my %seen;
199 0         0 $oval->{$key} = [ sort grep { !$seen{$_}++ } @{$oval->{$key}}, @add ];
  0         0  
  0         0  
200             }
201             else {
202 11         48 $oval->{$key} = [ sort @add ];
203             }
204             }
205             }
206              
207 12         73 return $out;
208             }
209              
210             sub report {
211 6     6 1 19 my $class = shift;
212 6         46 my %params = @_;
213              
214 6         53 my $data = $class->data(%params);
215 6         37 my $details = "This test covered " . scalar(keys %$data) . " source files.";
216 6 100       34 my $type = $FROM_MODIFIED ? 'split' : 'flat';
217              
218 6   33     50 my $ctx = $params{ctx} // context();
219             my $event = $ctx->send_ev2(
220             about => {package => __PACKAGE__, details => $details},
221              
222             coverage => {
223             files => $data,
224             details => $details,
225             test_type => $type,
226             from_manager => $FROM_MANAGER,
227             },
228              
229 6         694 info => [{tag => 'COVERAGE', details => $details, debug => $params{verbose}}],
230             );
231 6 50       3356 $ctx->release unless $params{ctx};
232              
233 6         230 return $event;
234             }
235              
236             sub _process {
237 18     18   43 my $class = shift;
238 18         44 my %params = @_;
239              
240 18         95 my $filter = $class->can('filter');
241 18         66 my $extract = $class->can('extract');
242              
243 18         1322 my $clone = dclone(\%REPORT);
244 18         52 my %report;
245              
246 18         77 for my $raw (keys %$clone) {
247 99 50       4797 next unless $raw;
248 99 100       244 next if $FILTER{$raw};
249              
250 77   100     224 my $file = $class->$extract($raw, %params) // next;
251 74 100       258 next if $FILTER{$file};
252              
253 68   100     192 my $path = $class->$filter($file, %params) // next;
254 21 50       5972 next if $FILTER{$path};
255              
256 21         42 my $from = $clone->{$raw};
257              
258             # Merge
259 21   50     110 my $into = $report{$path} //= {};
260              
261 21         76 for my $sub (keys %$from) {
262 21 50       46 if ($into->{$sub}) {
263 0         0 $into->{$sub} = {%{dclone($into->{$sub})}, %{$from->{$sub}}};
  0         0  
  0         0  
264             }
265             else {
266 21 50       475 $into->{$sub} = dclone($from->{$sub}) if $from->{$sub};
267             }
268             }
269             }
270              
271 18         350 return \%report;
272             }
273              
274             1;
275              
276             __END__