File Coverage

blib/lib/Test2/Mock.pm
Criterion Covered Total %
statement 261 262 99.6
branch 83 92 90.2
condition 62 75 82.6
subroutine 46 46 100.0
pod 22 22 100.0
total 474 497 95.3


line stmt bran cond sub pod time code
1             package Test2::Mock;
2 156     156   1542 use strict;
  156         669  
  156         4438  
3 156     156   774 use warnings;
  156         313  
  156         6427  
4              
5             our $VERSION = '0.000155';
6              
7 156     156   874 use Carp qw/croak confess/;
  156         354  
  156         10354  
8             our @CARP_NOT = (__PACKAGE__);
9              
10 156     156   1303 use Scalar::Util qw/weaken reftype blessed/;
  156         347  
  156         8830  
11 156     156   999 use Test2::Util qw/pkg_to_file/;
  156         471  
  156         8250  
12 156     156   1528 use Test2::Util::Stash qw/parse_symbol slot_to_sig get_symbol get_stash purge_symbol/;
  156         362  
  156         10483  
13 156     156   1196 use Test2::Util::Sub qw/gen_accessor gen_reader gen_writer/;
  156         408  
  156         11677  
14              
15             sub new; # Prevent hashbase from giving us 'new';
16 156     156   1086 use Test2::Util::HashBase qw/class parent child _purge_on_destroy _blocked_load _symbols _track sub_tracking call_tracking/;
  156         397  
  156         1690  
