File Coverage

inc/Test/Mock/Guard.pm
Criterion Covered Total %
statement 95 168 56.5
branch 6 34 17.6
condition 7 35 20.0
subroutine 23 32 71.8
pod 3 4 75.0
total 134 273 49.0


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