File Coverage

blib/lib/Test/BDD/Cucumber/Executor.pm
Criterion Covered Total %
statement 267 290 92.0
branch 68 94 72.3
condition 23 38 60.5
subroutine 41 41 100.0
pod 9 9 100.0
total 408 472 86.4


line stmt bran cond sub pod time code
1              
2 22     22   12748 use v5.14;
  22         105  
3 22     22   191 use warnings;
  22         43  
  22         2162  
4              
5             package Test::BDD::Cucumber::Executor 0.87;
6              
7             =head1 NAME
8              
9             Test::BDD::Cucumber::Executor - Run through Feature and Harness objects
10              
11             =head1 VERSION
12              
13             version 0.87
14              
15             =head1 DESCRIPTION
16              
17             The Executor runs through Features, matching up the Step Lines with Step
18             Definitions, and reporting on progress through the passed-in harness.
19              
20             =cut
21              
22 22     22   1471 use Moo;
  22         20557  
  22         202  
23 22     22   32230 use MooX::HandlesVia;
  22         290650  
  22         168  
24 22     22   13325 use Types::Standard qw( Bool Str ArrayRef HashRef );
  22         1833332  
  22         289  
25 22     22   70344 use List::Util qw/first any/;
  22         63  
  22         2555  
26 22     22   159 use Module::Runtime qw/use_module/;
  22         51  
  22         204  
27 22     22   3160 use utf8;
  22         1020  
  22         174  
28 22     22   943 use Carp qw(carp croak);
  22         41  
  22         1549  
29 22     22   6100 use Encode ();
  22         184959  
  22         935  
30              
31 22     22   7047 use Test2::API qw/intercept/;
  22         724565  
  22         3302  
32              
33             # Use-ing the formatter results in a
34             # 'loaded too late to be used globally' warning
35             # But we only need it locally anyway.
36             require Test2::Formatter::TAP;
37              
38 22     22   14434 use Test2::Tools::Basic qw/ pass fail done_testing /;
  22         29367  
  22         2412  
39             # Needed for subtest() -- we don't want to import all its functions though
40             require Test::More;
41              
42 22     22   12985 use Test::BDD::Cucumber::StepFile ();
  22         109  
  22         1178  
43 22     22   17080 use Test::BDD::Cucumber::StepContext;
  22         287  
  22         1306  
44 22     22   12946 use Test::BDD::Cucumber::Util;
  22         76  
  22         904  
45 22     22   13319 use Test::BDD::Cucumber::Model::Result;
  22         97  
  22         1400  
46 22     22   9470 use Test::BDD::Cucumber::Errors qw/parse_error_from_line/;
  22         69  
  22         67766  
