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