File Coverage

blib/lib/Test/CallFlow/Plan.pm
Criterion Covered Total %
statement 108 108 100.0
branch 57 76 75.0
condition 16 24 66.6
subroutine 10 10 100.0
pod 7 7 100.0
total 198 225 88.0


line stmt bran cond sub pod time code
1             package Test::CallFlow::Plan;
2 6     6   92 use strict;
  6         10  
  6         280  
3 6     6   33 use Carp;
  6         10  
  6         11813  
4              
5             =head1 Test::CallFlow::Plan
6              
7             Contains planned calls to mocked functions.
8              
9             =head1 METHODS
10              
11             =head2 new
12              
13             my $mock_call_plan = Test::CallFlow::Plan->new( %properties );
14              
15             =cut
16              
17             sub new {
18 7     7 1 15 my $class = shift;
19 7         23 my $self = bless {@_}, $class;
20 7         33 $self->reset;
21 7         37 $self;
22             }
23              
24             =head2 add_call
25              
26             $mock_call_plan->add_call( Test::CallFlow::Call->new( 'subname', @args ) );
27              
28             Adds a call into this plan.
29              
30             =cut
31              
32             sub add_call {
33 12     12 1 21 my ( $self, $call ) = @_;
34 12   50     24 push @{ $self->{calls} ||= [] }, $call;
  12         54  
35             }
36              
37             =head2 call
38              
39             $mock_call_plan->call( 'subname', @args );
40              
41             Heart of plan execution. Searches for a matching call and returns the result.
42              
43             This should be shortened for ease of further development. Then again, it seems to work.
44              
45             =cut
46              
47             sub call {
48 36     36 1 93 my ( $self, $sub, @args ) = @_;
49 36 50       98 warn "Plan got call $sub(@args) for planned call #$self->{at}\n"
50             if $self->{debug};
51 36         88 my $got_args = [ $sub, @args ];
52              
53 36         58 my ( @error, @errors );
54 36         50 my $at = 0;
55 36         46 my $call;
56             my @try_calls;
57 36         43 my $trying_in_order = 1;
58 36   100     155 my @try_call_at = ( $self->{at} || 0 );
59 36         48 my @unordered;
60 36 50       42 my $num_calls = @{ $self->{calls} || [] };
  36         125  
61 36         48 my $first_in_order_at;
62              
63 36         91 while (@try_call_at) {
64 84 0       714 warn "Calls to try: @try_call_at",
    50          
65             $trying_in_order ? "; delayed: @unordered" : ''
66             if $self->{debug};
67 84         108 $at = shift @try_call_at;
68 84 100       745 $call = $self->{calls}[$at]
69             or last;
70              
71 83 100 100     391 if ( $trying_in_order and not $call->in_order ) {
    100 66        
72              
73             # try again later if match not found in order
74 11 50 50     62 push @unordered, $at
75             unless $at <= ( $self->{latest_unordered_at} || -1 );
76 11 100       29 if ( ++$at < $num_calls ) {
77              
78             # try next in order
79 8         15 unshift @try_call_at, $at;
80             } else {
81              
82             # start going through unordered ones
83 3         3 $trying_in_order = 0;
84 3 100       4 @try_call_at = ( @{ $self->{anytime} || [] }, @unordered );
  3         15  
85             }
86 11         32 next;
87             } elsif ( not defined $first_in_order_at
88             and not $call->over )
89             {
90 35         50 $first_in_order_at = $at;
91             }
92              
93             last # matched!
94 72 100       227 unless @error = $call->check($got_args);
95              
96 44         101 push @errors, [ $at, @error ];
97              
98 44 100       292 if ($trying_in_order) {
99 31 100 100     278 if ( $call->satisfied and ++$at < $num_calls ) {
100              
101             # this plan may be passed;
102             # keep looking ahead in order
103 12         21 unshift @try_call_at, $at;
104 12         47 next;
105             } else {
106              
107             # this plan must be matched in order before any later ones;
108             # fallback to looking at unordered ones
109 19         25 $trying_in_order = 0;
110 19 100       22 @try_call_at = ( @{ $self->{anytime} || [] }, @unordered );
  19         132  
111             }
112             }
113             }
114              
115 36 100       131 $self->failed_call( [ $sub, @args ], $self->{calls}, \@errors )
116             if @error;
117              
118 29 100       267 croak "Unplanned call to mock at $self->{at}: $sub(@args)"
119             unless $call;
120              
121 28 100       75 if (@unordered) {
122 6   100     9 push @{ $self->{anytime} ||= [] }, @unordered;
  6         35  
123 6         14 $self->{latest_unordered_at} = $unordered[-1];
124             }
125              
126 28 100       243 if ( my $end_calls = $call->{end} ) {
127 3 50       12 warn "end calls: @$end_calls" if $self->{debug};
128 4 100       14 my %end_calls = map {
129 3         9 croak(
130 2         399 "Expected call $_->{sub}(@{$_->{args}}) not made until end of scope\n"
131             ) unless $_->satisfied;
132 2         9 $_ => 1
133             } @$end_calls;
134 2         7 $self->{anytime} =
135 1 50       2 [ grep { !$end_calls{$_} } @{ $self->{anytime} || [] } ];
  1         6  
136             }
137              
138 26         94 my $result = $call->call;
139 26 0       75 $result = wantarray ? ( $result->(@_) ) : ( scalar $result->(@_) )
    50          
140             if ref $result eq 'CODE';
141 26 100       65 $result = [] unless defined $result;
142 26 50       77 $result = [$result] unless ref $result eq 'ARRAY';
143              
144             # point to where to start looking at next time
145 26 50 0     70 warn "Going from ", ( $self->{at} || 0 ), " to $first_in_order_at"
146             if $self->{debug};
147 26         40 $self->{at} = $first_in_order_at;
148              
149             # skip those we can't use anymore
150 26         73 while ( $self->{at} < $num_calls ) {
151 30         63 my $call = $self->{calls}[ $self->{at} ];
152 30 50       68 confess("Bad item in call plan at $self->{at} ($call)")
153             unless ref $call;
154 30 100       88 last unless $call->over;
155 7 50       140 warn "Passing completed call #$self->{at} ($call->{called}/",
156             $call->min, "-", $call->max, "): ", $call->name
157             if $self->{debug};
158 7         25 ++$self->{at};
159             }
160              
161 26 50       86 warn "mock #$at $sub(@args) -> (@$result)" if $self->{debug};
162 26 50       178 return wantarray ? @$result : $result->[0];
163             }
164              
165             =head2 failed_call
166              
167             $mock_call_plan->failed_call( $called, $calls, \@errors );
168              
169             Used by C to report errors. Croaks with a list of tried and failed call proposals.
170              
171             =cut
172              
173             sub failed_call {
174 7     7 1 15 my ( $self, $called, $calls, $errors ) = @_;
175 7         13 my $msg = '';
176 7         22 my $at = -1;
177 7         28 while ( ++$at < @$errors ) {
178 9         12 my ( $call_at, $arg_at, $test_at ) = @{ $errors->[$at] };
  9         21  
179 9         23 my $call = $self->{calls}[$call_at];
180 7         44 $msg .=
181             $arg_at
182 9 100       30 ? $test_at < @{ $call->{args} }
    100          
183             ? "Call '$called->[0]' argument #$arg_at ($called->[$arg_at]) did not match "
184             : "Too many arguments (" . ( @$called - 1 ) . ") after last "
185             : "Called sub name '$called->[0]' did not match ";
186 9 100       31 $msg .=
187             $test_at
188             ? "argument test #$test_at"
189             : "sub name";
190 9 50       93 $msg .= " of "
    100          
191             . ( $call->in_order
192             ? $call->min
193             ? 'required'
194             : 'optional'
195             : 'unordered' );
196 9         37 $msg .= " planned call " . $call->name . "\n";
197             }
198              
199 7         1660 croak($msg);
200             }
201              
202             =head2 unsatisfied
203              
204             Returns an array of remaining unsatisfied calls.
205              
206             Whole plan can be seen as successfully executed once this returns an empty array.
207              
208             =cut
209              
210             sub unsatisfied {
211 6     6 1 12 my $self = shift;
212 6         11 my $last_call = @{ $self->{calls} } - 1;
  6         18  
213              
214 10         120 grep { !$_->satisfied }
  4         12  
215             $self->{calls}[ ( $self->{at} || 0 ), $last_call ],
216 6 100 66     39 map { $self->{calls}[$_] } @{ $self->{anytime} || [] };
  6         43  
217             }
218              
219             =head2 reset
220              
221             $plan->reset;
222              
223             Return to planning state, preserving all previously made plans and discarding any results of running.
224              
225             =cut
226              
227             sub reset {
228 15     15 1 33 my $self = shift;
229 15 50 0     116 warn "Reset Mock Plan at ", ( $self->{at} || 0 )
230             if $self->{debug};
231 15         31 $_->reset for @{ $self->{calls} };
  15         157  
232 15         58 $self->_clean;
233             }
234              
235             sub _clean {
236 15     15   27 my $self = shift;
237 15         34 delete $self->{at};
238 15         74 delete $self->{anytime};
239 15         42 delete $self->{latest_unordered_at};
240             }
241              
242             =head2 list_calls
243              
244             Returns the list of calls in this plan.
245              
246             =cut
247              
248             sub list_calls {
249 1     1 1 2 my $self = shift;
250 1         1 @{ $self->{calls} };
  1         5  
251             }
252              
253             1;