47              
48             =head1 ATTRIBUTES
49              
50             =head2 matching
51              
52             The value of this attribute should be one of C (default), C and C.
53              
54             By default (C), the first matching step is executed immediately,
55             terminating the search for (further) matching steps. When C is set
56             to anything other than C, all steps are checked for matches. When set
57             to C, a warning will be generated on multiple matches. When set to
58             C, an exception will be thrown.
59              
60             =cut
61              
62             has matching => ( is => 'rw', isa => Str, default => 'first');
63              
64             =head1 METHODS
65              
66             =head2 extensions
67              
68             =head2 add_extensions
69              
70             The attributes C is an arrayref of
71             L extensions. Extensions have their
72             hook-functions called by the Executor at specific points in the BDD feature
73             execution.
74              
75             B> adds items in FIFO using unshift()>, and are called in
76             reverse order at the end hook; this means that if you:
77              
78             add_extensions( 1 );
79             add_extensions( 2, 3 );
80              
81             The C will be called in order 2, 3, 1, and C will be called in
82             1, 3, 2.
83              
84             =cut
85              
86             has extensions => (
87             is => 'ro',
88             isa => ArrayRef,
89             default => sub { [] },
90             handles_via => 'Array',
91             handles => { add_extensions => 'unshift' },
92             );
93              
94             =head2 steps
95              
96             =head2 add_steps
97              
98             The attributes C is a hashref of arrayrefs, storing steps by their Verb.
99             C takes step definitions of the item list form:
100              
101             (
102             [ Given => qr//, sub {} ],
103             ),
104              
105             Or, when metadata is specified with the step, of the form:
106              
107             (
108             [ Given => qr//, { meta => $data }, sub {} ]
109             ),
110              
111             (where the hashref stores step metadata) and populates C with them.
112              
113             =cut
114              
115             has 'steps' => ( is => 'rw', isa => HashRef, default => sub { {} } );
116              
117             sub add_steps {
118 53     53 1 760 my ( $self, @steps ) = @_;
119              
120             # Map the steps to be lower case...
121 53         176 for (@steps) {
122 196         356 my ( $verb, $match, $meta, $code );
123              
124 196 100       470 if (@$_ == 3) {
125 46         119 ( $verb, $match, $code ) = @$_;
126 46         86 $meta = {};
127             }
128             else {
129 150         375 ( $verb, $match, $meta, $code ) = @$_;
130             }
131 196         447 $verb = lc $verb;
132              
133 196 100       517 unless ( ref($match) ) {
134 59         191 $match =~ s/:\s*$//;
135 59         147 $match = quotemeta($match);
136 59         1550 $match = qr/^$match:?/i;
137             }
138              
139 196 100 100     817 if ( $verb eq 'transform' or $verb eq 'after' ) {
140              
141             # Most recently defined Transform takes precedence
142             # and After blocks need to be run in reverse order
143 12         21 unshift( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  12         96  
144             } else {
145 184         299 push( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  184         1042  
146             }
147              
148             }
149             }
150              
151             =head2 execute
152              
153             Execute accepts a feature object, a harness object, and an optional
154             L object and for each scenario in the
155             feature which meets the tag requirements (or all of them, if you
156             haven't specified one), runs C.
157              
158             =cut
159              
160             sub execute {
161 40     40 1 471 my ( $self, $feature, $harness, $tag_spec ) = @_;
162 40         148 my $feature_stash = {};
163              
164 40         284 $harness->feature($feature);
165 40 100       2090 my @background =
166             ( $feature->background ? ( background => $feature->background ) : () );
167              
168             # Get all scenarios
169 40         702 my @scenarios = @{ $feature->scenarios() };
  40         786  
170              
171 40         300 $_->pre_feature( $feature, $feature_stash ) for @{ $self->extensions };
  40         268  
172 40         123 for my $outline (@scenarios) {
173              
174             # Execute the scenario itself
175 141         1423 $self->execute_outline(
176             {
177             @background,
178             scenario => $outline,
179             feature => $feature,
180             feature_stash => $feature_stash,
181             harness => $harness,
182             tagspec => $tag_spec,
183             }
184             );
185             }
186             $_->post_feature( $feature, $feature_stash, 'no' )
187 40         98 for reverse @{ $self->extensions };
  40         212  
188              
189 40         302 $harness->feature_done($feature);
190             }
191              
192             =head2 execute_outline
193              
194             Accepts a hashref of options and executes each scenario definition in the
195             scenario outline, or, lacking an outline, executes the single defined
196             scenario.
197              
198             Options:
199              
200             C< feature > - A L object
201              
202             C< feature_stash > - A hashref that should live the lifetime of
203             feature execution
204              
205             C< harness > - A L subclass object
206              
207             C< outline > - A L object
208              
209             C< background > - An optional L object
210             representing the Background
211              
212             =cut
213              
214             sub _match_tags {
215 12     12   57 my ($spec, @tagged_components) = @_;
216 12         24 state $deprecation_warned = 0;
217              
218 12 50       95 if ($spec->isa('Cucumber::TagExpressions::ExpressionNode')) {
219             return grep {
220 12         27 $spec->evaluate( @{ $_->tags } )
  12         44  
  12         325  
221             } @tagged_components;
222             }
223             else {
224 0   0     0 $deprecation_warned ||=
225             carp 'Test::BDD::Cucumber::Model::TagSpec is deprecated; replace with Cucumber::TagExpressions';
226              
227 0         0 return $spec->filter( @tagged_components );
228             }
229             }
230              
231             sub execute_outline {
232 141     141 1 430 my ( $self, $options ) = @_;
233             my ( $feature, $feature_stash, $harness, $outline, $background, $tagspec )
234 141         695 = @$options{qw/ feature feature_stash harness scenario background tagspec /};
235              
236             # Multiply out Scenario Outlines as appropriate
237 141         330 my @datasets = @{ $outline->datasets };
  141         4258  
238 141 100       1544 if (not @datasets) {
239 116 100 100     554 if (not $tagspec or _match_tags( $tagspec, $outline )) {
240 110         1531 $self->execute_scenario(
241             {
242             feature => $feature,
243             feature_stash => $feature_stash,
244             harness => $harness,
245             scenario => $outline,
246             background => $background,
247             scenario_stash => {},
248             dataset => {},
249             });
250             }
251 116         2178 $harness->scenario_skip( $outline, {} );
252              
253 116         3435 return;
254             }
255              
256 25 50       141 if ($tagspec) {
257 0         0 @datasets = _match_tags( $tagspec, @datasets );
258              
259 0 0       0 unless (@datasets) {
260 0         0 $harness->scenario_skip( $outline, {} );
261 0         0 return;
262             }
263             }
264              
265              
266 25         78 foreach my $rows (@datasets) {
267              
268 25         51 foreach my $row (@{$rows->data}) {
  25         612  
269              
270 71   100     602 my $name = $outline->{name} || "";
271             $name =~ s/\Q<$_>\E/$row->{$_}/g
272 71         2521 for (keys %$row);
273 71         311 local $outline->{name} = $name;
274              
275 71         875 $self->execute_scenario(
276             {
277             feature => $feature,
278             feature_stash => $feature_stash,
279             harness => $harness,
280             scenario => $outline,
281             background => $background,
282             scenario_stash => {},
283             dataset => $row,
284             });
285             }
286             }
287             }
288              
289             =head2 execute_scenario
290              
291             Accepts a hashref of options, and executes each step in a scenario. Options:
292              
293             C - A L object
294              
295             C - A hashref that should live the lifetime of feature execution
296              
297             C - A L subclass object
298              
299             C - A L object
300              
301             C - An optional L object
302             representing the Background
303              
304             C - A hashref that lives the lifetime of the scenario execution
305              
306             For each step, a L object is created, and
307             passed to C. Nothing is returned - everything is played back through
308             the Harness interface.
309              
310             =cut
311              
312              
313             sub _execute_steps {
314 258     258   817 my ( $self, $options ) = @_;
315             my ( $feature, $feature_stash, $harness, $outline,
316             $scenario_stash, $scenario_state, $dataset, $context_defaults )
317             = @$options{
318 258         1374 qw/ feature feature_stash harness scenario scenario_stash
319             scenario_state dataset context_defaults
320             /
321             };
322              
323              
324 258         596 foreach my $step ( @{ $outline->steps } ) {
  258         7721  
325              
326             # Multiply out any placeholders
327 729         21696 my $text =
328             $self->add_placeholders( $step->text, $dataset, $step->line );
329 729         2804 my $data = $step->data;
330 729 100       3980 $data = (ref $data) ?
    100          
331             $self->add_table_placeholders( $data, $dataset, $step->line )
332             : (defined $data) ?
333             $self->add_placeholders( $data, $dataset, $step->line )
334             : '';
335              
336             # Set up a context
337 729   100     20098 my $context = Test::BDD::Cucumber::StepContext->new(
338             {
339             %$context_defaults,
340              
341             # Data portion
342             columns => $step->columns || [],
343             data => $data,
344              
345             # Step-specific info
346             step => $step,
347             verb => lc( $step->verb ),
348             text => $text,
349             }
350             );
351              
352             my $result =
353             $self->find_and_dispatch( $context,
354 729         193863 $scenario_state->{'short_circuit'}, 0 );
355              
356             # If it didn't pass, short-circuit the rest
357 729 100       8397 unless ( $result->result eq 'passing' ) {
358 35         278 $scenario_state->{'short_circuit'}++;
359             }
360              
361             }
362              
363 258         949 return;
364             }
365              
366              
367             sub _execute_hook_steps {
368 384     384   1373 my ( $self, $phase, $context_defaults, $scenario_state ) = @_;
369 384         980 my $want_short = ($phase eq 'before');
370              
371 384 100       813 for my $step ( @{ $self->{'steps'}->{$phase} || [] } ) {
  384         2531  
372              
373 258         10166 my $context = Test::BDD::Cucumber::StepContext->new(
374             { %$context_defaults, verb => $phase, } );
375              
376             my $result =
377             $self->dispatch(
378             $context, $step,
379 258 100       58439 ($want_short ? $scenario_state->{'short_circuit'} : 0),
380             0 );
381              
382             # If it didn't pass, short-circuit the rest
383 258 50       2722 unless ( $result->result eq 'passing' ) {
384 0 0       0 if ($want_short) {
385 0         0 $scenario_state->{'short_circuit'} = 1;
386             }
387             }
388             }
389              
390 384         1154 return;
391             }
392              
393              
394             sub execute_scenario {
395 192     192 1 622 my ( $self, $options ) = @_;
396             my ( $feature, $feature_stash, $harness, $outline, $background_obj,
397             $scenario_stash, $dataset )
398             = @$options{
399 192         990 qw/ feature feature_stash harness scenario background scenario_stash
400             dataset
401             /
402             };
403 192         426 my $scenario_state = {};
404              
405             my %context_defaults = (
406             executor => $self, # Held weakly by StepContext
407              
408             # Data portion
409             data => '',
410             stash => {
411             feature => $feature_stash,
412             step => {},
413             },
414              
415             # Step-specific info
416             feature => $feature,
417             scenario => $outline,
418              
419             # Communicators
420             harness => $harness,
421              
422 192   100     2727 transformers => $self->{'steps'}->{'transform'} || [],
423             );
424 192         587 $context_defaults{stash}->{scenario} = $scenario_stash;
425              
426             $harness->scenario( $outline, $dataset,
427 192         1398 $scenario_stash->{'longest_step_line'} );
428              
429             $_->pre_scenario( $outline, $feature_stash, $scenario_stash )
430 192         5899 for @{ $self->extensions };
  192         1074  
431              
432 192         1106 $self->_execute_hook_steps( 'before', \%context_defaults, $scenario_state );
433              
434 192 100       696 if ($background_obj) {
435             $harness->background( $outline, $dataset,
436 66         570 $scenario_stash->{'longest_step_line'} );
437 66         746 $self->_execute_steps(
438             {
439             scenario => $background_obj,
440             feature => $feature,
441             feature_stash => $feature_stash,
442             harness => $harness,
443             scenario_stash => $scenario_stash,
444             scenario_state => $scenario_state,
445             context_defaults => \%context_defaults,
446             }
447             );
448 66         593 $harness->background_done( $outline, $dataset );
449             }
450              
451             $self->_execute_steps(
452             {
453 192         2374 scenario => $outline,
454             feature => $feature,
455             feature_stash => $feature_stash,
456             harness => $harness,
457             scenario_stash => $scenario_stash,
458             scenario_state => $scenario_state,
459             dataset => $dataset,
460             context_defaults => \%context_defaults,
461             });
462              
463 192         1526 $self->_execute_hook_steps( 'after', \%context_defaults, $scenario_state );
464              
465             $_->post_scenario( $outline, $feature_stash, $scenario_stash,
466             $scenario_state->{'short_circuit'} )
467 192         429 for reverse @{ $self->extensions };
  192         958  
468              
469 192         1145 $harness->scenario_done( $outline, $dataset );
470              
471 192         3640 return;
472             }
473              
474             =head2 add_placeholders
475              
476             Accepts a text string and a hashref, and replaces C< > with the
477             values in the hashref, returning a string.
478              
479             =cut
480              
481             sub add_placeholders {
482 829     829 1 31473 my ( $self, $text, $dataset, $line ) = @_;
483 829         4031 my $quoted_text = Test::BDD::Cucumber::Util::bs_quote($text);
484 829         3361 $quoted_text =~ s/(<([^>]+)>)/
485 167 50       1578 exists $dataset->{$2} ? $dataset->{$2} :
486             die parse_error_from_line( "No mapping to placeholder $1", $line )
487             /eg;
488 829         2894 return Test::BDD::Cucumber::Util::bs_unquote($quoted_text);
489             }
490              
491              
492             =head2 add_table_placeholders
493              
494             Accepts a hash with parsed table data and a hashref, and replaces
495             C< > with the values in the hashref, returning a copy of the
496             parsed table hashref.
497              
498             =cut
499              
500             sub add_table_placeholders {
501 11     11 1 120 my ($self, $tbl, $dataset, $line) = @_;
502             my @rv = map {
503 11         41 my $row = $_;
  29         56  
504             my %inner_rv =
505 29         364 map { $_ => $self->add_placeholders($row->{$_}, $dataset, $line)
  69         209  
506             } keys %$row;
507 29         100 \%inner_rv;
508             } @$tbl;
509 11         41 return \@rv;
510             }
511              
512              
513             =head2 find_and_dispatch
514              
515             Accepts a L object, and searches through
516             the steps that have been added to the executor object, executing against the
517             first matching one (unless C<$self->matching> indicates otherwise).
518              
519             You can also pass in a boolean 'short-circuit' flag if the Scenario's remaining
520             steps should be skipped, and a boolean flag to denote if it's a redispatched
521             step.
522              
523             =cut
524              
525             sub find_and_dispatch {
526 753     753 1 3402 my ( $self, $context, $short_circuit, $redispatch ) = @_;
527              
528             # Short-circuit if we need to
529 753 100       2779 return $self->skip_step( $context, 'pending',
530             "Short-circuited from previous tests", 0 )
531             if $short_circuit;
532              
533             # Try and find a matching step
534 734         1512 my $stepdef;
535 734         2246 my $text = $context->text;
536 734 50       23055 if ($self->matching eq 'first') {
537 1687     1687   13987 $stepdef = first { $text =~ $_->[0] }
538 734 100       4648 @{ $self->{'steps'}->{ $context->verb } || [] },
539 734 100       9381 @{ $self->{'steps'}->{'step'} || [] };
  734         5958  
540             }
541             else {
542 0         0 my @stepdefs = grep { $text =~ $_->[0] }
543 0 0       0 @{ $self->{'steps'}->{ $context->verb } || [] },
544 0 0       0 @{ $self->{'steps'}->{'step'} || [] };
  0         0  
545              
546 0 0       0 if (@stepdefs > 1) {
547 0         0 my $filename = $context->step->line->document->filename;
548 0         0 my $line = $context->step->line->number;
549             my $msg =
550             join("\n ",
551             qq(Step "$text" ($filename:$line) matches multiple step functions:),
552             map {
553 0         0 qq{matcher $_->[0] defined at } .
554             (($_->[1]->{source} && $_->[1]->{line})
555 0 0 0     0 ? "$_->[1]->{source}:$_->[1]->{line}"
556             : '') } @stepdefs);
557              
558 0 0       0 if ($self->matching eq 'relaxed') {
559 0         0 warn $msg;
560             }
561             else {
562 0         0 die $msg;
563             }
564             }
565 0         0 $stepdef = shift @stepdefs;
566             }
567              
568             # Deal with the simple case of no-match first of all
569 734 100       4186 unless ($stepdef) {
570 6         40 my $message =
571             "No matching step definition for: "
572             . $context->verb . ' '
573             . $context->text;
574 6         31 my $result =
575             $self->skip_step( $context, 'undefined', $message, $redispatch );
576 6         24 return $result;
577             }
578              
579 728         1287 $_->pre_step( $stepdef, $context ) for @{ $self->extensions };
  728         3216  
580 728         2954 my $result = $self->dispatch( $context, $stepdef, 0, $redispatch );
581             $_->post_step( $stepdef, $context,
582             ( $result->result ne 'passing' ), $result )
583 728         1902 for reverse @{ $self->extensions };
  728         4614  
584 728         2368 return $result;
585             }
586              
587             =head2 dispatch($context, $stepdef, $short_circuit, $redispatch)
588              
589             Accepts a L object, and a
590             reference to a step definition triplet (verb, metadata hashref, coderef)
591             and executes it the coderef.
592              
593             You can also pass in a boolean 'short-circuit' flag if the Scenario's remaining
594             steps should be skipped.
595              
596             =cut
597              
598             sub dispatch {
599 986     986 1 3292 my ( $self, $context, $stepdef, $short_circuit, $redispatch ) = @_;
600              
601 986 50       6027 return $self->skip_step( $context, 'pending',
602             "Short-circuited from previous tests", $redispatch )
603             if $short_circuit;
604              
605             # Execute the step definition
606 986         2715 my ( $regular_expression, $meta, $coderef ) = @$stepdef;
607              
608 986 100       3907 my $step_name = $redispatch ? 'sub_step' : 'step';
609 986         2387 my $step_done_name = $step_name . '_done';
610              
611             # Say we're about to start it up
612 986         6878 $context->harness->$step_name($context);
613              
614 986         5439 my @match_locations;
615 986         1890 my $stash_keys = join ';', sort keys %{$context->stash};
  986         7870  
616             # Using `intercept()`, run the step function in an isolated
617             # environment -- this should not affect the enclosing scope
618             # which might be a TAP::Harness scope.
619             #
620             # Instead, we want the tests inside this scope to map to
621             # status values
622             my $events = intercept {
623             # This is a hack to make Test::More's $TODO variable work
624             # inside the intercepted scope.
625              
626             ###TODO: Both intercept() and Test::More::subtest() should
627             # be replaced by a specific Hub implementation for T::B::C
628             Test::More::subtest( 'execute step', sub {
629              
630             # Take a copy of this. Turns out actually matching against it
631             # directly causes all sorts of weird-ass heisenbugs which mst has
632             # promised to investigate.
633 986         1210867 my $text = $context->text;
634              
635             # Save the matches
636 986         17696 $context->matches( [ $text =~ $regular_expression ] );
637              
638             # Save the location of matched subgroups for highlighting hijinks
639 986         6259 my @starts = @-;
640 986         4516 my @ends = @+;
641              
642             # Store the string position of matches for highlighting
643 986         2788 @match_locations = map { [ $_, shift @ends ] } @starts;
  1689         5938  
644              
645             # OK, actually execute
646 986         1683 local $@;
647 986         1982 eval {
648 22     22   240 no warnings 'redefine';
  22         42  
  22         35095  
649              
650             local *Test::BDD::Cucumber::StepFile::_S = sub {
651 1067         10297 return $context->stash->{'scenario'};
652 986         6960 };
653             local *Test::BDD::Cucumber::StepFile::_C = sub {
654 536         2012 return $context;
655 986         3510 };
656              
657 986         4676 $coderef->($context)
658             };
659 986 50       305178 if ($@) {
660 0         0 fail("Step ran to completion", "Exception: ", $@);
661             }
662             else {
663 986         4250 pass("Step ran to completion");
664             }
665              
666 986         408267 done_testing();
667 986     986   489647 });
668 986         8287 };
669              
670 986         2489359 my $status = $self->_test_status( $events );
671              
672             my $result = Test::BDD::Cucumber::Model::Result->new(
673             {
674             result => $status,
675             # due to the hack above with the subtest inside the
676             # interception scope, we need to grovel the subtest
677             # from out of the other results first.
678             output => $self->_test_output(
679 1972     1972   12785 (first { $_->isa('Test2::Event::Subtest') }
680 986         7361 @$events)->{subevents}),
681             });
682             warn qq|Unsupported: Step modified C->stash instead of C->stash->{scenario} or C->stash->{feature}|
683 986 50       212559 if $stash_keys ne (join ';', sort keys %{$context->stash});
  986         10566  
684              
685 986         5261 my @clean_matches =
686             $self->_extract_match_strings( $context->text, \@match_locations );
687 986 100       3398 @clean_matches = [ 0, $context->text ] unless @clean_matches;
688              
689             # Say the step is done, and return the result. Happens outside
690             # the above block so that we don't have the localized harness
691             # anymore...
692 986 100       8707 $context->harness->add_result($result) unless $redispatch;
693 986         6653 $context->harness->$step_done_name( $context, $result, \@clean_matches );
694 986         58008 return $result;
695             }
696              
697             sub _extract_match_strings {
698 986     986   3191 my ( $self, $text, $locations ) = @_;
699              
700             # Clean up the match locations
701             my @match_locations = grep {
702 1689 100 100     10167 ( $_->[0] != $_->[1] ) && # No zero-length matches
703             # And nothing that matched the full string
704             ( !( ( $_->[0] == 0 ) && ( ( $_->[1] == length $text ) ) ) )
705             } grep {
706 986 50 33     2638 defined $_ && ref $_ && defined $_->[0] && defined $_->[1]
  1689   33     14374  
707             } @$locations;
708              
709 986 100       3091 return unless @match_locations;
710              
711             my %range =
712 571         1289 map { $_ => 1 } map { $_->[0] .. ($_->[1] - 1) } @match_locations;
  5941         15262  
  692         3014  
713              
714             # Walk the string, splitting
715 571         2685 my @parts = ( [ 0, '' ] );
716 571         2361 for ( 0 .. ( ( length $text ) - 1 ) ) {
717 17571   100     43395 my $to_highlight = $range{$_} || 0;
718 17571         30244 my $character = substr( $text, $_, 1 );
719              
720 17571 100       36474 if ( $parts[-1]->[0] != $to_highlight ) {
721 1154         2823 push( @parts, [ $to_highlight, '' ] );
722             }
723              
724 17571         31362 $parts[-1]->[1] .= $character;
725             }
726              
727 571         4785 return @parts;
728             }
729              
730             sub _test_output {
731 986     986   2565 my ($self, $events) = @_;
732 986         8386 my $fmt = Test2::Formatter::TAP->new();
733 19     19   14128 open my $stdout, '>:encoding(UTF-8)', \my $out_text;
  19         389  
  19         139  
  986         216406  
734 986         88853 my $idx = 0;
735              
736 986         6083 $fmt->set_handles([ $stdout, $stdout ]);
737 986         18413 $self->_test_output_from_subevents($events, $fmt, \$idx);
738 986         154573 close $stdout;
739              
740 986         6993 return Encode::decode('utf8', $out_text);
741             }
742              
743             sub _test_output_from_subevents {
744 986     986   3679 my ($self, $events, $fmt, $idx) = @_;
745              
746 986         2750 for my $event (@$events) {
747 2618 50       80509 if ($event->{subevents}) {
748             $self->_test_output_from_subevents(
749 0         0 $event->{subevents}, $fmt, $idx);
750             }
751             else {
752 2618         8535 $fmt->write($event, $$idx++);
753             }
754             }
755             }
756              
757             sub _test_status {
758 986     986   2485 my $self = shift;
759 986         2284 my $events = shift;
760              
761 986 100       7598 if (any { defined $_->{effective_pass}
762 1972 100   1972   10073 and ! $_->{effective_pass} } @$events) {
763 7         31 return 'failing';
764             }
765             else {
766 979 100       3492 return $self->_test_status_from_subevents($events) ? 'pending' : 'passing';
767             }
768             }
769              
770             sub _test_status_from_subevents {
771 1958     1958   3530 my $self = shift;
772 1958         3518 my $events = shift;
773              
774 1958         3990 for my $e (@$events) {
775 4527 100 66     14899 if (exists $e->{subevents}) {
    100 33        
      33        
776             $self->_test_status_from_subevents($e->{subevents})
777 979 100       3257 and return 1;
778             }
779             elsif (defined $e->{amnesty}
780             and $e->{effective_pass}
781             and (not $e->{pass})
782 3     3   17 and any { $_->{tag} eq 'TODO' } @{$e->{amnesty}}) {
  3         15  
783 3         27 return 1;
784             }
785             }
786              
787 1952         8702 return 0;
788             }
789              
790             =head2 skip_step
791              
792             Accepts a step-context, a result-type, and a textual reason, exercises the
793             Harness's step start and step_done methods, and returns a skipped-test result.
794              
795             =cut
796              
797             sub skip_step {
798 25     25 1 76 my ( $self, $context, $type, $reason, $redispatch ) = @_;
799              
800 25 50       75 my $step_name = $redispatch ? 'sub_step' : 'step';
801 25         56 my $step_done_name = $step_name . '_done';
802              
803             # Pretend to start step execution
804 25         171 $context->harness->$step_name($context);
805              
806             # Create a result object
807 25         1119 my $result = Test::BDD::Cucumber::Model::Result->new(
808             {
809             result => $type,
810             output => '1..0 # SKIP ' . $reason
811             }
812             );
813              
814             # Pretend we executed it
815 25 50       1782 $context->harness->add_result($result) unless $redispatch;
816 25         136 $context->harness->$step_done_name( $context, $result );
817 25         278 return $result;
818             }
819              
820             =head1 AUTHOR
821              
822             Peter Sergeant C
823              
824             =head1 LICENSE
825              
826             Copyright 2019-2023, Erik Huelsmann
827             Copyright 2011-2019, Peter Sergeant; Licensed under the same terms as Perl
828              
829             =cut
830              
831             1;