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