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 155     155   1539 use strict;
  155         311  
  155         4337  
3 155     155   752 use warnings;
  155         283  
  155         6017  
4              
5             our $VERSION = '0.000153';
6              
7 155     155   903 use Carp qw/croak confess/;
  155         314  
  155         9553  
8             our @CARP_NOT = (__PACKAGE__);
9              
10 155     155   992 use Scalar::Util qw/weaken reftype blessed/;
  155         398  
  155         8636  
11 155     155   967 use Test2::Util qw/pkg_to_file/;
  155         364  
  155         7834  
12 155     155   1386 use Test2::Util::Stash qw/parse_symbol slot_to_sig get_symbol get_stash purge_symbol/;
  155         368  
  155         9792  
13 155     155   994 use Test2::Util::Sub qw/gen_accessor gen_reader gen_writer/;
  155         409  
  155         10927  
14              
15             sub new; # Prevent hashbase from giving us 'new';
16 155     155   1192 use Test2::Util::HashBase qw/class parent child _purge_on_destroy _blocked_load _symbols _track sub_tracking call_tracking/;
  155         359  
  155         1319  
17              
18             sub new {
19 86     86 1 4348 my $class = shift;
20              
21 86 100       395 croak "Called new() on a blessed instance, did you mean to call \$control->class->new()?"
22             if blessed($class);
23              
24 85         244 my $self = bless({}, $class);
25              
26 85   50     661 $self->{+SUB_TRACKING} ||= {};
27 85   50     537 $self->{+CALL_TRACKING} ||= [];
28              
29 85         146 my @sets;
30 85         293 while (my $arg = shift @_) {
31 164         315 my $val = shift @_;
32              
33 164 100       969 if ($class->can(uc($arg))) {
34 86         275 $self->{$arg} = $val;
35 86         268 next;
36             }
37              
38 78         338 push @sets => [$arg, $val];
39             }
40              
41             croak "The 'class' field is required"
42 85 100       479 unless $self->{+CLASS};
43              
44 84         249 for my $set (@sets) {
45 78         209 my ($meth, $val) = @$set;
46 78         250 my $type = reftype($val);
47              
48 78 100       612 confess "'$meth' is not a valid constructor argument for $class"
49             unless $self->can($meth);
50              
51 77 100       467 if (!$type) {
    100          
    100          
52 22         44 $self->$meth($val);
53             }
54             elsif($type eq 'HASH') {
55 1         22 $self->$meth(%$val);
56             }
57             elsif($type eq 'ARRAY') {
58 53         272 $self->$meth(@$val);
59             }
60             else {
61 1         131 croak "'$val' is not a valid argument for '$meth'"
62             }
63             }
64              
65 80         285 return $self;
66             }
67              
68             sub _check {
69 341 100   341   1118 return unless $_[0]->{+CHILD};
70 1         84 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       46 ($self->{+_PURGE_ON_DESTROY}) = @_ if @_;
76 12         76 return $self->{+_PURGE_ON_DESTROY};
77             }
78              
79             sub stash {
80 21     21 1 33 my $self = shift;
81 21         66 get_stash($self->{+CLASS});
82             }
83              
84             sub file {
85 20     20 1 53 my $self = shift;
86 20         46 my $file = $self->class;
87 20         74 return pkg_to_file($self->class);
88             }
89              
90             sub block_load {
91 9     9 1 18 my $self = shift;
92 9         20 $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       162 if $INC{$file};
98              
99 8         24 $INC{$file} = __FILE__;
100              
101 8         20 $self->{+_BLOCKED_LOAD} = 1;
102             }
103              
104             my %NEW = (
105             hash => sub {
106 1     1   12 my ($class, %params) = @_;
107 1         6 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 38 my $self = shift;
133 6         15 my ($name, $type) = @_;
134 6         20 $self->_check();
135              
136 6   100     212 my $sub = $NEW{$type}
137             || croak "'$type' is not a known constructor type";
138              
139 4         13 $self->override($name => $sub);
140             }
141              
142             sub add_constructor {
143 9     9 1 19 my $self = shift;
144 9         21 my ($name, $type) = @_;
145 9         23 $self->_check();
146              
147 9   100     207 my $sub = $NEW{$type}
148             || croak "'$type' is not a known constructor type";
149              
150 7         17 $self->add($name => $sub);
151             }
152              
153             sub autoload {
154 13     13 1 31 my $self = shift;
155 13         27 $self->_check();
156 13         29 my $class = $self->class;
157 13         54 my $stash = $self->stash;
158              
159             croak "Class '$class' already has an AUTOLOAD"
160 13 100 100     40 if $stash->{AUTOLOAD} && *{$stash->{AUTOLOAD}}{CODE};
  4         213  
161             croak "Class '$class' already has an can"
162 11 50 100     29 if $stash->{can} && *{$stash->{can}}{CODE};
  2         8  
163              
164             # Weaken this reference so that AUTOLOAD does not prevent its own
165             # destruction.
166 11         38 weaken(my $c = $self);
167              
168 11         22 my ($file, $line) = (__FILE__, __LINE__ + 3);
169 11   100     3236 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         166 $line = __LINE__ + 3;
198 10   50     731 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         1506 local $self->{+_TRACK} = 0;
  10         27  
216 10         38 $self->add(AUTOLOAD => $autoload);
217 10         27 $self->add(can => $can);
218             }
219             }
220              
221             sub before {
222 2     2 1 9 my $self = shift;
223 2         7 my ($name, $sub) = @_;
224 2         9 $self->_check();
225 2         7 my $orig = $self->current($name);
226 2     1   19 $self->_inject({}, $name => sub { $sub->(@_); $orig->(@_) });
  1         18  
  1         4  
227             }
228              
229             sub after {
230 1     1 1 16 my $self = shift;
231 1         4 my ($name, $sub) = @_;
232 1         5 $self->_check();
233 1         4 my $orig = $self->current($name);
234             $self->_inject({}, $name => sub {
235 3     3   25 my @out;
236              
237 3         10 my $want = wantarray;
238              
239 3 100       10 if ($want) {
    100          
240 1         38 @out = $orig->(@_);
241             }
242             elsif(defined $want) {
243 1         13 $out[0] = $orig->(@_);
244             }
245             else {
246 1         4 $orig->(@_);
247             }
248              
249 3         31 $sub->(@_);
250              
251 3 100       22 return @out if $want;
252 2 100       9 return $out[0] if defined $want;
253 1         4 return;
254 1         16 });
255             }
256              
257             sub around {
258 1     1 1 17 my $self = shift;
259 1         3 my ($name, $sub) = @_;
260 1         5 $self->_check();
261 1         4 my $orig = $self->current($name);
262 1     1   14 $self->_inject({}, $name => sub { $sub->($orig, @_) });
  1         14  
263             }
264              
265             sub add {
266 63     63 1 324 my $self = shift;
267 63         145 $self->_check();
268 63         236 $self->_inject({add => 1}, @_);
269             }
270              
271             sub override {
272 39     39 1 963 my $self = shift;
273 39         223 $self->_check();
274 39         241 $self->_inject({}, @_);
275             }
276              
277             sub set {
278 5     5 1 36 my $self = shift;
279 5         12 $self->_check();
280 5         20 $self->_inject({set => 1}, @_);
281             }
282              
283             sub current {
284 182     182 1 459 my $self = shift;
285 182         337 my ($sym) = @_;
286              
287 182         666 return get_symbol($sym, $self->{+CLASS});
288             }
289              
290             sub orig {
291 17     17 1 112 my $self = shift;
292 17         34 my ($sym) = @_;
293              
294 17 100       76 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
295              
296 17 100       211 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     254 croak "Symbol '$sym' is not mocked"
302             unless $ref && @$ref;
303              
304 13         25 my ($orig) = @$ref;
305              
306 13         185 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 14 sub clear_call_tracking { @{shift->{+CALL_TRACKING}} = () }
  1         8  
318              
319             sub clear_sub_tracking {
320 2     2 1 37 my $self = shift;
321              
322 2 100       11 unless (@_) {
323 1         3 %{$self->{+SUB_TRACKING}} = ();
  1         5  
324 1         3 return;
325             }
326              
327 1         3 for my $item (@_) {
328 2         6 delete $self->{+SUB_TRACKING}->{$item};
329             }
330              
331 1         3 return;
332             }
333              
334             sub _parse_inject {
335 147     147   231 my $self = shift;
336 147         293 my ($param, $arg) = @_;
337              
338 147 100       445 if ($param =~ m/^-(.*)$/) {
339 15         41 my $sym = $1;
340 15         58 my $sig = slot_to_sig(reftype($arg));
341 15         27 my $ref = $arg;
342 15         47 return ($sig, $sym, $ref);
343             }
344              
345 132 100 100     893 return ('&', $param, $arg)
346             if ref($arg) && reftype($arg) eq 'CODE';
347              
348 17         44 my ($is, $field, $val);
349              
350 17 100 100     124 if(defined($arg) && !ref($arg) && $arg =~ m/^(rw|ro|wo)$/) {
    100 100        
    50          
351 6         11 $is = $arg;
352 6         9 $field = $param;
353             }
354             elsif (!ref($arg)) {
355 5         11 $val = $arg;
356 5         7 $is = 'val';
357             }
358             elsif (reftype($arg) eq 'HASH') {
359 6   66     23 $field = delete $arg->{field} || $param;
360              
361 6         12 $val = delete $arg->{val};
362 6         10 $is = delete $arg->{is};
363              
364 6 50 66     22 croak "Cannot specify 'is' and 'val' together" if $val && $is;
365              
366 6 50 66     24 $is ||= $val ? 'val' : 'rw';
367              
368 6 50       20 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         24 my $sub;
376 17 100       58 if ($is eq 'rw') {
    100          
    100          
377 4         20 $sub = gen_accessor($field);
378             }
379             elsif ($is eq 'ro') {
380 2         16 $sub = gen_reader($field);
381             }
382             elsif ($is eq 'wo') {
383 2         18 $sub = gen_writer($field);
384             }
385             else { # val
386 9     12   34 $sub = sub { $val };
  12         57  
387             }
388              
389 17         54 return ('&', $param, $sub);
390             }
391              
392             sub _inject {
393 111     111   204 my $self = shift;
394 111         324 my ($params, @pairs) = @_;
395              
396 111         206 my $add = $params->{add};
397 111         203 my $set = $params->{set};
398              
399 111         174 my $class = $self->{+CLASS};
400              
401 111   100     490 $self->{+_SYMBOLS} ||= {};
402 111         188 my $syms = $self->{+_SYMBOLS};
403              
404 111         338 while (my $param = shift @pairs) {
405 147         225 my $arg = shift @pairs;
406 147         372 my ($sig, $sym, $ref) = $self->_parse_inject($param, $arg);
407 147         587 my $orig = $self->current("$sig$sym");
408              
409 147 100 100     776 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 144 50 100     637 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 142   100     836 $syms->{"$sig$sym"} ||= [];
418 142         187 push @{$syms->{"$sig$sym"}} => $orig; # Might be undef, thats expected
  142         341  
419              
420 142 100 66     394 if ($self->{+_TRACK} && $sig eq '&') {
421 3         6 my $sub_tracker = $self->{+SUB_TRACKING};
422 3         5 my $call_tracker = $self->{+CALL_TRACKING};
423 3         5 my $sub = $ref;
424             $ref = sub {
425 5     5   69 my $call = {sub_name => $sym, sub_ref => $sub, args => [@_]};
426 5         10 push @{$sub_tracker->{$param}} => $call;
  5         16  
427 5         8 push @$call_tracker => $call;
428 5         48 goto &$sub;
429 3         17 };
430             }
431              
432 155     155   458619 no strict 'refs';
  155         471  
  155         5990  
433 155     155   962 no warnings 'redefine';
  155         390  
  155         24086  
434 142         212 *{"$class\::$sym"} = $ref;
  142         856  
435             }
436              
437 106         351 return;
438             }
439              
440             sub _set_or_unset {
441 129     129   199 my $self = shift;
442 129         241 my ($symbol, $set) = @_;
443              
444 129         198 my $class = $self->{+CLASS};
445              
446 129 100       367 return purge_symbol($symbol, $class)
447             unless $set;
448              
449 48         158 my $sym = parse_symbol($symbol, $class);
450 155     155   1176 no strict 'refs';
  155         416  
  155         6286  
451 155     155   1010 no warnings 'redefine';
  155         378  
  155         80619  
452 48         119 *{"$class\::$sym->{name}"} = $set;
  48         440  
453             }
454              
455             sub restore {
456 4     4 1 40 my $self = shift;
457 4         10 my ($sym) = @_;
458 4         12 $self->_check();
459              
460 4 50       20 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
461              
462 4 100       85 my $syms = $self->{+_SYMBOLS}
463             or croak "No symbols are mocked";
464              
465 3         8 my $ref = $syms->{$sym};
466              
467 3 100 66     87 croak "Symbol '$sym' is not mocked"
468             unless $ref && @$ref;
469              
470 2         3 my $old = pop @$ref;
471 2 50       8 delete $syms->{$sym} unless @$ref;
472              
473 2         5 return $self->_set_or_unset($sym, $old);
474             }
475              
476             sub reset {
477 129     129 1 257 my $self = shift;
478 129         259 my ($sym) = @_;
479 129         289 $self->_check();
480              
481 129 100       509 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
482              
483 129 100       415 my $syms = $self->{+_SYMBOLS}
484             or croak "No symbols are mocked";
485              
486 128         238 my $ref = delete $syms->{$sym};
487              
488 128 100 66     596 croak "Symbol '$sym' is not mocked"
489             unless $ref && @$ref;
490              
491 127         272 my ($old) = @$ref;
492              
493 127         324 return $self->_set_or_unset($sym, $old);
494             }
495              
496             sub reset_all {
497 58     58 1 121 my $self = shift;
498 58         170 $self->_check();
499              
500 58   50     182 my $syms = $self->{+_SYMBOLS} || return;
501              
502 58         280 $self->reset($_) for keys %$syms;
503              
504 58         178 delete $self->{+_SYMBOLS};
505             }
506              
507             sub _purge {
508 7     7   11 my $self = shift;
509 7         14 my $stash = $self->stash;
510 7         82 delete $stash->{$_} for keys %$stash;
511             }
512              
513             sub DESTROY {
514 80     80   1046 my $self = shift;
515              
516 80         178 delete $self->{+CHILD};
517 80 100       329 $self->reset_all if $self->{+_SYMBOLS};
518              
519 80 100       243 delete $INC{$self->file} if $self->{+_BLOCKED_LOAD};
520              
521 80 100       980 $self->_purge if $self->{+_PURGE_ON_DESTROY};
522             }
523              
524             1;
525              
526             __END__