File Coverage

blib/lib/Mock/Quick/Class.pm
Criterion Covered Total %
statement 160 160 100.0
branch 40 48 83.3
condition 16 21 76.1
subroutine 35 35 100.0
pod 8 12 66.6
total 259 276 93.8


line stmt bran cond sub pod time code
1             package Mock::Quick::Class;
2 4     4   838 use strict;
  4         11  
  4         156  
3 4     4   22 use warnings;
  4         8  
  4         110  
4              
5 4     4   2441 use Mock::Quick::Util;
  4         10  
  4         348  
6 4     4   22 use Scalar::Util qw/blessed weaken/;
  4         8  
  4         201  
7 4     4   23 use Carp qw/croak confess carp/;
  4         6  
  4         5244  
8              
9             our @CARP_NOT = ('Mock::Quick', 'Mock::Quick::Object');
10             our $ANON = 'AAAAAAAAAA';
11              
12 122     122 1 2986 sub package { shift->{'-package'} }
13 5     5 0 38 sub inc { shift->{'-inc'} }
14 44     44 1 271 sub is_takeover { shift->{'-takeover'} }
15 14     14 1 87 sub is_implement { shift->{'-implement'}}
16              
17             sub metrics {
18 45     45 1 55 my $self = shift;
19 45   100     157 $self->{'-metrics'} ||= {};
20 45         97 return $self->{'-metrics'};
21             }
22              
23             sub takeover {
24 7     7 0 980 my $class = shift;
25 7         16 my ( $proto, %params ) = @_;
26 7   66     43 my $package = blessed( $proto ) || $proto;
27              
28 7         38 my $self = bless( { -package => $package, -takeover => 1 }, $class );
29              
30 7         25 for my $key ( keys %params ) {
31 2 50       10 croak "param '$key' is not valid in a takeover"
32             if $key =~ m/^-/;
33 2         12 $self->override( $key => $params{$key} );
34             }
35              
36 7         20 $self->inject_meta();
37              
38 7         24 return $self;
39             }
40              
41             sub implement {
42 3     3 0 47 my $class = shift;
43 3         9 my ( $package, %params ) = @_;
44 3   50     23 my $caller = delete $params{'-caller'} || [caller()];
45              
46 3         66 my $inc = $package;
47 3         8 $inc =~ s|::|/|g;
48 3         8 $inc .= '.pm';
49              
50 3 50       11 croak "$package has already been loaded, cannot implement it."
51             if $INC{$inc};
52              
53 3         10 $INC{$inc} = $caller->[1];
54              
55 3         19 my $self = bless(
56             { -package => $package, -implement => 1, -inc => $inc },
57             $class
58             );
59              
60 3         9 $self->inject_meta();
61              
62 3         13 $self->_configure( %params );
63              
64 3         14 return $self;
65             }
66              
67             alt_meth new => (
68             obj => sub { my $self = shift; $self->package->new(@_) },
69             class => sub {
70             my $class = shift;
71             my %params = @_;
72              
73             croak "You cannot combine '-takeover' and '-implement' arguments"
74             if $params{'-takeover'} && $params{'-implement'};
75              
76             return $class->takeover( delete( $params{'-takeover'} ), %params )
77             if $params{'-takeover'};
78              
79             return $class->implement( delete( $params{'-implement'} ), %params )
80             if $params{'-implement'};
81              
82             my $package = __PACKAGE__ . "::__ANON__::" . $ANON++;
83              
84             my $self = bless( { %params, -package => $package }, $class );
85              
86             $self->inject_meta();
87              
88             $self->_configure( %params );
89              
90             return $self;
91             }
92             );
93              
94             sub inject_meta {
95 18     18 0 28 my $self = shift;
96 18         22 my $weak_self = $self;
97 18         55 weaken $weak_self;
98 18     1   40 inject( $self->package, 'MQ_CONTROL', sub { $weak_self } );
  1         7  
99             }
100              
101             sub _configure {
102 11     11   15 my $self = shift;
103 11         26 my %params = @_;
104 11         23 my $package = $self->package;
105 11         32 my $metrics = $self->metrics;
106              
107 11         32 for my $key ( keys %params ) {
108 17         23 my $value = $params{$key};
109              
110 17 100       69 if ( $key =~ m/^-/ ) {
    100          
111 9         26 $self->_configure_pair( $key, $value );
112             }
113             elsif( _is_sub_ref( $value )) {
114 4     6   23 inject( $package, $key, sub { $metrics->{$key}++; $value->(@_) });
  6         24  
  6         17  
115             }
116             else {
117 4     1   20 inject( $package, $key, sub { $metrics->{$key}++; $value });
  1         2  
  1         5  
118             }
119             }
120             }
121              
122             sub _configure_pair {
123 9     9   15 my $control = shift;
124 9         15 my ( $param, $value ) = @_;
125 9         19 my $package = $control->package;
126 9         16 my $metrics = $control->metrics;
127              
128 9 100       42 if ( $param eq '-subclass' ) {
    100          
    50          
129 2 100       8 $value = [ $value ] unless ref $value eq 'ARRAY';
130 4     4   29 no strict 'refs';
  4         7  
  4         4174  
131 2         3 push @{"$package\::ISA"} => @$value;
  2         30  
132             }
133             elsif ( $param eq '-attributes' ) {
134 1 50       4 $value = [ $value ] unless ref $value eq 'ARRAY';
135 1         3 for my $attr ( @$value ) {
136             inject( $package, $attr, sub {
137 2     2   7 my $self = shift;
138              
139 2 50       7 croak "$attr() called on class '$self' instead of an instance"
140             unless blessed( $self );
141              
142 2         4 $metrics->{$attr}++;
143 2 100       8 ( $self->{$attr} ) = @_ if @_;
144 2         6 return $self->{$attr};
145 3         14 });
146             }
147             }
148             elsif ( $param eq '-with_new' ) {
149             inject( $package, 'new', sub {
150 5     5   10 my $class = shift;
151 5 50 33     17 croak "Expected hash, received reference to hash"
152             if @_ == 1 and ref $_[0] eq 'HASH';
153 5         10 my %proto = @_;
154 5         10 $metrics->{new}++;
155              
156 5 50       15 croak "new() cannot be called on an instance"
157             if blessed( $class );
158              
159 5         27 return bless( \%proto, $class );
160 6         40 });
161             }
162             }
163              
164             sub _is_sub_ref {
165 27     27   33 my $in = shift;
166 27         46 my $type = ref $in;
167 27         53 my $class = blessed( $in );
168              
169 27 100 100     169 return 1 if $type && $type eq 'CODE';
170 8 100 66     36 return 1 if $class && $class->isa( 'Mock::Quick::Method' );
171 7         29 return 0;
172             }
173              
174             sub override {
175 11     11 1 1182 my $self = shift;
176 11         26 my $package = $self->package;
177 11         31 my %pairs = @_;
178 11         16 my @originals;
179 11         26 my $metrics = $self->metrics;
180              
181 11         29 for my $name ( keys %pairs ) {
182 12         18 my $orig_value = $pairs{$name};
183              
184 12 100 100     32 carp "Overriding non-existent method '$name'"
185             if $self->is_takeover && !$package->can($name);
186              
187             my $real_value = _is_sub_ref( $orig_value )
188 9     9   57 ? sub { $metrics->{$name}++; return $orig_value->(@_) }
  9         22  
189 12 100   6   1205 : sub { $metrics->{$name}++; return $orig_value };
  6         20  
  6         10  
190              
191 12         31 my $original = $self->original( $name );
192 12         38 inject( $package, $name, $real_value );
193              
194 12         33 push @originals, $original;
195             }
196              
197 11         37 return @originals;
198             }
199              
200             sub original {
201 25     25 1 33 my $self = shift;
202 25         36 my ( $name ) = @_;
203 25 100       61 unless ( exists $self->{$name} ) {
204 9   100     23 $self->{$name} = $self->package->can( $name ) || undef;
205             }
206 25         50 return $self->{$name};
207             }
208              
209             sub restore {
210 11     11 1 1805 my $self = shift;
211              
212 11         23 for my $name ( @_ ) {
213 12         30 my $original = $self->original($name);
214 12         31 delete $self->metrics->{$name};
215              
216 12 100       32 if ( $original ) {
217 7 100   2   15 my $sub = _is_sub_ref( $original ) ? $original : sub { $original };
  2         8  
218 7         16 inject( $self->package, $name, $sub );
219             }
220             else {
221 5         20 $self->_clear( $name );
222             }
223             }
224             }
225              
226             sub _clear {
227 5     5   9 my $self = shift;
228 5         11 my ( $name ) = @_;
229 5         21 my $package = $self->package;
230 4     4   29 no strict 'refs';
  4         8  
  4         469  
231 5         9 my $ref = \%{"$package\::"};
  5         18  
232 5         59 delete $ref->{ $name };
233             }
234              
235             sub undefine {
236 14     14 1 1869 my $self = shift;
237 14         26 my $package = $self->package;
238 14 50       27 croak "Refusing to undefine a class that was taken over."
239             if $self->is_takeover;
240 4     4   24 no strict 'refs';
  4         8  
  4         550  
241 14         18 undef( *{"$package\::"} );
  14         153  
242 14 100       165 delete $INC{$self->inc} if $self->is_implement;
243             }
244              
245             sub DESTROY {
246 18     18   7924 my $self = shift;
247 18 100       44 return $self->undefine unless $self->is_takeover;
248              
249 7         25 my $package = $self->package;
250              
251             {
252 4     4   24 no strict 'refs';
  4         7  
  4         122  
  7         9  
253 4     4   26 no warnings 'redefine';
  4         6  
  4         741  
254              
255 7         19 my $ref = \%{"$package\::"};
  7         20  
256 7         32 delete $ref->{MQ_CONTROL};
257             }
258              
259 7         9 for my $sub ( keys %{$self} ) {
  7         18  
260 26 100       94 next if $sub =~ m/^-/;
261 6         16 $self->restore( $sub );
262             }
263             }
264              
265             purge_util();
266              
267             1;
268              
269             __END__
270              
271             =head1 NAME
272              
273             Mock::Quick::Class - Class mocking for Mock::Quick
274              
275             =head1 DESCRIPTION
276              
277             Provides class mocking for L<Mock::Quick>
278              
279             =head1 SYNOPSIS
280              
281             =head2 IMPLEMENT A CLASS
282              
283             This will implement a class at the namespace provided via the -implement
284             argument. The class must not already be loaded. Once complete the real class
285             will be prevented from loading until you call undefine() on the control object.
286              
287             use Mock::Quick::Class;
288              
289             my $control = Mock::Quick::Class->new(
290             -implement => 'My::Package',
291              
292             # Insert a generic new() method (blessed hash)
293             -with_new => 1,
294              
295             # Inheritance
296             -subclass => 'Some::Class',
297             # Can also do
298             -subclass => [ 'Class::A', 'Class::B' ],
299              
300             # generic get/set attribute methods.
301             -attributes => [ qw/a b c d/ ],
302              
303             # Method that simply returns a value.
304             simple => 'value',
305              
306             # Custom method.
307             method => sub { ... },
308             );
309              
310             my $obj = $control->package->new;
311             # OR
312             my $obj = My::Package->new;
313              
314             # Override a method
315             $control->override( foo => sub { ... });
316              
317             # Restore it to the original
318             $control->restore( 'foo' );
319              
320             # Remove the namespace we created, which would allow the real thing to load
321             # in a require or use statement.
322             $control->undefine();
323              
324             You can also use the 'implement' method instead of new:
325              
326             use Mock::Quick::Class;
327              
328             my $control = Mock::Quick::Class->implement(
329             'Some::Package',
330             %args
331             );
332              
333             =head2 ANONYMOUS MOCKED CLASS
334              
335             This is if you just need to generate a class where the package name does not
336             matter. This is done when the -takeover and -implement arguments are both
337             omitted.
338              
339             use Mock::Quick::Class;
340              
341             my $control = Mock::Quick::Class->new(
342             # Insert a generic new() method (blessed hash)
343             -with_new => 1,
344              
345             # Inheritance
346             -subclass => 'Some::Class',
347             # Can also do
348             -subclass => [ 'Class::A', 'Class::B' ],
349              
350             # generic get/set attribute methods.
351             -attributes => [ qw/a b c d/ ],
352              
353             # Method that simply returns a value.
354             simple => 'value',
355              
356             # Custom method.
357             method => sub { ... },
358             );
359              
360             my $obj = $control->package->new;
361              
362             # Override a method
363             $control->override( foo => sub { ... });
364              
365             # Restore it to the original
366             $control->restore( 'foo' );
367              
368             # Remove the anonymous namespace we created.
369             $control->undefine();
370              
371             =head2 TAKING OVER EXISTING/LOADED CLASSES
372              
373             use Mock::Quick::Class;
374              
375             my $control = Mock::Quick::Class->takeover( 'Some::Package' );
376              
377             # Override a method
378             $control->override( foo => sub { ... });
379              
380             # Restore it to the original
381             $control->restore( 'foo' );
382              
383             # Destroy the control object and completely restore the original class
384             # Some::Package.
385             $control = undef;
386              
387             You can also do this through new()
388              
389             use Mock::Quick::Class;
390              
391             my $control = Mock::Quick::Class->new(
392             -takeover => 'Some::Package',
393             %overrides
394             );
395              
396             =head1 ACCESSING THE CONTROL OBJECY
397              
398             While the control object exists, it can be accessed via
399             C<YOUR::PACKAGE->MQ_CONTROL()>. It is important to note that this method will
400             disappear whenever the control object you track falls out of scope.
401              
402             Example (taken from Class.t):
403              
404             $obj = $CLASS->new( -takeover => 'Baz' );
405             $obj->override( 'foo', sub {
406             my $class = shift;
407             return "PREFIX: " . $class->MQ_CONTROL->original( 'foo' )->();
408             });
409              
410             is( Baz->foo, "PREFIX: foo", "Override and accessed original through MQ_CONTROL" );
411             $obj = undef;
412              
413             is( Baz->foo, 'foo', 'original' );
414             ok( !Baz->can('MQ_CONTROL'), "Removed control" );
415              
416             =head1 METHODS
417              
418             =over 4
419              
420             =item $package = $obj->package()
421              
422             Get the name of the package controlled by this object.
423              
424             =item $bool = $obj->is_takeover()
425              
426             Check if the control object was created to takeover an existing class.
427              
428             =item $bool = $obj->is_implement()
429              
430             Check if the control object was created to implement a class.
431              
432             =item $data = $obj->metrics()
433              
434             Returns a hash where keys are method names, and values are the number of times
435             the method has been called. When a method is altered or removed the key is
436             deleted.
437              
438             =item $obj->override( name => sub { ... })
439              
440             Override a method.
441              
442             =item $obj->original( $name );
443              
444             Get the original method (coderef). Note: The first time this is called it find
445             and remembers the value of package->can( $name ). This means that if you modify
446             or replace the method without using Mock::Quick before this is called, it will
447             have the updated method, not the true original.
448              
449             The override() method will call this first to ensure the original method is
450             cached and available for restore(). Once a value is set it is never replaced or
451             cleared.
452              
453             =item $obj->restore( $name )
454              
455             Restore a method (Resets metrics)
456              
457             =item $obj->undefine()
458              
459             Undefine the package controlled by the control.
460              
461             =back
462              
463             =head1 AUTHORS
464              
465             =over 4
466              
467             =item Chad Granum L<exodist7@gmail.com>
468              
469             =item Glen Hinkle L<glen@empireenterprises.com>
470              
471             =back
472              
473             =head1 COPYRIGHT
474              
475             Copyright (C) 2011 Chad Granum
476              
477             Mock-Quick is free software; Standard perl licence.
478              
479             Mock-Quick is distributed in the hope that it will be useful, but WITHOUT ANY
480             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
481             PARTICULAR PURPOSE. See the license for more details.