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   1631 use strict;
  156         322  
  156         4703  
3 156     156   818 use warnings;
  156         358  
  156         6127  
4              
5             our $VERSION = '0.000156';
6              
7 156     156   1263 use Carp qw/croak confess/;
  156         337  
  156         11897  
8             our @CARP_NOT = (__PACKAGE__);
9              
10 156     156   1260 use Scalar::Util qw/weaken reftype blessed/;
  156         395  
  156         9624  
11 156     156   1137 use Test2::Util qw/pkg_to_file/;
  156         339  
  156         8219  
12 156     156   1469 use Test2::Util::Stash qw/parse_symbol slot_to_sig get_symbol get_stash purge_symbol/;
  156         606  
  156         11169  
13 156     156   1423 use Test2::Util::Sub qw/gen_accessor gen_reader gen_writer/;
  156         496  
  156         12562  
14              
15             sub new; # Prevent hashbase from giving us 'new';
16 156     156   1405 use Test2::Util::HashBase qw/class parent child _purge_on_destroy _blocked_load _symbols _track sub_tracking call_tracking/;
  156         349  
  156         1649  
17              
18             sub new {
19 86     86 1 4350 my $class = shift;
20              
21 86 100       426 croak "Called new() on a blessed instance, did you mean to call \$control->class->new()?"
22             if blessed($class);
23              
24 85         359 my $self = bless({}, $class);
25              
26 85   50     716 $self->{+SUB_TRACKING} ||= {};
27 85   50     520 $self->{+CALL_TRACKING} ||= [];
28              
29 85         144 my @sets;
30 85         320 while (my $arg = shift @_) {
31 164         301 my $val = shift @_;
32              
33 164 100       1100 if ($class->can(uc($arg))) {
34 86         260 $self->{$arg} = $val;
35 86         315 next;
36             }
37              
38 78         303 push @sets => [$arg, $val];
39             }
40              
41             croak "The 'class' field is required"
42 85 100       463 unless $self->{+CLASS};
43              
44 84         226 for my $set (@sets) {
45 78         250 my ($meth, $val) = @$set;
46 78         266 my $type = reftype($val);
47              
48 78 100       617 confess "'$meth' is not a valid constructor argument for $class"
49             unless $self->can($meth);
50              
51 77 100       365 if (!$type) {
    100          
    100          
52 22         53 $self->$meth($val);
53             }
54             elsif($type eq 'HASH') {
55 1         4 $self->$meth(%$val);
56             }
57             elsif($type eq 'ARRAY') {
58 53         253 $self->$meth(@$val);
59             }
60             else {
61 1         144 croak "'$val' is not a valid argument for '$meth'"
62             }
63             }
64              
65 80         424 return $self;
66             }
67              
68             sub _check {
69 341 100   341   854 return unless $_[0]->{+CHILD};
70 1         80 croak "There is an active child controller, cannot proceed";
71             }
72              
73             sub purge_on_destroy {
74 12     12 1 32 my $self = shift;
75 12 100       45 ($self->{+_PURGE_ON_DESTROY}) = @_ if @_;
76 12         92 return $self->{+_PURGE_ON_DESTROY};
77             }
78              
79             sub stash {
80 21     21 1 38 my $self = shift;
81 21         72 get_stash($self->{+CLASS});
82             }
83              
84             sub file {
85 20     20 1 70 my $self = shift;
86 20         51 my $file = $self->class;
87 20         88 return pkg_to_file($self->class);
88             }
89              
90             sub block_load {
91 9     9 1 23 my $self = shift;
92 9         24 $self->_check();
93              
94 9         19 my $file = $self->file;
95              
96             croak "Cannot block the loading of module '" . $self->class . "', already loaded in file $INC{$file}"
97 9 100       178 if $INC{$file};
98              
99 8         26 $INC{$file} = __FILE__;
100              
101 8         21 $self->{+_BLOCKED_LOAD} = 1;
102             }
103              
104             my %NEW = (
105             hash => sub {
106 1     1   19 my ($class, %params) = @_;
107 1         10 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 33 my $self = shift;
133 6         19 my ($name, $type) = @_;
134 6         16 $self->_check();
135              
136 6   100     214 my $sub = $NEW{$type}
137             || croak "'$type' is not a known constructor type";
138              
139 4         9 $self->override($name => $sub);
140             }
141              
142             sub add_constructor {
143 9     9 1 38 my $self = shift;
144 9         24 my ($name, $type) = @_;
145 9         24 $self->_check();
146              
147 9   100     281 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 39 my $self = shift;
155 13         33 $self->_check();
156 13         33 my $class = $self->class;
157 13         65 my $stash = $self->stash;
158              
159             croak "Class '$class' already has an AUTOLOAD"
160 13 100 100     44 if $stash->{AUTOLOAD} && *{$stash->{AUTOLOAD}}{CODE};
  4         186  
161             croak "Class '$class' already has an can"
162 11 50 100     66 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         41 weaken(my $c = $self);
167              
168 11         25 my ($file, $line) = (__FILE__, __LINE__ + 3);
169 11   100     3777 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         238 $line = __LINE__ + 3;
198 10   50     819 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         1887 local $self->{+_TRACK} = 0;
  10         40  
216 10         39 $self->add(AUTOLOAD => $autoload);
217 10         51 $self->add(can => $can);
218             }
219             }
220              
221             sub before {
222 2     2 1 13 my $self = shift;
223 2         5 my ($name, $sub) = @_;
224 2         5 $self->_check();
225 2         5 my $orig = $self->current($name);
226 2     1   15 $self->_inject({}, $name => sub { $sub->(@_); $orig->(@_) });
  1         26  
  1         10  
227             }
228              
229             sub after {
230 1     1 1 7 my $self = shift;
231 1         4 my ($name, $sub) = @_;
232 1         6 $self->_check();
233 1         4 my $orig = $self->current($name);
234             $self->_inject({}, $name => sub {
235 3     3   56 my @out;
236              
237 3         10 my $want = wantarray;
238              
239 3 100       11 if ($want) {
    100          
240 1         6 @out = $orig->(@_);
241             }
242             elsif(defined $want) {
243 1         5 $out[0] = $orig->(@_);
244             }
245             else {
246 1         5 $orig->(@_);
247             }
248              
249 3         26 $sub->(@_);
250              
251 3 100       23 return @out if $want;
252 2 100       13 return $out[0] if defined $want;
253 1         2 return;
254 1         10 });
255             }
256              
257             sub around {
258 1     1 1 23 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   7 $self->_inject({}, $name => sub { $sub->($orig, @_) });
  1         12  
263             }
264              
265             sub add {
266 63     63 1 394 my $self = shift;
267 63         155 $self->_check();
268 63         217 $self->_inject({add => 1}, @_);
269             }
270              
271             sub override {
272 39     39 1 863 my $self = shift;
273 39         173 $self->_check();
274 39         243 $self->_inject({}, @_);
275             }
276              
277             sub set {
278 5     5 1 46 my $self = shift;
279 5         14 $self->_check();
280 5         21 $self->_inject({set => 1}, @_);
281             }
282              
283             sub current {
284 182     182 1 542 my $self = shift;
285 182         348 my ($sym) = @_;
286              
287 182         670 return get_symbol($sym, $self->{+CLASS});
288             }
289              
290             sub orig {
291 17     17 1 144 my $self = shift;
292 17         35 my ($sym) = @_;
293              
294 17 100       75 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
295              
296 17 100       198 my $syms = $self->{+_SYMBOLS}
297             or croak "No symbols have been mocked yet";
298              
299 15         25 my $ref = $syms->{$sym};
300              
301 15 100 66     246 croak "Symbol '$sym' is not mocked"
302             unless $ref && @$ref;
303              
304 13         26 my ($orig) = @$ref;
305              
306 13         192 return $orig;
307             }
308              
309             sub track {
310 2     2 1 13 my $self = shift;
311              
312 2 50       14 ($self->{+_TRACK}) = @_ if @_;
313              
314 2         5 return $self->{+_TRACK};
315             }
316              
317 1     1 1 25 sub clear_call_tracking { @{shift->{+CALL_TRACKING}} = () }
  1         7  
