File Coverage

blib/lib/Object/Exercise/Execute.pm
Criterion Covered Total %
statement 73 81 90.1
branch 29 40 72.5
condition 8 12 66.6
subroutine 14 14 100.0
pod 0 5 0.0
total 124 152 81.5


line stmt bran cond sub pod time code
1             #######################################################################
2             # housekeeping
3             #######################################################################
4              
5             package Object::Exercise::Execute v3.1.1;
6 12     12   730 use v5.20;
  12         35  
7 12     12   57 no warnings;
  12         20  
  12         341  
8              
9 12     12   54 use Carp;
  12         20  
  12         740  
10              
11 12     12   64 use List::Util qw( first );
  12         22  
  12         1270  
12 12     12   80 use Scalar::Util qw( reftype );
  12         25  
  12         589  
13 12     12   3299 use Time::HiRes qw( gettimeofday tv_interval );
  12         8239  
  12         50  
14 12     12   5997 use YAML::XS qw( Load );
  12         27995  
  12         551  
15              
16 12     12   715 use Test::More;
  12         59345  
  12         73  
17              
18             ########################################################################
19             # package variables
20             ########################################################################
21              
22             our @CARP_NOT = ( __PACKAGE__ );
23              
24             my @defaultz =
25             qw
26             (
27             benchmark 0
28             counter 0
29             break 0
30             regex 0
31              
32             debug 0
33             verbose 0
34              
35              
36             continue 1
37             finish 1
38              
39             export exercise
40             );
41              
42             my %globalz = @defaultz;
43              
44             ########################################################################
45             # local utility subs
46             ########################################################################
47              
48             ########################################################################
49             # deal with flag settings
50              
51             sub extract_flags
52             {
53 163     163 0 222 my %flagz = ();
54 163         197 my $count = 0;
55 163         183 my $found = 0;
56              
57 163         260 for( @_ )
58             {
59 174 100       349 ref $_
60             and last;
61              
62 25         51 ++$count;
63              
64 25         166 my ( $negate, $name, $value )
65             = m{ (no)? (\w+) (?:=)?( .+ )? }x;
66              
67             exists $globalz{ $name }
68             or do
69 25 100       89 {
70 11         44 diag "Unknown option: '$_'";
71             next
72 11         2957 };
73              
74 14         24 ++$found;
75              
76 14 100 50     97 $flagz{ $name }
      100        
77             = $negate
78             ? ! ( $value // 1 )
79             : ( $value // 1 )
80             ;
81             }
82              
83             $count
84 163 100       451 or return;
85              
86 25         56 splice @_, 0, $count;
87              
88 25 100       86 note "Extracted flags:\n", explain \%flagz
89             if $found;
90              
91             wantarray
92 25 100       54896 ? %flagz
93             : \%flagz
94             }
95              
96             sub import_flags
97             {
98 12     12 0 42 %globalz = ( @defaultz, &extract_flags );
99              
100             # hand back the name to export.
101             # remainder are left in the global settings.
102              
103             delete $globalz{ export }
104 12         41 }
105              
106             ########################################################################
107             # execute individual tests
108              
109             my $execute
110             = sub
111             {
112             # note the lack of pass/fail: all this will do is report errors.
113             # any pass/fail handling is dealt with in test_result.
114              
115             state $r = [];
116             state $t0 = '';
117             state $t1 = '';
118             state $n = 0;
119              
120             my ( $obj, $test ) = @_;
121              
122             my ( $method, @argz ) = @$test;
123              
124             # assume that "@$r =" is not significant in the benchmark.
125             # also assume that nothing in gettimeofday will set $@.
126              
127             @$r = ();
128              
129             eval
130             {
131             $DB::single = 1 if $globalz{ break };
132              
133             $t0 = [ gettimeofday ];
134             @$r = $obj->$method( @argz );
135             $t1 = [ gettimeofday ];
136             };
137             chomp( my $error = $@ );
138              
139             if( $globalz{ counter } )
140             {
141             note 'Instruction: ' . ++$n;
142             }
143              
144             if( $globalz{ benchmark } )
145             {
146             state $format = "Benchmark: %0.6f sec\t%s( %s )";
147              
148             my $wall = tv_interval $t0, $t1;
149              
150             note sprintf $format => $wall, $method, explain @argz;
151             }
152              
153             if( $error )
154             {
155             diag "Error: '$method'\n", explain $error;
156             }
157             elsif( $globalz{ verbose } )
158             {
159             note "Clean: '$method'\n", explain $r;
160             }
161              
162             if( $error && $globalz{ debug } )
163             {
164             # stop here where the error is visible and the
165             # test contents are still in scope.
166              
167             $DB::single = 1;
168             0
169             }
170              
171             ( $error, $r )
172             };
173              
174             my $test_result
175             = sub
176             {
177             my ( $obj, $test, $expect, $message ) = @_;
178              
179             my $method = $test->[0];
180              
181             my ( $error, $found ) = $obj->$execute( $test );
182              
183             $message ||= "$method";
184             $message .= " ($error)"
185             if $error;
186              
187             for my $type ( reftype $expect )
188             {
189             if( 'CODE' eq $type )
190             {
191             ok $expect->( $test, $found, $error ), $message;
192             }
193             elsif( $expect )
194             {
195             # this may pass if expect is [ undef ] but will
196             # report the error text either way.
197              
198             if
199             (
200             ! $type
201             and
202             $globalz{ regex }
203             )
204             {
205             like "@$found", qr{$expect}x, $message;
206             }
207             elsif( 'REGEXP' eq $type )
208             {
209             like "@$found", $expect, $message;
210             }
211             elsif( 'ARRAY' eq $type )
212             {
213             is_deeply $found, $expect, $message;
214             }
215             else
216             {
217             BAIL_OUT
218             "Invalid expect: '$type'\n" .
219             explain( $expect, $found, $message );
220             }
221             }
222             elsif( $error )
223             {
224             # explicit undef expects an error.
225             # zero the string here to avoid rejecting the test
226             # in the caller.
227              
228             pass "Expected error: '$error'";
229              
230             $error = '';
231             }
232             else
233             {
234             # expected error was not returned, this is a failure.
235              
236             fail "Unexpected success: '$method' (no error)";
237             diag "Return value:\n", explain $found;
238             }
239             }
240              
241             # mainly useful as a boolean value in the caller.
242              
243             $error
244             };
245              
246             my $process
247             = sub
248             {
249             my $obj = shift;
250              
251             # flattened out test entry is left on the stack.
252             # extract any local flags (e.g., verbose for one
253             # test only).
254              
255             my $localz = &extract_flags;
256              
257             # trailing flags left nothing further to process.
258              
259             if( @_ )
260             {
261             # sane
262             }
263             else
264             {
265             diag "Bogus test: contains only local flags\n", explain $localz;
266             return
267             }
268              
269             local @globalz{ keys %$localz } = values %$localz
270             if $localz;
271              
272             # if there is no expect value then skip the ok check and just
273             # run the method.
274              
275             my $handler
276             = @_> 1
277             ? $test_result
278             : $execute
279             ;
280              
281             my $error = $obj->$handler( @_ )
282             or return;
283              
284             $globalz{ continue }
285             or
286             die "Error during processing (continue turned off)\n" .
287             explain $error;
288              
289             return
290             };
291              
292             ########################################################################
293             # break up contents of exercise.
294              
295             sub validate_plan
296             {
297             # entrys are anything that gets run, expects have an
298             # tests value and issue a pass/fail for the test.
299              
300 10     10 0 19 my $entrys = 0;
301 10         19 my $tests = 0;
302              
303 10         24 for my $fieldz ( @_ )
304             {
305 80 100       131 ref $fieldz or next;
306              
307 68         84 my $n = $#$fieldz;
308              
309 68   50 68   341 my $i = first { ref $fieldz->[$_] } ( 0 .. $n )
  68         172  
310             // next;
311              
312 68         140 ++$entrys;
313              
314 68 100       128 $n > $i # i.e., exists $entry->[ 1 + $i ]
315             or next;
316              
317 17         39 ++$tests;
318             }
319              
320             $entrys
321             or
322 10 50       36 BAIL_OUT 'Bogus exercise: no executable entry', explain \@_;
323              
324             note "Executing: $entrys entrys ($tests with tests)"
325 10 100       35 if $globalz{ verbose };
326              
327             return
328 10         232 }
329              
330             sub prepare_tests
331             {
332 12 50   12 0 46 @_ or croak 'Bogus exercise: no tests on the stack';
333              
334 12 100 66     56 if
335             (
336             1 == @_
337             and
338             ! ref $_[0]
339             )
340             {
341 2 50 66     10 if
342             (
343             'finish' eq $_[0]
344             or
345             'nofinish' eq $_[0]
346             )
347             {
348 2         9 %globalz = ( %globalz, &extract_flags );
349             return
350 2         4 }
351             else
352             {
353             # anything else useful requires multiple
354             # entries at this point: the input is YAML.
355              
356 0         0 my $yaml = shift;
357              
358             note "Non-ref test: assume YAML\n$yaml",
359              
360 0         0 my $struct = eval { Load $yaml };
  0         0  
361              
362 0 0       0 BAIL_OUT "Invalid YAML: $@"
363             if $@;
364              
365 0 0       0 @$struct
366             or BAIL_OUT "Invalid YAML: empty content\n", $yaml;
367              
368 0         0 @_ = @$struct;
369             }
370             }
371            
372             # extract any flags floating at the start and
373             # leave the tests in place.
374              
375 10         64 %globalz = ( %globalz, &extract_flags );
376              
377             # @_ will be empty at this point if there were only flags.
378              
379 10 50       36 if( @_ )
    0          
380             {
381             # there is a plan on the stack
382              
383 10         35 &validate_plan
384             }
385             elsif( $globalz{ finish } )
386             {
387 0         0 croak
388             "Bogus plan: no tests and finish is true (missing 'nofinish'?)";
389             }
390             else
391             {
392             # no plan, no finish => fine.
393             }
394              
395             # at this point the first 'test' is at the head of the stack.
396              
397             return
398 10         15 }
399              
400              
401             ########################################################################
402             # interface pushed into caller via Object::Exercise
403             ########################################################################
404              
405             sub exercise
406             {
407 12     12 0 1175 my $obj = shift;
408              
409             # test seqeunce is left on the stack.
410              
411 12         38 &prepare_tests;
412              
413             # at this point the stack may be empty if the inputs were
414             # flags (e.g., 'finish', or 'nofinish').
415              
416 12         44 while( @_ )
417             {
418 71 100       141 if( my @flagz = &extract_flags )
419             {
420 3         23 %globalz = ( %globalz, @flagz );
421              
422             next
423 3         19 }
424              
425 68 50       143 if( my $entry = shift )
426             {
427 68         147 $obj->$process( @$entry );
428             }
429             else
430             {
431 0         0 croak "Bogus test: false entry ($entry).";
432             }
433             }
434              
435             done_testing
436 12 100       71 if $globalz{ finish };
437              
438             return
439 12         5940 }
440              
441             # keep require happy
442             1
443             __END__