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