17              
18             sub new {
19 78     78 1 4356 my $class = shift;
20              
21 78 100       454 croak "Called new() on a blessed instance, did you mean to call \$control->class->new()?"
22             if blessed($class);
23              
24 77         264 my $self = bless({}, $class);
25              
26 77   50     885 $self->{+SUB_TRACKING} ||= {};
27 77   50     554 $self->{+CALL_TRACKING} ||= [];
28              
29 77         146 my @sets;
30 77         339 while (my $arg = shift @_) {
31 148         311 my $val = shift @_;
32              
33 148 100       961 if ($class->can(uc($arg))) {
34 78         238 $self->{$arg} = $val;
35 78         308 next;
36             }
37              
38 70         354 push @sets => [$arg, $val];
39             }
40              
41             croak "The 'class' field is required"
42 77 100       453 unless $self->{+CLASS};
43              
44 76         227 for my $set (@sets) {
45 70         257 my ($meth, $val) = @$set;
46 70         344 my $type = reftype($val);
47              
48 70 100       664 confess "'$meth' is not a valid constructor argument for $class"
49             unless $self->can($meth);
50              
51 69 100       435 if (!$type) {
    100          
    100          
52 22         72 $self->$meth($val);
53             }
54             elsif($type eq 'HASH') {
55 1         5 $self->$meth(%$val);
56             }
57             elsif($type eq 'ARRAY') {
58 45         318 $self->$meth(@$val);
59             }
60             else {
61 1         137 croak "'$val' is not a valid argument for '$meth'"
62             }
63             }
64              
65 72         281 return $self;
66             }
67              
68             sub _check {
69 317 100   317   807 return unless $_[0]->{+CHILD};
70 1         83 croak "There is an active child controller, cannot proceed";
71             }
72              
73             sub purge_on_destroy {
74 12     12 1 27 my $self = shift;
75 12 100       64 ($self->{+_PURGE_ON_DESTROY}) = @_ if @_;
76 12         85 return $self->{+_PURGE_ON_DESTROY};
77             }
78              
79             sub stash {
80 21     21 1 51 my $self = shift;
81 21         64 get_stash($self->{+CLASS});
82             }
83              
84             sub file {
85 20     20 1 60 my $self = shift;
86 20         59 my $file = $self->class;
87 20         109 return pkg_to_file($self->class);
88             }
89              
90             sub block_load {
91 9     9 1 22 my $self = shift;
92 9         27 $self->_check();
93              
94 9         22 my $file = $self->file;
95              
96             croak "Cannot block the loading of module '" . $self->class . "', already loaded in file $INC{$file}"
97 9 100       190 if $INC{$file};
98              
99 8         26 $INC{$file} = __FILE__;
100              
101 8         31 $self->{+_BLOCKED_LOAD} = 1;
102             }
103              
104             my %NEW = (
105             hash => sub {
106 1     1   12 my ($class, %params) = @_;
107 1         9 return bless \%params, $class;
108             },
109             array => sub {
110             my ($class, @params) = @_;
111             return bless \@params, $class;
112             },
113             ref => sub {
114             my ($class, $params) = @_;
115             return bless $params, $class;
116             },
117             ref_copy => sub {
118             my ($class, $params) = @_;
119             my $type = reftype($params);
120              
121             return bless {%$params}, $class
122             if $type eq 'HASH';
123              
124             return bless [@$params], $class
125             if $type eq 'ARRAY';
126              
127             croak "Not sure how to construct an '$class' from '$params'";
128             },
129             );
130              
131             sub override_constructor {
132 6     6 1 42 my $self = shift;
133 6         15 my ($name, $type) = @_;
134 6         20 $self->_check();
135              
136 6   100     226 my $sub = $NEW{$type}
137             || croak "'$type' is not a known constructor type";
138              
139 4         16 $self->override($name => $sub);
140             }
141              
142             sub add_constructor {
143 9     9 1 23 my $self = shift;
144 9         20 my ($name, $type) = @_;
145 9         30 $self->_check();
146              
147 9   100     220 my $sub = $NEW{$type}
148             || croak "'$type' is not a known constructor type";
149              
150 7         18 $self->add($name => $sub);
151             }
152              
153             sub autoload {
154 13     13 1 37 my $self = shift;
155 13         37 $self->_check();
156 13         35 my $class = $self->class;
157 13         67 my $stash = $self->stash;
158              
159             croak "Class '$class' already has an AUTOLOAD"
160 13 100 100     46 if $stash->{AUTOLOAD} && *{$stash->{AUTOLOAD}}{CODE};
  4         197  
161             croak "Class '$class' already has an can"
162 11 50 100     39 if $stash->{can} && *{$stash->{can}}{CODE};
  2         10  
163              
164             # Weaken this reference so that AUTOLOAD does not prevent its own
165             # destruction.
166 11         54 weaken(my $c = $self);
167              
168 11         28 my ($file, $line) = (__FILE__, __LINE__ + 3);
169 11   100     4067 my $autoload = eval <
170             package $class;
171             #line $line "$file (Generated AUTOLOAD)"
172             our \$AUTOLOAD;
173             sub {
174             my (\$self) = \@_;
175             my (\$pkg, \$name) = (\$AUTOLOAD =~ m/^(.*)::([^:]+)\$/g);
176             \$AUTOLOAD = undef;
177              
178             return if \$name eq 'DESTROY';
179             my \$sub = sub {
180             my \$self = shift;
181             (\$self->{\$name}) = \@_ if \@_;
182             return \$self->{\$name};
183             };
184              
185             \$c->add(\$name => \$sub);
186              
187             if (\$c->{_track}) {
188             my \$call = {sub_name => \$name, sub_ref => \$sub, args => [\@_]};
189             push \@{\$c->{sub_tracking}->{\$name}} => \$call;
190             push \@{\$c->{call_tracking}} => \$call;
191             }
192              
193             goto &\$sub;
194             }
195             EOT
196              
197 10         223 $line = __LINE__ + 3;
198 10   50     854 my $can = eval <
199             package $class;
200             #line $line "$file (Generated can)"
201             use Scalar::Util 'reftype';
202             sub {
203             my (\$self, \$meth) = \@_;
204             if (\$self->SUPER::can(\$meth)) {
205             return \$self->SUPER::can(\$meth);
206             }
207             elsif (ref \$self && reftype \$self eq 'HASH' && exists \$self->{\$meth}) {
208             return sub { shift->\$meth(\@_) };
209             }
210             return undef;
211             }
212             EOT
213              
214             {
215 10         1839 local $self->{+_TRACK} = 0;
  10         33  
216 10         47 $self->add(AUTOLOAD => $autoload);
217 10         53 $self->add(can => $can);
218             }
219             }
220              
221             sub before {
222 2     2 1 16 my $self = shift;
223 2         7 my ($name, $sub) = @_;
224 2         9 $self->_check();
225 2         49 my $orig = $self->current($name);
226 2     1   24 $self->_inject({}, $name => sub { $sub->(@_); $orig->(@_) });
  1         30  
  1         7  
227             }
228              
229             sub after {
230 1     1 1 12 my $self = shift;
231 1         5 my ($name, $sub) = @_;
232 1         4 $self->_check();
233 1         6 my $orig = $self->current($name);
234             $self->_inject({}, $name => sub {
235 3     3   21 my @out;
236              
237 3         7 my $want = wantarray;
238              
239 3 100       12 if ($want) {
    100          
240 1         14 @out = $orig->(@_);
241             }
242             elsif(defined $want) {
243 1         4 $out[0] = $orig->(@_);
244             }
245             else {
246 1         5 $orig->(@_);
247             }
248              
249 3         28 $sub->(@_);
250              
251 3 100       22 return @out if $want;
252 2 100       9 return $out[0] if defined $want;
253 1         2 return;
254 1         14 });
255             }
256              
257             sub around {
258 1     1 1 10 my $self = shift;
259 1         4 my ($name, $sub) = @_;
260 1         4 $self->_check();
261 1         3 my $orig = $self->current($name);
262 1     1   9 $self->_inject({}, $name => sub { $sub->($orig, @_) });
  1         13  
263             }
264              
265             sub add {
266 63     63 1 353 my $self = shift;
267 63         171 $self->_check();
268 63         243 $self->_inject({add => 1}, @_);
269             }
270              
271             sub override {
272 31     31 1 850 my $self = shift;
273 31         219 $self->_check();
274 31         300 $self->_inject({}, @_);
275             }
276              
277             sub set {
278 5     5 1 47 my $self = shift;
279 5         15 $self->_check();
280 5         21 $self->_inject({set => 1}, @_);
281             }
282              
283             sub current {
284 174     174 1 460 my $self = shift;
285 174         316 my ($sym) = @_;
286              
287 174         793 return get_symbol($sym, $self->{+CLASS});
288             }
289              
290             sub orig {
291 17     17 1 135 my $self = shift;
292 17         36 my ($sym) = @_;
293              
294 17 100       74 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
295              
296 17 100       191 my $syms = $self->{+_SYMBOLS}
297             or croak "No symbols have been mocked yet";
298              
299 15         29 my $ref = $syms->{$sym};
300              
301 15 100 66     230 croak "Symbol '$sym' is not mocked"
302             unless $ref && @$ref;
303              
304 13         25 my ($orig) = @$ref;
305              
306 13         181 return $orig;
307             }
308              
309             sub track {
310 2     2 1 4 my $self = shift;
311              
312 2 50       10 ($self->{+_TRACK}) = @_ if @_;
313              
314 2         5 return $self->{+_TRACK};
315             }
316              
317 1     1 1 18 sub clear_call_tracking { @{shift->{+CALL_TRACKING}} = () }
  1         9  
