File Coverage

blib/lib/Test/Mock/Guard.pm
Criterion Covered Total %
statement 169 169 100.0
branch 31 34 91.1
condition 35 41 85.3
subroutine 32 32 100.0
pod 3 4 75.0
total 270 280 96.4


line stmt bran cond sub pod time code
1             package Test::Mock::Guard;
2              
3 12     12   524359 use strict;
  12         43  
  12         1039  
4 12     12   72 use warnings;
  12         23  
  12         367  
5              
6 12     12   5816 use 5.006001;
  12         65  
  12         664  
7              
8 12     12   65 use Exporter qw(import);
  12         22  
  12         539  
9 12     12   17512 use Class::Load qw(load_class);
  12         745093  
  12         941  
10 12     12   116 use Scalar::Util qw(blessed refaddr set_prototype);
  12         26  
  12         676  
11 12     12   68 use List::Util qw(max);
  12         24  
  12         1442  
12 12     12   75 use Carp qw(croak);
  12         26  
  12         5489  
13              
14             our $VERSION = '0.10';
15             our @EXPORT = qw(mock_guard);
16              
17             sub mock_guard {
18 44     44 1 16384 return Test::Mock::Guard->new(@_);
19             }
20              
21             my $stash = {};
22             sub new {
23 44     44 1 128 my ($class, @args) = @_;
24 44 100 100     897 croak 'must be specified key-value pair' unless @args && @args % 2 == 0;
25 42         90 my $restore = {};
26 42         79 my $object = {};
27 42         150 while (@args) {
28 46         122 my ($class_name, $method_defs) = splice @args, 0, 2;
29 46 100 100     533 croak 'Usage: mock_guard($class_or_objct, $methods_hashref)'
30             unless defined $class_name && ref $method_defs eq 'HASH';
31              
32             # object section
33 44 100       225 if (my $klass = blessed $class_name) {
34 8         35 my $refaddr = refaddr $class_name;
35 8         72 my $guard = Test::Mock::Guard::Instance->new($class_name, $method_defs);
36 8         48 $object->{"$klass#$refaddr"} = $guard;
37 8         33 next;
38             }
39              
40             # Class::Name section
41 36         177 load_class $class_name;
42 35   100     3041 $stash->{$class_name} ||= {};
43 35         98 $restore->{$class_name} = {};
44              
45 35         120 for my $method_name (keys %$method_defs) {
46 52         166 $class->_stash($class_name, $method_name, $restore);
47             my $mocked_method = ref $method_defs->{$method_name} eq 'CODE'
48             ? $method_defs->{$method_name}
49 52 100   48   287 : sub { $method_defs->{$method_name} };
  48         188  
50              
51 52         115 my $fully_qualified_method_name = "$class_name\::$method_name";
52 52         120 my $prototype = prototype($fully_qualified_method_name);
53              
54 12     12   93 no strict 'refs';
  12         117  
  12         582  
55 12     12   178 no warnings 'redefine';
  12         29  
  12         10670  
56              
57 52         355 *{$fully_qualified_method_name} = set_prototype(sub {
58 69     69   20196 ++$stash->{$class_name}->{$method_name}->{called_count};
59 69         139 &$mocked_method;
60 52         252 }, $prototype);
61             }
62             }
63 39         278 return bless { restore => $restore, object => $object } => $class;
64             }
65              
66             sub call_count {
67 45     45 1 22082 my ($self, $klass, $method_name) = @_;
68              
69 45 100       184 if (my $class_name = blessed $klass) {
70             # object
71 27         212 my $refaddr = refaddr $klass;
72 27   100     978 my $guard = $self->{object}->{"$class_name#$refaddr"}
73             || return undef; ## no critic
74 18         49 return $guard->call_count($method_name);
75             }
76             else {
77             # class
78 18         22 my $class_name = $klass;
79 18 50       59 return unless exists $stash->{$class_name}->{$method_name};
80 18         77 return $stash->{$class_name}->{$method_name}->{called_count};
81             }
82             }
83              
84             sub reset {
85 11     11 0 40 my ($self, @args) = @_;
86 11 100 100     284 croak 'must be specified key-value pair' unless @args && @args % 2 == 0;
87 9         23 while (@args) {
88 11         22 my ($class_name, $methods) = splice @args, 0, 2;
89 11 100 100     242 croak 'Usage: $guard->reset($class_or_objct, $methods_arrayref)'
90             unless defined $class_name && ref $methods eq 'ARRAY';
91 9         13 for my $method (@$methods) {
92 9 100       26 if (my $klass = blessed $class_name) {
93 1         3 my $refaddr = refaddr $class_name;
94 1   50     10 my $restore = $self->{object}{"$klass#$refaddr"} || next;
95 1         5 $restore->reset($method);
96 1         4 next;
97             }
98 8         17 $self->_restore($class_name, $method);
99             }
100             }
101             }
102              
103             sub _stash {
104 52     52   101 my ($class, $class_name, $method_name, $restore) = @_;
105 52   100     494 $stash->{$class_name}{$method_name} ||= {
106             counter => 0,
107             restore => {},
108             delete_flags => {},
109             called_count => 0,
110             };
111 52         1167 my $index = ++$stash->{$class_name}{$method_name}{counter};
112 52         381 $stash->{$class_name}{$method_name}{restore}{$index} = $class_name->can($method_name);
113 52         164 $restore->{$class_name}{$method_name} = $index;
114             }
115              
116             sub _restore {
117 53     53   137 my ($self, $class_name, $method_name) = @_;
118              
119 53   100     232 my $index = delete $self->{restore}{$class_name}{$method_name} || return;
120 52         128 my $stuff = $stash->{$class_name}{$method_name};
121 52 100 50     65 if ($index < (max(keys %{$stuff->{restore}}) || 0)) {
122 6         34 $stuff->{delete_flags}{$index} = 1; # fix: destraction problem
123             }
124             else {
125 46         103 my $orig_method = delete $stuff->{restore}{$index}; # current restore method
126              
127             # restored old mocked method
128 46         68 for my $index (sort { $b <=> $a } keys %{$stuff->{delete_flags}}) {
  1         49  
  46         284  
129 6         11 delete $stuff->{delete_flags}{$index};
130 6         114 $orig_method = delete $stuff->{restore}{$index};
131             }
132              
133             # cleanup
134 46 100       209 unless (keys %{$stuff->{restore}}) {
  46         137  
135 44         91 delete $stash->{$class_name}{$method_name};
136             }
137              
138 12     12   83 no strict 'refs';
  12         24  
  12         398  
139 12     12   56 no warnings qw(redefine prototype);
  12         19  
  12         2489  
140 46         544 *{"$class_name\::$method_name"} = $orig_method
141 46   66     126 || *{"$class_name\::$method_name is unregistered"}; # black magic!
142             }
143             }
144              
145             sub DESTROY {
146 39     39   15825 my $self = shift;
147 39         75 while (my ($class_name, $method_defs) = each %{$self->{restore}}) {
  74         746  
148 35         122 for my $method_name (keys %$method_defs) {
149 45         127 $self->_restore($class_name, $method_name);
150             }
151             }
152             }
153              
154             # taken from cho45's code
155             package
156             Test::Mock::Guard::Instance;
157              
158 12     12   69 use Scalar::Util qw(blessed refaddr);
  12         34  
  12         2218  
