File Coverage

blib/lib/Test/Stream/Mock.pm
Criterion Covered Total %
statement 248 248 100.0
branch 85 92 92.3
condition 41 49 83.6
subroutine 44 44 100.0
pod 18 18 100.0
total 436 451 96.6


line stmt bran cond sub pod time code
1             package Test::Stream::Mock;
2 95     95   1223 use strict;
  95         190  
  95         2764  
3 95     95   487 use warnings;
  95         320  
  95         2958  
4              
5 95     95   500 use Scalar::Util qw/weaken reftype blessed/;
  95         192  
  95         6005  
6              
7 95     95   601 use Carp qw/croak confess/;
  95         205  
  95         7559  
8             our @CARP_NOT = (__PACKAGE__, 'Test::Stream::Mock', 'Test::Stream::Workflow');
9              
10 95     95   545 use Test::Stream::Util qw/parse_symbol slot_to_sig pkg_to_file/;
  95         289  
  95         678  
11              
12             use Test::Stream::HashBase(
13 95         921 accessors => [qw/class parent child _purge_on_destroy _blocked_load _symbols/],
14             no_new => 1,
15 95     95   570 );
  95         244  
16              
17             sub new {
18 91     91 1 458 my $class = shift;
19              
20 91 100       455 croak "Called new() on a blessed instance, did you mean to call \$control->class->new()?"
21             if blessed($class);
22              
23 90         186 my $self = bless({}, $class);
24              
25 90         129 my @sets;
26 90         267 while (my $arg = shift @_) {
27 213         315 my $val = shift @_;
28              
29 213 100       1298 if ($class->can(uc($arg))) {
30 91         288 $self->{$arg} = $val;
31 91         319 next;
32             }
33              
34 122         477 push @sets => [$arg, $val];
35             }
36              
37             croak "The 'class' field is required"
38 90 100       431 unless $self->{+CLASS};
39              
40 89         188 for my $set (@sets) {
41 122         310 my ($meth, $val) = @$set;
42 122         331 my $type = reftype($val);
43              
44 122 100       799 confess "'$meth' is not a valid constructor argument for $class"
45             unless $self->can($meth);
46              
47 121 100       365 if (!$type) {
    100          
    100          
48 78         213 $self->$meth($val);
49             }
50             elsif($type eq 'HASH') {
51 7         37 $self->$meth(%$val);
52             }
53             elsif($type eq 'ARRAY') {
54 35         122 $self->$meth(@$val);
55             }
56             else {
57 1         152 croak "'$val' is not a valid argument for '$meth'"
58             }
59             }
60              
61 85         335 return $self;
62             }
63              
64             sub _check {
65 426 100   426   1318 return unless $_[0]->{+CHILD};
66 1         132 croak "There is an active child controller, cannot proceed";
67             }
68              
69             sub purge_on_destroy {
70 32     32 1 56 my $self = shift;
71 32 100       119 ($self->{+_PURGE_ON_DESTROY}) = @_ if @_;
72 32         86 return $self->{+_PURGE_ON_DESTROY};
73             }
74              
75             sub stash {
76 361     361 1 561 my $self = shift;
77 361         519 my $class = $self->{+CLASS};
78              
79 95     95   583 no strict 'refs';
  95         208  
  95         106469  
80 361         399 return \%{"${class}\::"};
  361         1377  
81             }
82              
83             sub file {
84 53     53 1 84 my $self = shift;
85 53         177 my $file = $self->class;
86 53         281 return pkg_to_file($self->class);
87             }
88              
89             sub block_load {
90 29     29 1 47 my $self = shift;
91 29         84 $self->_check();
92              
93 29         80 my $file = $self->file;
94              
95             croak "Cannot block the loading of module '" . $self->class . "', already loaded in file $INC{$file}"
96 29 100       110 if $INC{$file};
97              
98 28         80 $INC{$file} = __FILE__;
99              
100 28         89 $self->{+_BLOCKED_LOAD} = 1;
101             }
102              
103             my %NEW = (
104             hash => sub {
105 1     1   13 my ($class, %params) = @_;
106 1         4 return bless \%params, $class;
107             },
108             array => sub {
109             my ($class, @params) = @_;
110             return bless \@params, $class;
111             },
112             ref => sub {
113             my ($class, $params) = @_;
114             return bless $params, $class;
115             },
116             ref_copy => sub {
117             my ($class, $params) = @_;
118             my $type = reftype($params);
119              
120             return bless {%$params}, $class
121             if $type eq 'HASH';
122              
123             return bless [@$params], $class
124             if $type eq 'ARRAY';
125              
126             croak "Not sure how to construct an '$class' from '$params'";
127             },
128             );
129              
130             sub override_constructor {
131 6     6 1 23 my $self = shift;
132 6         12 my ($name, $type) = @_;
133 6         17 $self->_check();
134              
135 6   100     367 my $sub = $NEW{$type}
136             || croak "'$type' is not a known constructor type";
137              
138 4         16 $self->override($name => $sub);
139             }
140              
141             sub add_constructor {
142 8     8 1 18 my $self = shift;
143 8         18 my ($name, $type) = @_;
144 8         23 $self->_check();
145              
146 8   100     280 my $sub = $NEW{$type}
147             || croak "'$type' is not a known constructor type";
148              
149 6         21 $self->add($name => $sub);
150             }
151              
152             sub autoload {
153 31     31 1 56 my $self = shift;
154 31         74 $self->_check();
155 31         96 my $class = $self->class;
156 31         137 my $stash = $self->stash;
157              
158             croak "Class '$class' already has an AUTOLOAD"
159 31 100 100     113 if $stash->{AUTOLOAD} && *{$stash->{AUTOLOAD}}{CODE};
  4         287  
160              
161             # Weaken this reference so that AUTOLOAD does not prevent its own
162             # destruction.
163 29         82 weaken(my $c = $self);
164              
165 29         58 my ($file, $line) = (__FILE__, __LINE__ + 3);
166 29   100     8647 my $sub = eval <
167             package $class;
168             #line $line "$file (Generated AUTOLOAD)"
169             our \$AUTOLOAD;
170             sub {
171             my (\$self) = \@_;
172             my (\$pkg, \$name) = (\$AUTOLOAD =~ m/^(.*)::([^:]+)\$/g);
173             \$AUTOLOAD = undef;
174              
175             return if \$name eq 'DESTROY';
176             my \$sub = Test::Stream::HashBase->gen_accessor(\$name);
177              
178             \$c->add(\$name => \$sub);
179             goto &\$sub;
180             }
181             EOT
182              
183 28         496 $self->add(AUTOLOAD => $sub);
184             }
185              
186             sub before {
187 2     2 1 8 my $self = shift;
188 2         5 my ($name, $sub) = @_;
189 2         6 $self->_check();
190 2         8 my $orig = $self->current($name);
191 2     1   14 $self->_inject(0, $name => sub { $sub->(@_); $orig->(@_) });
  1         11  
  1         5  
192             }
193              
194             sub after {
195 1     1 1 8 my $self = shift;
196 1         3 my ($name, $sub) = @_;
197 1         4 $self->_check();
198 1         3 my $orig = $self->current($name);
199             $self->_inject(0, $name => sub {
200 3     3   8 my @out;
201              
202 3         8 my $want = wantarray;
203              
204 3 100       10 if ($want) {
    100          
205 1         4 @out = $orig->(@_);
206             }
207             elsif(defined $want) {
208 1         4 $out[0] = $orig->(@_);
209             }
210             else {
211 1         3 $orig->(@_);
212             }
213              
214 3         22 $sub->(@_);
215              
216 3 100       25 return @out if $want;
217 2 100       10 return $out[0] if defined $want;
218 1         3 return;
219 1         7 });
220             }
221              
222             sub around {
223 1     1 1 9 my $self = shift;
224 1         3 my ($name, $sub) = @_;
225 1         4 $self->_check();
226 1         4 my $orig = $self->current($name);
227 1     1   8 $self->_inject(0, $name => sub { $sub->($orig, @_) });
  1         9  
228             }
229              
230             sub add {
231 110     110 1 326 my $self = shift;
232 110         256 $self->_check();
233 110         307 $self->_inject(1, @_);
234             }
235              
236             sub override {
237 32     32 1 140 my $self = shift;
238 32         91 $self->_check();
239 32         112 $self->_inject(0, @_);
240             }
241              
242             sub current {
243 213     213 1 373 my $self = shift;
244 213         306 my ($sym) = @_;
245              
246 213         417 my $class = $self->{+CLASS};
247 213         638 my ($name, $type) = parse_symbol($sym);
248              
249 213         536 my $stash = $self->stash;
250 213 100       626 return unless $stash->{$name};
251              
252 95     95   586 no strict 'refs';
  95         222  
  95         74070  
253 169         199 return *{"$class\::$name"}{$type};
  169         826  
254             }
255              
256             sub orig {
257 17     17 1 60 my $self = shift;
258 17         26 my ($sym) = @_;
259              
260 17 100       64 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
261              
262 17 100       251 my $syms = $self->{+_SYMBOLS}
263             or croak "No symbols have been mocked yet";
264              
265 15         22 my $ref = $syms->{$sym};
266              
267 15 100 66     315 croak "Symbol '$sym' is not mocked"
268             unless $ref && @$ref;
269              
270 13         21 my ($orig) = @$ref;
271              
272 13         183 return $orig;
273             }
274              
275             sub _parse_inject {
276 178     178   237 my $self = shift;
277 178         264 my ($param, $arg) = @_;
278              
279 178 100       488 if ($param =~ m/^-(.*)$/) {
280 15         91 my $sym = $1;
281 15         65 my $sig = slot_to_sig(reftype($arg));
282 15         23 my $ref = $arg;
283 15         49 return ($sig, $sym, $ref);
284             }
285              
286 163 100 100     1206 return ('&', $param, $arg)
287             if ref($arg) && reftype($arg) eq 'CODE';
288              
289 12         16 my ($is, $field, $val);
290              
291 12 100       42 if (!ref($arg)) {
    50          
292 6 50       29 $is = $arg if $arg =~ m/^(rw|ro|wo)$/;
293 6         9 $field = $param;
294             }
295             elsif (reftype($arg) eq 'HASH') {
296 6   66     23 $field = delete $arg->{field} || $param;
297              
298 6         10 $val = delete $arg->{val};
299 6         11 $is = delete $arg->{is};
300              
301 6 50 66     28 croak "Cannot specify 'is' and 'val' together" if $val && $is;
302              
303 6 50 66     26 $is ||= $val ? 'val' : 'rw';
304              
305 6 50       23 croak "The following keys are not valid when defining a mocked sub with a hashref: " . join(", " => keys %$arg)
306             if keys %$arg;
307             }
308              
309 12 50       27 confess "'$arg' is not a valid argument when defining a mocked sub"
310             unless $is;
311              
312 12         14 my $sub;
313 12 100       43 if ($is eq 'rw') {
    100          
    100          
314 4         24 $sub = Test::Stream::HashBase->gen_accessor($field);
315             }
316             elsif ($is eq 'ro') {
317 2         10 $sub = Test::Stream::HashBase->gen_getter($field);
318             }
319             elsif ($is eq 'wo') {
320 2         9 $sub = Test::Stream::HashBase->gen_setter($field);
321             }
322             else { # val
323 4     8   15 $sub = sub { $val };
  8         35  
324             }
325              
326 12         41 return ('&', $param, $sub);
327             }
328              
329             sub _inject {
330 146     146   199 my $self = shift;
331 146         358 my ($add, @pairs) = @_;
332              
333 146         254 my $class = $self->{+CLASS};
334              
335 146   100     514 $self->{+_SYMBOLS} ||= {};
336 146         228 my $syms = $self->{+_SYMBOLS};
337              
338 146         412 while (my $param = shift @pairs) {
339 178         244 my $arg = shift @pairs;
340 178         479 my ($sig, $sym, $ref) = $self->_parse_inject($param, $arg);
341 178         584 my $orig = $self->current("$sig$sym");
342              
343 178 100 100     1023 croak "Cannot override '$sig$class\::$sym', symbol is not already defined"
344             unless $orig || $add;
345              
346             # Cannot be too sure about scalars in globs
347 176 100 100     1015 croak "Cannot add '$sig$class\::$sym', symbol is already defined"
      100        
      66        
348             if $add && $orig
349             && (reftype($orig) ne 'SCALAR' || defined($$orig));
350              
351 174   100     844 $syms->{"$sig$sym"} ||= [];
352 174         218 push @{$syms->{"$sig$sym"}} => $orig; # Might be undef, thats expected
  174         455  
353              
354 95     95   609 no strict 'refs';
  95         212  
  95         3238  
355 95     95   579 no warnings 'redefine';
  95         231  
  95         15674  
356 174         234 *{"$class\::$sym"} = $ref;
  174         1232  
357             }
358              
359 142         454 return;
360             }
361              
362             sub _set_or_unset {
363 141     141   193 my $self = shift;
364 141         227 my ($sym, $set) = @_;
365              
366 141         224 my $class = $self->{+CLASS};
367 141         450 my ($name, $type) = parse_symbol($sym);
368              
369 141 100       415 if (defined $set) {
370 95     95   514 no strict 'refs';
  95         194  
  95         2857  
371 95     95   560 no warnings 'redefine';
  95         199  
  95         7372  
372 45         63 return *{"$class\::$name"} = $set;
  45         444  
373             }
374              
375             # Damn, need to clear it, this gets complicated :-(
376 96         222 my $stash = $self->stash;
377 95     95   488 local *__ORIG__ = do { no strict 'refs'; *{"$class\::$name"} };
  95         197  
  95         6866  
  96         133  
  96         115  
  96         513  
378 96         250 delete $stash->{$name};
379              
380 96         188 for my $slot (qw/CODE SCALAR HASH ARRAY/) {
381 384 100       809 next if $slot eq $type;
382 95     95   499 no strict 'refs';
  95         222  
  95         2587  
383 95     95   534 no warnings 'redefine';
  95         187  
  95         53204  
384 288 100       753 *{"$class\::$name"} = *__ORIG__{$slot} if defined(*__ORIG__{$slot});
  100         520  
385             }
386              
387 96         549 return undef;
388             }
389              
390             sub restore {
391 5     5 1 27 my $self = shift;
392 5         10 my ($sym) = @_;
393 5         15 $self->_check();
394              
395 5 50       27 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
396              
397 5 100       132 my $syms = $self->{+_SYMBOLS}
398             or croak "No symbols are mocked";
399              
400 4         9 my $ref = $syms->{$sym};
401              
402 4 100 66     127 croak "Symbol '$sym' is not mocked"
403             unless $ref && @$ref;
404              
405 3         12 my $old = pop @$ref;
406 3 100       11 delete $syms->{$sym} unless @$ref;
407              
408 3         11 return $self->_set_or_unset($sym, $old);
409             }
410              
411             sub reset {
412 140     140 1 210 my $self = shift;
413 140         215 my ($sym) = @_;
414 140         286 $self->_check();
415              
416 140 100       476 $sym = "&$sym" unless $sym =~ m/^[&\$\%\@]/;
417              
418 140 100       461 my $syms = $self->{+_SYMBOLS}
419             or croak "No symbols are mocked";
420              
421 139         259 my $ref = delete $syms->{$sym};
422              
423 139 100 66     771 croak "Symbol '$sym' is not mocked"
424             unless $ref && @$ref;
425              
426 138         208 my ($old) = @$ref;
427              
428 138         334 return $self->_set_or_unset($sym, $old);
429             }
430              
431             sub reset_all {
432 59     59 1 101 my $self = shift;
433 59         165 $self->_check();
434              
435 59   50     174 my $syms = $self->{+_SYMBOLS} || return;
436              
437 59         318 $self->reset($_) for keys %$syms;
438              
439 59         292 delete $self->{+_SYMBOLS};
440             }
441              
442             sub _purge {
443 20     20   33 my $self = shift;
444 20         55 my $stash = $self->stash;
445 20         334 delete $stash->{$_} for keys %$stash;
446             }
447              
448             sub DESTROY {
449 82     82   3010 my $self = shift;
450              
451 82 100       362 $self->reset_all if $self->{+_SYMBOLS};
452              
453 82 100       248 delete $INC{$self->file} if $self->{+_BLOCKED_LOAD};
454              
455 82 100       764 $self->_purge if $self->{+_PURGE_ON_DESTROY};
456             }
457              
458             1;
459              
460             __END__