318              
319             sub clear_sub_tracking {
320 2     2 1 34 my $self = shift;
321              
322 2 100       8 unless (@_) {
323 1         3 %{$self->{+SUB_TRACKING}} = ();
  1         5  
324 1         3 return;
325             }
326              
327 1         4 for my $item (@_) {
328 2         6 delete $self->{+SUB_TRACKING}->{$item};
329             }
330              
331 1         3 return;
332             }
333              
334             sub _parse_inject {
335 139     139   253 my $self = shift;
336 139         338 my ($param, $arg) = @_;
337              
338 139 100       567 if ($param =~ m/^-(.*)$/) {
339 15         67 my $sym = $1;
340 15         69 my $sig = slot_to_sig(reftype($arg));
341 15         29 my $ref = $arg;
342 15         50 return ($sig, $sym, $ref);
343             }
344              
345 124 100 100     989 return ('&', $param, $arg)
346             if ref($arg) && reftype($arg) eq 'CODE';
347              
348 17         28 my ($is, $field, $val);
349              
350 17 100 100     130 if(defined($arg) && !ref($arg) && $arg =~ m/^(rw|ro|wo)$/) {
    100 100        
    50          
351 6         12 $is = $arg;
352 6         9 $field = $param;
353             }
354             elsif (!ref($arg)) {
355 5         10 $val = $arg;
356 5         10 $is = 'val';
357             }
358             elsif (reftype($arg) eq 'HASH') {
359 6   66     25 $field = delete $arg->{field} || $param;
360              
361 6         15 $val = delete $arg->{val};
362 6         11 $is = delete $arg->{is};
363              
364 6 50 66     21 croak "Cannot specify 'is' and 'val' together" if $val && $is;
365              
366 6 50 66     24 $is ||= $val ? 'val' : 'rw';
367              
368 6 50       18 croak "The following keys are not valid when defining a mocked sub with a hashref: " . join(", " => keys %$arg)
369             if keys %$arg;
370             }
371             else {
372 0         0 confess "'$arg' is not a valid argument when defining a mocked sub";
373             }
374              
375 17         25 my $sub;
376 17 100       58 if ($is eq 'rw') {
    100          
    100          
377 4         16 $sub = gen_accessor($field);
378             }
379             elsif ($is eq 'ro') {
380 2         15 $sub = gen_reader($field);
381             }
382             elsif ($is eq 'wo') {
383 2         15 $sub = gen_writer($field);
384             }
385             else { # val
386 9     12   34 $sub = sub { $val };
  12         57  
387             }
388              
389 17         55 return ('&', $param, $sub);
390             }
391              
392             sub _inject {
393 103     103   180 my $self = shift;
394 103         366 my ($params, @pairs) = @_;
395              
396 103         240 my $add = $params->{add};
397 103         192 my $set = $params->{set};
398              
399 103         220 my $class = $self->{+CLASS};
400              
401 103   100     561 $self->{+_SYMBOLS} ||= {};
402 103         202 my $syms = $self->{+_SYMBOLS};
403              
404 103         318 while (my $param = shift @pairs) {
405 139         227 my $arg = shift @pairs;
406 139         466 my ($sig, $sym, $ref) = $self->_parse_inject($param, $arg);
407 139         546 my $orig = $self->current("$sig$sym");
408              
409 139 100 100     936 croak "Cannot override '$sig$class\::$sym', symbol is not already defined"
      100        
      100        
      100        
410             unless $orig || $add || $set || ($sig eq '&' && $class->can($sym));
411              
412             # Cannot be too sure about scalars in globs
413 136 50 100     616 croak "Cannot add '$sig$class\::$sym', symbol is already defined"
      66        
      66        
414             if $add && $orig
415             && (reftype($orig) ne 'SCALAR' || defined($$orig));
416              
417 134   100     915 $syms->{"$sig$sym"} ||= [];
418 134         222 push @{$syms->{"$sig$sym"}} => $orig; # Might be undef, thats expected
  134         336  
419              
420 134 100 66     385 if ($self->{+_TRACK} && $sig eq '&') {
421 3         8 my $sub_tracker = $self->{+SUB_TRACKING};
422 3         7 my $call_tracker = $self->{+CALL_TRACKING};
423 3         6 my $sub = $ref;
424             $ref = sub {
425 5     5   89 my $call = {sub_name => $sym, sub_ref => $sub, args => [@_]};
426 5         11 push @{$sub_tracker->{$param}} => $call;
  5         14  
427 5         11 push @$call_tracker => $call;
428 5         19 goto &$sub;
429 3         20 };
430             }
431              
432 156     156   499021 no strict 'refs';
  156         534  
  156         6250  
433 156     156   1073 no warnings 'redefine';
  156         403  
  156         26322  
434 134         198 *{"$class\::$sym"} = $ref;
  134         911  
435             }
436              
437 98         363 return;
438             }
439              
440             sub _set_or_unset {
441 121     121   193 my $self = shift;
442 121         238 my ($symbol, $set) = @_;
443              
444 121         216 my $class = $self->{+CLASS};
445              
446 121 100       365 return purge_symbol($symbol, $class)
447             unless $set;
448              
449 40         166 my $sym = parse_symbol($symbol, $class);
450 156     156   1221 no strict 'refs';
  156         476  
  156         7227  
451 156     156   1012 no warnings 'redefine';
  156         364  
  156         86044  
452 40         112 *{"$class\::$sym->{name}"} = $set;
  40         385  
453             }
454              
455             sub restore {
456 4     4 1 48 my $self = shift;
457 4         10 my ($sym) = @_;
458 4         13 $self->_check();
459              
460 4 50       19 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
461              
462 4 100       83 my $syms = $self->{+_SYMBOLS}
463             or croak "No symbols are mocked";
464              
465 3         7 my $ref = $syms->{$sym};
466              
467 3 100 66     84 croak "Symbol '$sym' is not mocked"
468             unless $ref && @$ref;
469              
470 2         4 my $old = pop @$ref;
471 2 50       6 delete $syms->{$sym} unless @$ref;
472              
473 2         6 return $self->_set_or_unset($sym, $old);
474             }
475              
476             sub reset {
477 121     121 1 234 my $self = shift;
478 121         239 my ($sym) = @_;
479 121         310 $self->_check();
480              
481 121 100       502 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
482              
483 121 100       378 my $syms = $self->{+_SYMBOLS}
484             or croak "No symbols are mocked";
485              
486 120         225 my $ref = delete $syms->{$sym};
487              
488 120 100 66     593 croak "Symbol '$sym' is not mocked"
489             unless $ref && @$ref;
490              
491 119         239 my ($old) = @$ref;
492              
493 119         302 return $self->_set_or_unset($sym, $old);
494             }
495              
496             sub reset_all {
497 50     50 1 144 my $self = shift;
498 50         166 $self->_check();
499              
500 50   50     159 my $syms = $self->{+_SYMBOLS} || return;
501              
502 50         270 $self->reset($_) for keys %$syms;
503              
504 50         187 delete $self->{+_SYMBOLS};
505             }
506              
507             sub _purge {
508 7     7   12 my $self = shift;
509 7         15 my $stash = $self->stash;
510 7         103 delete $stash->{$_} for keys %$stash;
511             }
512              
513             sub DESTROY {
514 72     72   1241 my $self = shift;
515              
516 72         176 delete $self->{+CHILD};
517 72 100       393 $self->reset_all if $self->{+_SYMBOLS};
518              
519 72 100       267 delete $INC{$self->file} if $self->{+_BLOCKED_LOAD};
520              
521 72 100       913 $self->_purge if $self->{+_PURGE_ON_DESTROY};
522             }
523              
524             1;
525              
526             __END__