318              
319             sub clear_sub_tracking {
320 2     2 1 37 my $self = shift;
321              
322 2 100       8 unless (@_) {
323 1         3 %{$self->{+SUB_TRACKING}} = ();
  1         5  
324 1         4 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   227 my $self = shift;
336 147         278 my ($param, $arg) = @_;
337              
338 147 100       490 if ($param =~ m/^-(.*)$/) {
339 15         35 my $sym = $1;
340 15         57 my $sig = slot_to_sig(reftype($arg));
341 15         29 my $ref = $arg;
342 15         47 return ($sig, $sym, $ref);
343             }
344              
345 132 100 100     934 return ('&', $param, $arg)
346             if ref($arg) && reftype($arg) eq 'CODE';
347              
348 17         44 my ($is, $field, $val);
349              
350 17 100 100     119 if(defined($arg) && !ref($arg) && $arg =~ m/^(rw|ro|wo)$/) {
    100 100        
    50          
351 6         15 $is = $arg;
352 6         8 $field = $param;
353             }
354             elsif (!ref($arg)) {
355 5         9 $val = $arg;
356 5         12 $is = 'val';
357             }
358             elsif (reftype($arg) eq 'HASH') {
359 6   66     22 $field = delete $arg->{field} || $param;
360              
361 6         13 $val = delete $arg->{val};
362 6         9 $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     22 $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         26 my $sub;
376 17 100       63 if ($is eq 'rw') {
    100          
    100          
377 4         17 $sub = gen_accessor($field);
378             }
379             elsif ($is eq 'ro') {
380 2         14 $sub = gen_reader($field);
381             }
382             elsif ($is eq 'wo') {
383 2         14 $sub = gen_writer($field);
384             }
385             else { # val
386 9     12   32 $sub = sub { $val };
  12         54  
387             }
388              
389 17         56 return ('&', $param, $sub);
390             }
391              
392             sub _inject {
393 111     111   208 my $self = shift;
394 111         300 my ($params, @pairs) = @_;
395              
396 111         204 my $add = $params->{add};
397 111         229 my $set = $params->{set};
398              
399 111         212 my $class = $self->{+CLASS};
400              
401 111   100     520 $self->{+_SYMBOLS} ||= {};
402 111         214 my $syms = $self->{+_SYMBOLS};
403              
404 111         295 while (my $param = shift @pairs) {
405 147         226 my $arg = shift @pairs;
406 147         406 my ($sig, $sym, $ref) = $self->_parse_inject($param, $arg);
407 147         582 my $orig = $self->current("$sig$sym");
408              
409 147 100 100     806 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     660 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     763 $syms->{"$sig$sym"} ||= [];
418 142         231 push @{$syms->{"$sig$sym"}} => $orig; # Might be undef, thats expected
  142         370  
419              
420 142 100 66     369 if ($self->{+_TRACK} && $sig eq '&') {
421 3         24 my $sub_tracker = $self->{+SUB_TRACKING};
422 3         5 my $call_tracker = $self->{+CALL_TRACKING};
423 3         6 my $sub = $ref;
424             $ref = sub {
425 5     5   76 my $call = {sub_name => $sym, sub_ref => $sub, args => [@_]};
426 5         7 push @{$sub_tracker->{$param}} => $call;
  5         13  
427 5         10 push @$call_tracker => $call;
428 5         18 goto &$sub;
429 3         17 };
430             }
431              
432 156     156   512392 no strict 'refs';
  156         494  
  156         6958  
433 156     156   1202 no warnings 'redefine';
  156         666  
  156         28278  
434 142         220 *{"$class\::$sym"} = $ref;
  142         1004  
435             }
436              
437 106         464 return;
438             }
439              
440             sub _set_or_unset {
441 129     129   217 my $self = shift;
442 129         240 my ($symbol, $set) = @_;
443              
444 129         220 my $class = $self->{+CLASS};
445              
446 129 100       404 return purge_symbol($symbol, $class)
447             unless $set;
448              
449 48         184 my $sym = parse_symbol($symbol, $class);
450 156     156   1239 no strict 'refs';
  156         408  
  156         5641  
451 156     156   1131 no warnings 'redefine';
  156         338  
  156         90579  
452 48         126 *{"$class\::$sym->{name}"} = $set;
  48         504  
453             }
454              
455             sub restore {
456 4     4 1 50 my $self = shift;
457 4         9 my ($sym) = @_;
458 4         13 $self->_check();
459              
460 4 50       24 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
461              
462 4 100       84 my $syms = $self->{+_SYMBOLS}
463             or croak "No symbols are mocked";
464              
465 3         6 my $ref = $syms->{$sym};
466              
467 3 100 66     87 croak "Symbol '$sym' is not mocked"
468             unless $ref && @$ref;
469              
470 2         4 my $old = pop @$ref;
471 2 50       7 delete $syms->{$sym} unless @$ref;
472              
473 2         8 return $self->_set_or_unset($sym, $old);
474             }
475              
476             sub reset {
477 129     129 1 312 my $self = shift;
478 129         254 my ($sym) = @_;
479 129         305 $self->_check();
480              
481 129 100       541 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
482              
483 129 100       403 my $syms = $self->{+_SYMBOLS}
484             or croak "No symbols are mocked";
485              
486 128         234 my $ref = delete $syms->{$sym};
487              
488 128 100 66     630 croak "Symbol '$sym' is not mocked"
489             unless $ref && @$ref;
490              
491 127         232 my ($old) = @$ref;
492              
493 127         313 return $self->_set_or_unset($sym, $old);
494             }
495              
496             sub reset_all {
497 58     58 1 132 my $self = shift;
498 58         172 $self->_check();
499              
500 58   50     237 my $syms = $self->{+_SYMBOLS} || return;
501              
502 58         332 $self->reset($_) for keys %$syms;
503              
504 58         189 delete $self->{+_SYMBOLS};
505             }
506              
507             sub _purge {
508 7     7   30 my $self = shift;
509 7         18 my $stash = $self->stash;
510 7         111 delete $stash->{$_} for keys %$stash;
511             }
512              
513             sub DESTROY {
514 80     80   1006 my $self = shift;
515              
516 80         160 delete $self->{+CHILD};
517 80 100       379 $self->reset_all if $self->{+_SYMBOLS};
518              
519 80 100       277 delete $INC{$self->file} if $self->{+_BLOCKED_LOAD};
520              
521 80 100       863 $self->_purge if $self->{+_PURGE_ON_DESTROY};
522             }
523              
524             1;
525              
526             __END__