File Coverage

blib/lib/Test/BDD/Cucumber/Executor.pm
Criterion Covered Total %
statement 255 274 93.0
branch 66 92 71.7
condition 22 35 62.8
subroutine 38 38 100.0
pod 9 9 100.0
total 390 448 87.0


line stmt bran cond sub pod time code
1             package Test::BDD::Cucumber::Executor;
2             $Test::BDD::Cucumber::Executor::VERSION = '0.84';
3             =head1 NAME
4              
5             Test::BDD::Cucumber::Executor - Run through Feature and Harness objects
6              
7             =head1 VERSION
8              
9             version 0.84
10              
11             =head1 DESCRIPTION
12              
13             The Executor runs through Features, matching up the Step Lines with Step
14             Definitions, and reporting on progress through the passed-in harness.
15              
16             =cut
17              
18 13     13   7889 use Moo;
  13         52414  
  13         90  
19 13     13   18303 use MooX::HandlesVia;
  13         126854  
  13         90  
20 13     13   4777 use Types::Standard qw( Bool Str ArrayRef HashRef );
  13         384303  
  13         206  
21 13     13   14257 use List::Util qw/first any/;
  13         31  
  13         1163  
22 13     13   123 use Module::Runtime qw/use_module/;
  13         30  
  13         112  
23 13     13   2575 use utf8;
  13         68  
  13         108  
24 13     13   5454 use Encode ();
  13         86224  
  13         373  
25              
26 13     13   738 use Test2::API qw/intercept/;
  13         73413  
  13         1149  
27              
28             # Use-ing the formatter results in a
29             # 'loaded too late to be used globally' warning
30             # But we only need it locally anyway.
31             require Test2::Formatter::TAP;
32              
33 13     13   6046 use Test2::Tools::Basic qw/ pass fail done_testing /;
  13         13897  
  13         1088  
34             # Needed for subtest() -- we don't want to import all its functions though
35             require Test::More;
36              
37 13     13   6219 use Test::BDD::Cucumber::StepFile ();
  13         37  
  13         448  
38 13     13   6929 use Test::BDD::Cucumber::StepContext;
  13         58  
  13         573  
39 13     13   6269 use Test::BDD::Cucumber::Util;
  13         36  
  13         447  
40 13     13   5214 use Test::BDD::Cucumber::Model::Result;
  13         38  
  13         465  
41 13     13   3287 use Test::BDD::Cucumber::Errors qw/parse_error_from_line/;
  13         37  
  13         34574  