159              
160             my $mocked = {};
161             sub new {
162 8     8   20 my ($class, $object, $methods) = @_;
163 8         55 my $klass = blessed($object);
164 8         26 my $refaddr = refaddr($object);
165              
166 8         17 my $methods_map = {};
167 8   100     72 $mocked->{$klass}->{_mocked} ||= {};
168 8         176 for my $method (keys %$methods) {
169 16         73 $methods_map->{$method} = {
170             method => $methods->{$method},
171             called_count => 0,
172             };
173 16 100       60 unless ($mocked->{$klass}->{_mocked}->{$method}) {
174 8         58 my $original_method = $klass->can($method);
175 8         25 $mocked->{$klass}->{_mocked}->{$method} = $original_method;
176 12     12   75 no strict 'refs';
  12         19  
  12         388  
177 12     12   59 no warnings qw(redefine prototype);
  12         20  
  12         7413  
178 8     53   37 *{"$klass\::$method"} = sub { _mocked($method, $original_method, @_) };
  8         45  
  53         35492  
179             }
180             }
181              
182 8         30 $mocked->{$klass}->{$refaddr} = $methods_map;
183 8         43 bless { object => $object }, $class;
184             }
185              
186             sub reset {
187 1     1   1 my ($self, $method) = @_;
188 1         2 my $object = $self->{object};
189 1         3 my $klass = blessed($object);
190 1         3 my $refaddr = refaddr($object);
191              
192 1 50 33     9 if (exists $mocked->{$klass}{$refaddr} && exists $mocked->{$klass}{$refaddr}{$method}) {
193 1         3 delete $mocked->{$klass}{$refaddr}{$method};
194             }
195             }
196              
197             sub call_count {
198 18     18   25 my ($self, $method_name) = @_;
199 18         60 my $klass = blessed $self->{object};
200 18         44 my $refaddr = refaddr $self->{object};
201 18 50       69 return unless exists $mocked->{$klass}{$refaddr}{$method_name}{called_count};
202 18         95 return $mocked->{$klass}{$refaddr}{$method_name}{called_count};
203             }
204              
205             sub _mocked {
206 53     53   115 my ($method, $original, $object, @rest) = @_;
207 53         665 my $klass = blessed($object);
208 53         128 my $refaddr = refaddr($object);
209 53 100 66     605 if ($klass && $refaddr && exists $mocked->{$klass}->{$refaddr} && exists $mocked->{$klass}->{$refaddr}->{$method}) {
      100        
      100        
210 43         102 ++$mocked->{$klass}->{$refaddr}->{$method}->{called_count};
211 43         368 my $val = $mocked->{$klass}->{$refaddr}->{$method}->{method};
212 43 100       241 ref($val) eq 'CODE' ? $val->($object, @rest) : $val;
213             } else {
214 10         34 $original->($object, @rest);
215             }
216             }
217              
218             sub DESTROY {
219 8     8   19 my ($self) = @_;
220 8         35 my $object = $self->{object};
221 8         36 my $klass = blessed($object);
222 8         31 my $refaddr = refaddr($object);
223 8         47 delete $mocked->{$klass}->{$refaddr};
224              
225 8 100       14 unless (keys %{ $mocked->{$klass} } == 1) {
  8         71  
226 2         4 my $mocked = delete $mocked->{$klass}->{_mocked};
227 2         11 for my $method (keys %$mocked) {
228 12     12   76 no strict 'refs';
  12         30  
  12         419  
229 12     12   81 no warnings qw(redefine prototype);
  12         20  
  12         1207  
230 2         4 *{"$klass\::$method"} = $mocked->{$method};
  2         16  
231             }
232             }
233             }
234              
235             1;
236              
237             __END__