42              
43             =head1 ATTRIBUTES
44              
45             =head2 matching
46              
47             The value of this attribute should be one of C (default), C and C.
48              
49             By default (C), the first matching step is executed immediately,
50             terminating the search for (further) matching steps. When C is set
51             to anything other than C, all steps are checked for matches. When set
52             to C, a warning will be generated on multiple matches. When set to
53             C, an exception will be thrown.
54              
55             =cut
56              
57             has matching => ( is => 'rw', isa => Str, default => 'first');
58              
59             =head1 METHODS
60              
61             =head2 extensions
62              
63             =head2 add_extensions
64              
65             The attributes C is an arrayref of
66             L extensions. Extensions have their
67             hook-functions called by the Executor at specific points in the BDD feature
68             execution.
69              
70             B> adds items in FIFO using unshift()>, and are called in
71             reverse order at the end hook; this means that if you:
72              
73             add_extensions( 1 );
74             add_extensions( 2, 3 );
75              
76             The C will be called in order 2, 3, 1, and C will be called in
77             1, 3, 2.
78              
79             =cut
80              
81             has extensions => (
82             is => 'ro',
83             isa => ArrayRef,
84             default => sub { [] },
85             handles_via => 'Array',
86             handles => { add_extensions => 'unshift' },
87             );
88              
89             =head2 steps
90              
91             =head2 add_steps
92              
93             The attributes C is a hashref of arrayrefs, storing steps by their Verb.
94             C takes step definitions of the item list form:
95              
96             (
97             [ Given => qr//, sub {} ],
98             ),
99              
100             Or, when metadata is specified with the step, of the form:
101              
102             (
103             [ Given => qr//, { meta => $data }, sub {} ]
104             ),
105              
106             (where the hashref stores step metadata) and populates C with them.
107              
108             =cut
109              
110             has 'steps' => ( is => 'rw', isa => HashRef, default => sub { {} } );
111              
112             sub add_steps {
113 40     40 1 648 my ( $self, @steps ) = @_;
114              
115             # Map the steps to be lower case...
116 40         136 for (@steps) {
117 135         241 my ( $verb, $match, $meta, $code );
118              
119 135 100       293 if (@$_ == 3) {
120 46         94 ( $verb, $match, $code ) = @$_;
121 46         82 $meta = {};
122             }
123             else {
124 89         206 ( $verb, $match, $meta, $code ) = @$_;
125             }
126 135         252 $verb = lc $verb;
127              
128 135 100       319 unless ( ref($match) ) {
129 44         130 $match =~ s/:\s*$//;
130 44         108 $match = quotemeta($match);
131 44         698 $match = qr/^$match:?/i;
132             }
133              
134 135 100 100     517 if ( $verb eq 'transform' or $verb eq 'after' ) {
135              
136             # Most recently defined Transform takes precedence
137             # and After blocks need to be run in reverse order
138 6         9 unshift( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  6         22  
139             } else {
140 129         205 push( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  129         663  
141             }
142              
143             }
144             }
145              
146             =head2 execute
147              
148             Execute accepts a feature object, a harness object, and an optional
149             L object and for each scenario in the
150             feature which meets the tag requirements (or all of them, if you
151             haven't specified one), runs C.
152              
153             =cut
154              
155             sub execute {
156 27     27 1 456 my ( $self, $feature, $harness, $tag_spec ) = @_;
157 27         73 my $feature_stash = {};
158              
159 27         168 $harness->feature($feature);
160 27 100       1351 my @background =
161             ( $feature->background ? ( background => $feature->background ) : () );
162              
163             # Get all scenarios
164 27         610 my @scenarios = @{ $feature->scenarios() };
  27         503  
165              
166 27         289 $_->pre_feature( $feature, $feature_stash ) for @{ $self->extensions };
  27         199  
167 27         112 for my $outline (@scenarios) {
168              
169             # Execute the scenario itself
170 84         669 $self->execute_outline(
171             {
172             @background,
173             scenario => $outline,
174             feature => $feature,
175             feature_stash => $feature_stash,
176             harness => $harness,
177             tagspec => $tag_spec,
178             }
179             );
180             }
181             $_->post_feature( $feature, $feature_stash, 'no' )
182 27         79 for reverse @{ $self->extensions };
  27         144  
183              
184 27         206 $harness->feature_done($feature);
185             }
186              
187             =head2 execute_outline
188              
189             Accepts a hashref of options and executes each scenario definition in the
190             scenario outline, or, lacking an outline, executes the single defined
191             scenario.
192              
193             Options:
194              
195             C< feature > - A L object
196              
197             C< feature_stash > - A hashref that should live the lifetime of
198             feature execution
199              
200             C< harness > - A L subclass object
201              
202             C< outline > - A L object
203              
204             C< background > - An optional L object
205             representing the Background
206              
207             =cut
208              
209             sub execute_outline {
210 84     84 1 235 my ( $self, $options ) = @_;
211             my ( $feature, $feature_stash, $harness, $outline, $background, $tagspec )
212 84         358 = @$options{qw/ feature feature_stash harness scenario background tagspec /};
213              
214             # Multiply out Scenario Outlines as appropriate
215 84         148 my @datasets = @{ $outline->datasets };
  84         1898  
216 84 100       1024 if (not @datasets) {
217 69 50 66     299 if (not $tagspec or $tagspec->filter($outline) ) {
218 69         527 $self->execute_scenario(
219             {
220             feature => $feature,
221             feature_stash => $feature_stash,
222             harness => $harness,
223             scenario => $outline,
224             background => $background,
225             scenario_stash => {},
226             dataset => {},
227             });
228             }
229              
230 69         825 return;
231             }
232              
233 15 50       69 if ($tagspec) {
234 0         0 @datasets = $tagspec->filter(@datasets);
235 0 0       0 return unless @datasets;
236             }
237              
238              
239 15         62 foreach my $rows (@datasets) {
240              
241 15         31 foreach my $row (@{$rows->data}) {
  15         285  
242              
243 38   100     344 my $name = $outline->{name} || "";
244             $name =~ s/\Q<$_>\E/$row->{$_}/g
245 38         1254 for (keys %$row);
246 38         183 local $outline->{name} = $name;
247              
248 38         316 $self->execute_scenario(
249             {
250             feature => $feature,
251             feature_stash => $feature_stash,
252             harness => $harness,
253             scenario => $outline,
254             background => $background,
255             scenario_stash => {},
256             dataset => $row,
257             });
258             }
259             }
260             }
261              
262             =head2 execute_scenario
263              
264             Accepts a hashref of options, and executes each step in a scenario. Options:
265              
266             C - A L object
267              
268             C - A hashref that should live the lifetime of feature execution
269              
270             C - A L subclass object
271              
272             C - A L object
273              
274             C - An optional L object
275             representing the Background
276              
277             C - A hashref that lives the lifetime of the scenario execution
278              
279             For each step, a L object is created, and
280             passed to C. Nothing is returned - everything is played back through
281             the Harness interface.
282              
283             =cut
284              
285              
286             sub _execute_steps {
287 149     149   425 my ( $self, $options ) = @_;
288             my ( $feature, $feature_stash, $harness, $outline,
289             $scenario_stash, $scenario_state, $dataset, $context_defaults )
290             = @$options{
291 149         619 qw/ feature feature_stash harness scenario scenario_stash
292             scenario_state dataset context_defaults
293             /
294             };
295              
296              
297 149         293 foreach my $step ( @{ $outline->steps } ) {
  149         3433  
298              
299             # Multiply out any placeholders
300 435         11163 my $text =
301             $self->add_placeholders( $step->text, $dataset, $step->line );
302 435         1265 my $data = $step->data;
303 435 100       1918 $data = (ref $data) ?
    100          
304             $self->add_table_placeholders( $data, $dataset, $step->line )
305             : (defined $data) ?
306             $self->add_placeholders( $data, $dataset, $step->line )
307             : '';
308              
309             # Set up a context
310 435   100     8829 my $context = Test::BDD::Cucumber::StepContext->new(
311             {
312             %$context_defaults,
313              
314             # Data portion
315             columns => $step->columns || [],
316             data => $data,
317              
318             # Step-specific info
319             step => $step,
320             verb => lc( $step->verb ),
321             text => $text,
322             }
323             );
324              
325             my $result =
326             $self->find_and_dispatch( $context,
327 435         91833 $scenario_state->{'short_circuit'}, 0 );
328              
329             # If it didn't pass, short-circuit the rest
330 435 100       3404 unless ( $result->result eq 'passing' ) {
331 26         204 $scenario_state->{'short_circuit'}++;
332             }
333              
334             }
335              
336 149         459 return;
337             }
338              
339              
340             sub _execute_hook_steps {
341 236     236   736 my ( $self, $phase, $context_defaults, $scenario_state ) = @_;
342 236         516 my $want_short = ($phase eq 'before');
343              
344 236 100       385 for my $step ( @{ $self->{'steps'}->{$phase} || [] } ) {
  236         1231  
345              
346 192         6282 my $context = Test::BDD::Cucumber::StepContext->new(
347             { %$context_defaults, verb => $phase, } );
348              
349             my $result =
350             $self->dispatch(
351             $context, $step,
352 192 100       34870 ($want_short ? $scenario_state->{'short_circuit'} : 0),
353             0 );
354              
355             # If it didn't pass, short-circuit the rest
356 192 50       1607 unless ( $result->result eq 'passing' ) {
357 0 0       0 if ($want_short) {
358 0         0 $scenario_state->{'short_circuit'} = 1;
359             }
360             }
361             }
362              
363 236         661 return;
364             }
365              
366              
367             sub execute_scenario {
368 118     118 1 332 my ( $self, $options ) = @_;
369             my ( $feature, $feature_stash, $harness, $outline, $background_obj,
370             $scenario_stash, $dataset )
371             = @$options{
372 118         511 qw/ feature feature_stash harness scenario background scenario_stash
373             dataset
374             /
375             };
376 118         252 my $scenario_state = {};
377              
378             my %context_defaults = (
379             executor => $self, # Held weakly by StepContext
380              
381             # Data portion
382             data => '',
383             stash => {
384             feature => $feature_stash,
385             step => {},
386             },
387              
388             # Step-specific info
389             feature => $feature,
390             scenario => $outline,
391              
392             # Communicators
393             harness => $harness,
394              
395 118   100     1149 transformers => $self->{'steps'}->{'transform'} || [],
396             );
397 118         302 $context_defaults{stash}->{scenario} = $scenario_stash;
398              
399             $harness->scenario( $outline, $dataset,
400 118         671 $scenario_stash->{'longest_step_line'} );
401              
402             $_->pre_scenario( $outline, $feature_stash, $scenario_stash )
403 118         4067 for @{ $self->extensions };
  118         621  
404              
405 118         583 $self->_execute_hook_steps( 'before', \%context_defaults, $scenario_state );
406              
407 118 100       457 if ($background_obj) {
408             $harness->background( $outline, $dataset,
409 31         274 $scenario_stash->{'longest_step_line'} );
410 31         279 $self->_execute_steps(
411             {
412             scenario => $background_obj,
413             feature => $feature,
414             feature_stash => $feature_stash,
415             harness => $harness,
416             scenario_stash => $scenario_stash,
417             scenario_state => $scenario_state,
418             context_defaults => \%context_defaults,
419             }
420             );
421 31         200 $harness->background_done( $outline, $dataset );
422             }
423              
424             $self->_execute_steps(
425             {
426 118         1201 scenario => $outline,
427             feature => $feature,
428             feature_stash => $feature_stash,
429             harness => $harness,
430             scenario_stash => $scenario_stash,
431             scenario_state => $scenario_state,
432             dataset => $dataset,
433             context_defaults => \%context_defaults,
434             });
435              
436 118         801 $self->_execute_hook_steps( 'after', \%context_defaults, $scenario_state );
437              
438             $_->post_scenario( $outline, $feature_stash, $scenario_stash,
439             $scenario_state->{'short_circuit'} )
440 118         248 for reverse @{ $self->extensions };
  118         579  
441              
442 118         668 $harness->scenario_done( $outline, $dataset );
443              
444 118         1953 return;
445             }
446              
447             =head2 add_placeholders
448              
449             Accepts a text string and a hashref, and replaces C< > with the
450             values in the hashref, returning a string.
451              
452             =cut
453              
454             sub add_placeholders {
455 496     496 1 12879 my ( $self, $text, $dataset, $line ) = @_;
456 496         1696 my $quoted_text = Test::BDD::Cucumber::Util::bs_quote($text);
457 496         1624 $quoted_text =~ s/(<([^>]+)>)/
458 85 50       637 exists $dataset->{$2} ? $dataset->{$2} :
459             die parse_error_from_line( "No mapping to placeholder $1", $line )
460             /eg;
461 496         1449 return Test::BDD::Cucumber::Util::bs_unquote($quoted_text);
462             }
463              
464              
465             =head2 add_table_placeholders
466              
467             Accepts a hash with parsed table data and a hashref, and replaces
468             C< > with the values in the hashref, returning a copy of the
469             parsed table hashref.
470              
471             =cut
472              
473             sub add_table_placeholders {
474 7     7 1 89 my ($self, $tbl, $dataset, $line) = @_;
475             my @rv = map {
476 7         26 my $row = $_;
  17         37  
477             my %inner_rv =
478 17         74 map { $_ => $self->add_placeholders($row->{$_}, $dataset, $line)
  39         104  
479             } keys %$row;
480 17         64 \%inner_rv;
481             } @$tbl;
482 7         35 return \@rv;
483             }
484              
485              
486             =head2 find_and_dispatch
487              
488             Accepts a L object, and searches through
489             the steps that have been added to the executor object, executing against the
490             first matching one (unless C<$self->matching> indicates otherwise).
491              
492             You can also pass in a boolean 'short-circuit' flag if the Scenario's remaining
493             steps should be skipped, and a boolean flag to denote if it's a redispatched
494             step.
495              
496             =cut
497              
498             sub find_and_dispatch {
499 447     447 1 1718 my ( $self, $context, $short_circuit, $redispatch ) = @_;
500              
501             # Short-circuit if we need to
502 447 100       1244 return $self->skip_step( $context, 'pending',
503             "Short-circuited from previous tests", 0 )
504             if $short_circuit;
505              
506             # Try and find a matching step
507 434         669 my $stepdef;
508 434         1071 my $text = $context->text;
509 434 50       9489 if ($self->matching eq 'first') {
510 1572     1572   9123 $stepdef = first { $text =~ $_->[0] }
511 434 100       2213 @{ $self->{'steps'}->{ $context->verb } || [] },
512 434 100       4724 @{ $self->{'steps'}->{'step'} || [] };
  434         2583  
513             }
514             else {
515 0         0 my @stepdefs = grep { $text =~ $_->[0] }
516 0 0       0 @{ $self->{'steps'}->{ $context->verb } || [] },
517 0 0       0 @{ $self->{'steps'}->{'step'} || [] };
  0         0  
518              
519 0 0       0 if (@stepdefs > 1) {
520 0         0 my $filename = $context->step->line->document->filename;
521 0         0 my $line = $context->step->line->number;
522             my $msg =
523             join("\n ",
524             qq(Step "$text" ($filename:$line) matches multiple step functions:),
525             map {
526 0         0 qq{matcher $_->[0] defined at } .
527             (($_->[1]->{source} && $_->[1]->{line})
528 0 0 0     0 ? "$_->[1]->{source}:$_->[1]->{line}"
529             : '') } @stepdefs);
530              
531 0 0       0 if ($self->matching eq 'relaxed') {
532 0         0 warn $msg;
533             }
534             else {
535 0         0 die $msg;
536             }
537             }
538 0         0 $stepdef = shift @stepdefs;
539             }
540              
541             # Deal with the simple case of no-match first of all
542 434 100       2171 unless ($stepdef) {
543 4         47 my $message =
544             "No matching step definition for: "
545             . $context->verb . ' '
546             . $context->text;
547 4         19 my $result =
548             $self->skip_step( $context, 'undefined', $message, $redispatch );
549 4         31 return $result;
550             }
551              
552 430         811 $_->pre_step( $stepdef, $context ) for @{ $self->extensions };
  430         1472  
553 430         1510 my $result = $self->dispatch( $context, $stepdef, 0, $redispatch );
554             $_->post_step( $stepdef, $context,
555             ( $result->result ne 'passing' ), $result )
556 430         1138 for reverse @{ $self->extensions };
  430         1711  
557 430         1179 return $result;
558             }
559              
560             =head2 dispatch($context, $stepdef, $short_circuit, $redispatch)
561              
562             Accepts a L object, and a
563             reference to a step definition triplet (verb, metadata hashref, coderef)
564             and executes it the coderef.
565              
566             You can also pass in a boolean 'short-circuit' flag if the Scenario's remaining
567             steps should be skipped.
568              
569             =cut
570              
571             sub dispatch {
572 622     622 1 1787 my ( $self, $context, $stepdef, $short_circuit, $redispatch ) = @_;
573              
574 622 50       1530 return $self->skip_step( $context, 'pending',
575             "Short-circuited from previous tests", $redispatch )
576             if $short_circuit;
577              
578             # Execute the step definition
579 622         1626 my ( $regular_expression, $meta, $coderef ) = @$stepdef;
580              
581 622 100       1423 my $step_name = $redispatch ? 'sub_step' : 'step';
582 622         1323 my $step_done_name = $step_name . '_done';
583              
584             # Say we're about to start it up
585 622         3133 $context->harness->$step_name($context);
586              
587 622         3878 my @match_locations;
588 622         962 my $stash_keys = join ';', sort keys %{$context->stash};
  622         3790  
589             # Using `intercept()`, run the step function in an isolated
590             # environment -- this should not affect the enclosing scope
591             # which might be a TAP::Harness scope.
592             #
593             # Instead, we want the tests inside this scope to map to
594             # status values
595             my $events = intercept {
596             # This is a hack to make Test::More's $TODO variable work
597             # inside the intercepted scope.
598              
599             ###TODO: Both intercept() and Test::More::subtest() should
600             # be replaced by a specific Hub implementation for T::B::C
601             Test::More::subtest( 'execute step', sub {
602              
603             # Take a copy of this. Turns out actually matching against it
604             # directly causes all sorts of weird-ass heisenbugs which mst has
605             # promised to investigate.
606 622         604691 my $text = $context->text;
607              
608             # Save the matches
609 622         8358 $context->matches( [ $text =~ $regular_expression ] );
610              
611             # Save the location of matched subgroups for highlighting hijinks
612 622         2956 my @starts = @-;
613 622         2462 my @ends = @+;
614              
615             # Store the string position of matches for highlighting
616 622         1588 @match_locations = map { [ $_, shift @ends ] } @starts;
  1040         3096  
617              
618             # OK, actually execute
619 622         1137 local $@;
620 622         1236 eval {
621 13     13   156 no warnings 'redefine';
  13         72  
  13         16236  
622              
623             local *Test::BDD::Cucumber::StepFile::_S = sub {
624 702         5077 return $context->stash->{'scenario'};
625 622         3467 };
626             local *Test::BDD::Cucumber::StepFile::_C = sub {
627 265         944 return $context;
628 622         2124 };
629              
630 622         3073 $coderef->($context)
631             };
632 622 50       159616 if ($@) {
633 0         0 fail("Step ran to completion", "Exception: ", $@);
634             }
635             else {
636 622         2099 pass("Step ran to completion");
637             }
638              
639 622         199988 done_testing();
640 622     622   234811 });
641 622         4193 };
642              
643 622         1268778 my $status = $self->_test_status( $events );
644              
645             my $result = Test::BDD::Cucumber::Model::Result->new(
646             {
647             result => $status,
648             # due to the hack above with the subtest inside the
649             # interception scope, we need to grovel the subtest
650             # from out of the other results first.
651             output => $self->_test_output(
652 1244     1244   5828 (first { $_->isa('Test2::Event::Subtest') }
653 622         3847 @$events)->{subevents}),
654             });
655             warn qq|Unsupported: Step modified C->stash instead of C->stash->{scenario} or C->stash->{feature}|
656 622 50       106744 if $stash_keys ne (join ';', sort keys %{$context->stash});
  622         5201  
657              
658 622         3033 my @clean_matches =
659             $self->_extract_match_strings( $context->text, \@match_locations );
660 622 100       2018 @clean_matches = [ 0, $context->text ] unless @clean_matches;
661              
662             # Say the step is done, and return the result. Happens outside
663             # the above block so that we don't have the localized harness
664             # anymore...
665 622 100       3767 $context->harness->add_result($result) unless $redispatch;
666 622         3048 $context->harness->$step_done_name( $context, $result, \@clean_matches );
667 622         28516 return $result;
668             }
669              
670             sub _extract_match_strings {
671 622     622   1629 my ( $self, $text, $locations ) = @_;
672              
673             # Clean up the match locations
674             my @match_locations = grep {
675 1040 100 100     5293 ( $_->[0] != $_->[1] ) && # No zero-length matches
676             # And nothing that matched the full string
677             ( !( ( $_->[0] == 0 ) && ( ( $_->[1] == length $text ) ) ) )
678             } grep {
679 622 50 33     1273 defined $_ && ref $_ && defined $_->[0] && defined $_->[1]
  1040   33     6959  
680             } @$locations;
681              
682 622 100       1977 return unless @match_locations;
683              
684             my %range =
685 325         695 map { $_ => 1 } map { $_->[0] .. ($_->[1] - 1) } @match_locations;
  3355         7314  
  407         1331  
686              
687             # Walk the string, splitting
688 325         1312 my @parts = ( [ 0, '' ] );
689 325         1140 for ( 0 .. ( ( length $text ) - 1 ) ) {
690 10149   100     22985 my $to_highlight = $range{$_} || 0;
691 10149         15831 my $character = substr( $text, $_, 1 );
692              
693 10149 100       17974 if ( $parts[-1]->[0] != $to_highlight ) {
694 654         1444 push( @parts, [ $to_highlight, '' ] );
695             }
696              
697 10149         18919 $parts[-1]->[1] .= $character;
698             }
699              
700 325         1479 return @parts;
701             }
702              
703             sub _test_output {
704 622     622   1435 my ($self, $events) = @_;
705 622         3694 my $fmt = Test2::Formatter::TAP->new();
706 9     9   111 open my $stdout, '>:encoding(UTF-8)', \my $out_text;
  9     9   20  
  9         100  
  9         7421  
  9         26  
  9         48  
  622         126113  
707 622         43940 my $idx = 0;
708              
709 622         2538 $fmt->set_handles([ $stdout, $stdout ]);
710 622         13315 $self->_test_output_from_subevents($events, $fmt, \$idx);
711 622         80554 close $stdout;
712              
713 622         2366 return Encode::decode('utf8', $out_text);
714             }
715              
716             sub _test_output_from_subevents {
717 622     622   1586 my ($self, $events, $fmt, $idx) = @_;
718              
719 622         1533 for my $event (@$events) {
720 1652 50       46371 if ($event->{subevents}) {
721             $self->_test_output_from_subevents(
722 0         0 $event->{subevents}, $fmt, $idx);
723             }
724             else {
725 1652         4710 $fmt->write($event, $$idx++);
726             }
727             }
728             }
729              
730             sub _test_status {
731 622     622   1356 my $self = shift;
732 622         941 my $events = shift;
733              
734 622 100       3785 if (any { defined $_->{effective_pass}
735 1244 100   1244   5117 and ! $_->{effective_pass} } @$events) {
736 6         62 return 'failing';
737             }
738             else {
739 616 100       1887 return $self->_test_status_from_subevents($events) ? 'pending' : 'passing';
740             }
741             }
742              
743             sub _test_status_from_subevents {
744 1232     1232   1869 my $self = shift;
745 1232         1782 my $events = shift;
746              
747 1232         2249 for my $e (@$events) {
748 2841 100 66     7815 if (exists $e->{subevents}) {
    100 33        
      33        
749             $self->_test_status_from_subevents($e->{subevents})
750 616 100       1437 and return 1;
751             }
752             elsif (defined $e->{amnesty}
753             and $e->{effective_pass}
754             and (not $e->{pass})
755 3     3   13 and any { $_->{tag} eq 'TODO' } @{$e->{amnesty}}) {
  3         13  
756 3         28 return 1;
757             }
758             }
759              
760 1226         4113 return 0;
761             }
762              
763             =head2 skip_step
764              
765             Accepts a step-context, a result-type, and a textual reason, exercises the
766             Harness's step start and step_done methods, and returns a skipped-test result.
767              
768             =cut
769              
770             sub skip_step {
771 17     17 1 58 my ( $self, $context, $type, $reason, $redispatch ) = @_;
772              
773 17 50       57 my $step_name = $redispatch ? 'sub_step' : 'step';
774 17         44 my $step_done_name = $step_name . '_done';
775              
776             # Pretend to start step execution
777 17         102 $context->harness->$step_name($context);
778              
779             # Create a result object
780 17         720 my $result = Test::BDD::Cucumber::Model::Result->new(
781             {
782             result => $type,
783             output => '1..0 # SKIP ' . $reason
784             }
785             );
786              
787             # Pretend we executed it
788 17 50       1095 $context->harness->add_result($result) unless $redispatch;
789 17         89 $context->harness->$step_done_name( $context, $result );
790 17         220 return $result;
791             }
792              
793             =head1 AUTHOR
794              
795             Peter Sergeant C
796              
797             =head1 LICENSE
798              
799             Copyright 2019-2023, Erik Huelsmann
800             Copyright 2011-2019, Peter Sergeant; Licensed under the same terms as Perl
801              
802             =cut
803              
804             1;