File Coverage

blib/lib/Test/BDD/Cucumber/Executor.pm
Criterion Covered Total %
statement 263 290 90.6
branch 66 94 70.2
condition 21 38 55.2
subroutine 41 42 97.6
pod 9 9 100.0
total 400 473 84.5


line stmt bran cond sub pod time code
1              
2 12     12   5166 use v5.14;
  12         61  
3 12     12   64 use warnings;
  12         32  
  12         513  
4              
5             package Test::BDD::Cucumber::Executor 0.85;
6              
7             =head1 NAME
8              
9             Test::BDD::Cucumber::Executor - Run through Feature and Harness objects
10              
11             =head1 VERSION
12              
13             version 0.85
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 12     12   1285 use Moo;
  12         24861  
  12         101  
23 12     12   13829 use MooX::HandlesVia;
  12         113953  
  12         83  
24 12     12   4036 use Types::Standard qw( Bool Str ArrayRef HashRef );
  12         320375  
  12         122  
25 12     12   13143 use List::Util qw/first any/;
  12         34  
  12         957  
26 12     12   83 use Module::Runtime qw/use_module/;
  12         27  
  12         81  
27 12     12   2366 use utf8;
  12         83  
  12         88  
28 12     12   363 use Carp qw(carp croak);
  12         27  
  12         670  
29 12     12   4717 use Encode ();
  12         84521  
  12         312  
30              
31 12     12   689 use Test2::API qw/intercept/;
  12         71936  
  12         1309  
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 12     12   5215 use Test2::Tools::Basic qw/ pass fail done_testing /;
  12         12752  
  12         1051  
39             # Needed for subtest() -- we don't want to import all its functions though
40             require Test::More;
41              
42 12     12   5423 use Test::BDD::Cucumber::StepFile ();
  12         38  
  12         405  
43 12     12   5944 use Test::BDD::Cucumber::StepContext;
  12         45  
  12         538  
44 12     12   5412 use Test::BDD::Cucumber::Util;
  12         68  
  12         429  
45 12     12   4213 use Test::BDD::Cucumber::Model::Result;
  12         43  
  12         439  
46 12     12   2637 use Test::BDD::Cucumber::Errors qw/parse_error_from_line/;
  12         30  
  12         32764  
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 40     40 1 664 my ( $self, @steps ) = @_;
119              
120             # Map the steps to be lower case...
121 40         157 for (@steps) {
122 135         238 my ( $verb, $match, $meta, $code );
123              
124 135 100       299 if (@$_ == 3) {
125 46         107 ( $verb, $match, $code ) = @$_;
126 46         78 $meta = {};
127             }
128             else {
129 89         205 ( $verb, $match, $meta, $code ) = @$_;
130             }
131 135         245 $verb = lc $verb;
132              
133 135 100       322 unless ( ref($match) ) {
134 44         129 $match =~ s/:\s*$//;
135 44         103 $match = quotemeta($match);
136 44         665 $match = qr/^$match:?/i;
137             }
138              
139 135 100 100     519 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 6         9 unshift( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  6         21  
144             } else {
145 129         196 push( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  129         656  
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 27     27 1 447 my ( $self, $feature, $harness, $tag_spec ) = @_;
162 27         114 my $feature_stash = {};
163              
164 27         149 $harness->feature($feature);
165 27 100       1381 my @background =
166             ( $feature->background ? ( background => $feature->background ) : () );
167              
168             # Get all scenarios
169 27         485 my @scenarios = @{ $feature->scenarios() };
  27         466  
170              
171 27         264 $_->pre_feature( $feature, $feature_stash ) for @{ $self->extensions };
  27         181  
172 27         133 for my $outline (@scenarios) {
173              
174             # Execute the scenario itself
175 84         651 $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 27         77 for reverse @{ $self->extensions };
  27         146  
188              
189 27         177 $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 0     0   0 my ($spec, @tagged_components) = @_;
216 0         0 state $deprecation_warned = 0;
217              
218 0 0       0 if ($spec->isa('Cucumber::TagExpressions::ExpressionNode')) {
219             return grep {
220 0         0 $spec->evaluate( @{ $_->tags } )
  0         0  
  0         0  
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 84     84 1 253 my ( $self, $options ) = @_;
233             my ( $feature, $feature_stash, $harness, $outline, $background, $tagspec )
234 84         320 = @$options{qw/ feature feature_stash harness scenario background tagspec /};
235              
236             # Multiply out Scenario Outlines as appropriate
237 84         146 my @datasets = @{ $outline->datasets };
  84         1883  
238 84 100       1249 if (not @datasets) {
239 69 50 33     291 if (not $tagspec or _match_tags( $tagspec, $outline )) {
240 69         579 $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              
252 69         903 return;
253             }
254              
255 15 50       1856 if ($tagspec) {
256 0         0 @datasets = _match_tags( $tagspec, @datasets );
257 0 0       0 return unless @datasets;
258             }
259              
260              
261 15         45 foreach my $rows (@datasets) {
262              
263 15         38 foreach my $row (@{$rows->data}) {
  15         294  
264              
265 38   100     302 my $name = $outline->{name} || "";
266             $name =~ s/\Q<$_>\E/$row->{$_}/g
267 38         1288 for (keys %$row);
268 38         174 local $outline->{name} = $name;
269              
270 38         325 $self->execute_scenario(
271             {
272             feature => $feature,
273             feature_stash => $feature_stash,
274             harness => $harness,
275             scenario => $outline,
276             background => $background,
277             scenario_stash => {},
278             dataset => $row,
279             });
280             }
281             }
282             }
283              
284             =head2 execute_scenario
285              
286             Accepts a hashref of options, and executes each step in a scenario. Options:
287              
288             C - A L object
289              
290             C - A hashref that should live the lifetime of feature execution
291              
292             C - A L subclass object
293              
294             C - A L object
295              
296             C - An optional L object
297             representing the Background
298              
299             C - A hashref that lives the lifetime of the scenario execution
300              
301             For each step, a L object is created, and
302             passed to C. Nothing is returned - everything is played back through
303             the Harness interface.
304              
305             =cut
306              
307              
308             sub _execute_steps {
309 149     149   352 my ( $self, $options ) = @_;
310             my ( $feature, $feature_stash, $harness, $outline,
311             $scenario_stash, $scenario_state, $dataset, $context_defaults )
312             = @$options{
313 149         556 qw/ feature feature_stash harness scenario scenario_stash
314             scenario_state dataset context_defaults
315             /
316             };
317              
318              
319 149         260 foreach my $step ( @{ $outline->steps } ) {
  149         3350  
320              
321             # Multiply out any placeholders
322 435         10488 my $text =
323             $self->add_placeholders( $step->text, $dataset, $step->line );
324 435         1435 my $data = $step->data;
325 435 100       1902 $data = (ref $data) ?
    100          
326             $self->add_table_placeholders( $data, $dataset, $step->line )
327             : (defined $data) ?
328             $self->add_placeholders( $data, $dataset, $step->line )
329             : '';
330              
331             # Set up a context
332 435   100     8337 my $context = Test::BDD::Cucumber::StepContext->new(
333             {
334             %$context_defaults,
335              
336             # Data portion
337             columns => $step->columns || [],
338             data => $data,
339              
340             # Step-specific info
341             step => $step,
342             verb => lc( $step->verb ),
343             text => $text,
344             }
345             );
346              
347             my $result =
348             $self->find_and_dispatch( $context,
349 435         90051 $scenario_state->{'short_circuit'}, 0 );
350              
351             # If it didn't pass, short-circuit the rest
352 435 100       3420 unless ( $result->result eq 'passing' ) {
353 26         129 $scenario_state->{'short_circuit'}++;
354             }
355              
356             }
357              
358 149         465 return;
359             }
360              
361              
362             sub _execute_hook_steps {
363 236     236   650 my ( $self, $phase, $context_defaults, $scenario_state ) = @_;
364 236         499 my $want_short = ($phase eq 'before');
365              
366 236 100       402 for my $step ( @{ $self->{'steps'}->{$phase} || [] } ) {
  236         1157  
367              
368 192         5910 my $context = Test::BDD::Cucumber::StepContext->new(
369             { %$context_defaults, verb => $phase, } );
370              
371             my $result =
372             $self->dispatch(
373             $context, $step,
374 192 100       32978 ($want_short ? $scenario_state->{'short_circuit'} : 0),
375             0 );
376              
377             # If it didn't pass, short-circuit the rest
378 192 50       1461 unless ( $result->result eq 'passing' ) {
379 0 0       0 if ($want_short) {
380 0         0 $scenario_state->{'short_circuit'} = 1;
381             }
382             }
383             }
384              
385 236         549 return;
386             }
387              
388              
389             sub execute_scenario {
390 118     118 1 337 my ( $self, $options ) = @_;
391             my ( $feature, $feature_stash, $harness, $outline, $background_obj,
392             $scenario_stash, $dataset )
393             = @$options{
394 118         443 qw/ feature feature_stash harness scenario background scenario_stash
395             dataset
396             /
397             };
398 118         243 my $scenario_state = {};
399              
400             my %context_defaults = (
401             executor => $self, # Held weakly by StepContext
402              
403             # Data portion
404             data => '',
405             stash => {
406             feature => $feature_stash,
407             step => {},
408             },
409              
410             # Step-specific info
411             feature => $feature,
412             scenario => $outline,
413              
414             # Communicators
415             harness => $harness,
416              
417 118   100     1222 transformers => $self->{'steps'}->{'transform'} || [],
418             );
419 118         314 $context_defaults{stash}->{scenario} = $scenario_stash;
420              
421             $harness->scenario( $outline, $dataset,
422 118         743 $scenario_stash->{'longest_step_line'} );
423              
424             $_->pre_scenario( $outline, $feature_stash, $scenario_stash )
425 118         4112 for @{ $self->extensions };
  118         599  
426              
427 118         546 $self->_execute_hook_steps( 'before', \%context_defaults, $scenario_state );
428              
429 118 100       387 if ($background_obj) {
430             $harness->background( $outline, $dataset,
431 31         281 $scenario_stash->{'longest_step_line'} );
432 31         334 $self->_execute_steps(
433             {
434             scenario => $background_obj,
435             feature => $feature,
436             feature_stash => $feature_stash,
437             harness => $harness,
438             scenario_stash => $scenario_stash,
439             scenario_state => $scenario_state,
440             context_defaults => \%context_defaults,
441             }
442             );
443 31         209 $harness->background_done( $outline, $dataset );
444             }
445              
446             $self->_execute_steps(
447             {
448 118         1185 scenario => $outline,
449             feature => $feature,
450             feature_stash => $feature_stash,
451             harness => $harness,
452             scenario_stash => $scenario_stash,
453             scenario_state => $scenario_state,
454             dataset => $dataset,
455             context_defaults => \%context_defaults,
456             });
457              
458 118         799 $self->_execute_hook_steps( 'after', \%context_defaults, $scenario_state );
459              
460             $_->post_scenario( $outline, $feature_stash, $scenario_stash,
461             $scenario_state->{'short_circuit'} )
462 118         252 for reverse @{ $self->extensions };
  118         537  
463              
464 118         621 $harness->scenario_done( $outline, $dataset );
465              
466 118         1860 return;
467             }
468              
469             =head2 add_placeholders
470              
471             Accepts a text string and a hashref, and replaces C< > with the
472             values in the hashref, returning a string.
473              
474             =cut
475              
476             sub add_placeholders {
477 496     496 1 12289 my ( $self, $text, $dataset, $line ) = @_;
478 496         1594 my $quoted_text = Test::BDD::Cucumber::Util::bs_quote($text);
479 496         1654 $quoted_text =~ s/(<([^>]+)>)/
480 85 50       653 exists $dataset->{$2} ? $dataset->{$2} :
481             die parse_error_from_line( "No mapping to placeholder $1", $line )
482             /eg;
483 496         1366 return Test::BDD::Cucumber::Util::bs_unquote($quoted_text);
484             }
485              
486              
487             =head2 add_table_placeholders
488              
489             Accepts a hash with parsed table data and a hashref, and replaces
490             C< > with the values in the hashref, returning a copy of the
491             parsed table hashref.
492              
493             =cut
494              
495             sub add_table_placeholders {
496 7     7 1 95 my ($self, $tbl, $dataset, $line) = @_;
497             my @rv = map {
498 7         28 my $row = $_;
  17         33  
499             my %inner_rv =
500 17         61 map { $_ => $self->add_placeholders($row->{$_}, $dataset, $line)
  39         97  
501             } keys %$row;
502 17         59 \%inner_rv;
503             } @$tbl;
504 7         24 return \@rv;
505             }
506              
507              
508             =head2 find_and_dispatch
509              
510             Accepts a L object, and searches through
511             the steps that have been added to the executor object, executing against the
512             first matching one (unless C<$self->matching> indicates otherwise).
513              
514             You can also pass in a boolean 'short-circuit' flag if the Scenario's remaining
515             steps should be skipped, and a boolean flag to denote if it's a redispatched
516             step.
517              
518             =cut
519              
520             sub find_and_dispatch {
521 447     447 1 1638 my ( $self, $context, $short_circuit, $redispatch ) = @_;
522              
523             # Short-circuit if we need to
524 447 100       1233 return $self->skip_step( $context, 'pending',
525             "Short-circuited from previous tests", 0 )
526             if $short_circuit;
527              
528             # Try and find a matching step
529 434         656 my $stepdef;
530 434         1161 my $text = $context->text;
531 434 50       8973 if ($self->matching eq 'first') {
532 1572     1572   8857 $stepdef = first { $text =~ $_->[0] }
533 434 100       2210 @{ $self->{'steps'}->{ $context->verb } || [] },
534 434 100       4420 @{ $self->{'steps'}->{'step'} || [] };
  434         2624  
535             }
536             else {
537 0         0 my @stepdefs = grep { $text =~ $_->[0] }
538 0 0       0 @{ $self->{'steps'}->{ $context->verb } || [] },
539 0 0       0 @{ $self->{'steps'}->{'step'} || [] };
  0         0  
540              
541 0 0       0 if (@stepdefs > 1) {
542 0         0 my $filename = $context->step->line->document->filename;
543 0         0 my $line = $context->step->line->number;
544             my $msg =
545             join("\n ",
546             qq(Step "$text" ($filename:$line) matches multiple step functions:),
547             map {
548 0         0 qq{matcher $_->[0] defined at } .
549             (($_->[1]->{source} && $_->[1]->{line})
550 0 0 0     0 ? "$_->[1]->{source}:$_->[1]->{line}"
551             : '') } @stepdefs);
552              
553 0 0       0 if ($self->matching eq 'relaxed') {
554 0         0 warn $msg;
555             }
556             else {
557 0         0 die $msg;
558             }
559             }
560 0         0 $stepdef = shift @stepdefs;
561             }
562              
563             # Deal with the simple case of no-match first of all
564 434 100       2117 unless ($stepdef) {
565 4         45 my $message =
566             "No matching step definition for: "
567             . $context->verb . ' '
568             . $context->text;
569 4         23 my $result =
570             $self->skip_step( $context, 'undefined', $message, $redispatch );
571 4         21 return $result;
572             }
573              
574 430         703 $_->pre_step( $stepdef, $context ) for @{ $self->extensions };
  430         1394  
575 430         1446 my $result = $self->dispatch( $context, $stepdef, 0, $redispatch );
576             $_->post_step( $stepdef, $context,
577             ( $result->result ne 'passing' ), $result )
578 430         959 for reverse @{ $self->extensions };
  430         1667  
579 430         1058 return $result;
580             }
581              
582             =head2 dispatch($context, $stepdef, $short_circuit, $redispatch)
583              
584             Accepts a L object, and a
585             reference to a step definition triplet (verb, metadata hashref, coderef)
586             and executes it the coderef.
587              
588             You can also pass in a boolean 'short-circuit' flag if the Scenario's remaining
589             steps should be skipped.
590              
591             =cut
592              
593             sub dispatch {
594 622     622 1 1638 my ( $self, $context, $stepdef, $short_circuit, $redispatch ) = @_;
595              
596 622 50       1583 return $self->skip_step( $context, 'pending',
597             "Short-circuited from previous tests", $redispatch )
598             if $short_circuit;
599              
600             # Execute the step definition
601 622         1637 my ( $regular_expression, $meta, $coderef ) = @$stepdef;
602              
603 622 100       1472 my $step_name = $redispatch ? 'sub_step' : 'step';
604 622         1323 my $step_done_name = $step_name . '_done';
605              
606             # Say we're about to start it up
607 622         3103 $context->harness->$step_name($context);
608              
609 622         3732 my @match_locations;
610 622         954 my $stash_keys = join ';', sort keys %{$context->stash};
  622         3665  
611             # Using `intercept()`, run the step function in an isolated
612             # environment -- this should not affect the enclosing scope
613             # which might be a TAP::Harness scope.
614             #
615             # Instead, we want the tests inside this scope to map to
616             # status values
617             my $events = intercept {
618             # This is a hack to make Test::More's $TODO variable work
619             # inside the intercepted scope.
620              
621             ###TODO: Both intercept() and Test::More::subtest() should
622             # be replaced by a specific Hub implementation for T::B::C
623             Test::More::subtest( 'execute step', sub {
624              
625             # Take a copy of this. Turns out actually matching against it
626             # directly causes all sorts of weird-ass heisenbugs which mst has
627             # promised to investigate.
628 622         591841 my $text = $context->text;
629              
630             # Save the matches
631 622         8156 $context->matches( [ $text =~ $regular_expression ] );
632              
633             # Save the location of matched subgroups for highlighting hijinks
634 622         2834 my @starts = @-;
635 622         2359 my @ends = @+;
636              
637             # Store the string position of matches for highlighting
638 622         1679 @match_locations = map { [ $_, shift @ends ] } @starts;
  1040         3152  
639              
640             # OK, actually execute
641 622         1179 local $@;
642 622         1088 eval {
643 12     12   145 no warnings 'redefine';
  12         28  
  12         15234  
644              
645             local *Test::BDD::Cucumber::StepFile::_S = sub {
646 702         5229 return $context->stash->{'scenario'};
647 622         3586 };
648             local *Test::BDD::Cucumber::StepFile::_C = sub {
649 265         882 return $context;
650 622         1975 };
651              
652 622         3357 $coderef->($context)
653             };
654 622 50       157842 if ($@) {
655 0         0 fail("Step ran to completion", "Exception: ", $@);
656             }
657             else {
658 622         2092 pass("Step ran to completion");
659             }
660              
661 622         193879 done_testing();
662 622     622   231429 });
663 622         4003 };
664              
665 622         1239118 my $status = $self->_test_status( $events );
666              
667             my $result = Test::BDD::Cucumber::Model::Result->new(
668             {
669             result => $status,
670             # due to the hack above with the subtest inside the
671             # interception scope, we need to grovel the subtest
672             # from out of the other results first.
673             output => $self->_test_output(
674 1244     1244   5709 (first { $_->isa('Test2::Event::Subtest') }
675 622         3583 @$events)->{subevents}),
676             });
677             warn qq|Unsupported: Step modified C->stash instead of C->stash->{scenario} or C->stash->{feature}|
678 622 50       104675 if $stash_keys ne (join ';', sort keys %{$context->stash});
  622         5176  
679              
680 622         2655 my @clean_matches =
681             $self->_extract_match_strings( $context->text, \@match_locations );
682 622 100       1888 @clean_matches = [ 0, $context->text ] unless @clean_matches;
683              
684             # Say the step is done, and return the result. Happens outside
685             # the above block so that we don't have the localized harness
686             # anymore...
687 622 100       3493 $context->harness->add_result($result) unless $redispatch;
688 622         2980 $context->harness->$step_done_name( $context, $result, \@clean_matches );
689 622         28355 return $result;
690             }
691              
692             sub _extract_match_strings {
693 622     622   1651 my ( $self, $text, $locations ) = @_;
694              
695             # Clean up the match locations
696             my @match_locations = grep {
697 1040 100 100     5466 ( $_->[0] != $_->[1] ) && # No zero-length matches
698             # And nothing that matched the full string
699             ( !( ( $_->[0] == 0 ) && ( ( $_->[1] == length $text ) ) ) )
700             } grep {
701 622 50 33     1282 defined $_ && ref $_ && defined $_->[0] && defined $_->[1]
  1040   33     7014  
702             } @$locations;
703              
704 622 100       1823 return unless @match_locations;
705              
706             my %range =
707 325         799 map { $_ => 1 } map { $_->[0] .. ($_->[1] - 1) } @match_locations;
  3355         7403  
  407         1428  
708              
709             # Walk the string, splitting
710 325         1342 my @parts = ( [ 0, '' ] );
711 325         1226 for ( 0 .. ( ( length $text ) - 1 ) ) {
712 10149   100     22458 my $to_highlight = $range{$_} || 0;
713 10149         16049 my $character = substr( $text, $_, 1 );
714              
715 10149 100       18180 if ( $parts[-1]->[0] != $to_highlight ) {
716 654         1423 push( @parts, [ $to_highlight, '' ] );
717             }
718              
719 10149         16208 $parts[-1]->[1] .= $character;
720             }
721              
722 325         1476 return @parts;
723             }
724              
725             sub _test_output {
726 622     622   1352 my ($self, $events) = @_;
727 622         3487 my $fmt = Test2::Formatter::TAP->new();
728 9     9   93 open my $stdout, '>:encoding(UTF-8)', \my $out_text;
  9     9   22  
  9         97  
  9         7488  
  9         29  
  9         56  
  622         121796  
729 622         43727 my $idx = 0;
730              
731 622         2484 $fmt->set_handles([ $stdout, $stdout ]);
732 622         12997 $self->_test_output_from_subevents($events, $fmt, \$idx);
733 622         78735 close $stdout;
734              
735 622         2359 return Encode::decode('utf8', $out_text);
736             }
737              
738             sub _test_output_from_subevents {
739 622     622   1640 my ($self, $events, $fmt, $idx) = @_;
740              
741 622         1625 for my $event (@$events) {
742 1652 50       46556 if ($event->{subevents}) {
743             $self->_test_output_from_subevents(
744 0         0 $event->{subevents}, $fmt, $idx);
745             }
746             else {
747 1652         4706 $fmt->write($event, $$idx++);
748             }
749             }
750             }
751              
752             sub _test_status {
753 622     622   1298 my $self = shift;
754 622         968 my $events = shift;
755              
756 622 100       3561 if (any { defined $_->{effective_pass}
757 1244 100   1244   5355 and ! $_->{effective_pass} } @$events) {
758 6         49 return 'failing';
759             }
760             else {
761 616 100       1610 return $self->_test_status_from_subevents($events) ? 'pending' : 'passing';
762             }
763             }
764              
765             sub _test_status_from_subevents {
766 1232     1232   1858 my $self = shift;
767 1232         1721 my $events = shift;
768              
769 1232         2423 for my $e (@$events) {
770 2841 100 66     8178 if (exists $e->{subevents}) {
    100 33        
      33        
771             $self->_test_status_from_subevents($e->{subevents})
772 616 100       1539 and return 1;
773             }
774             elsif (defined $e->{amnesty}
775             and $e->{effective_pass}
776             and (not $e->{pass})
777 3     3   15 and any { $_->{tag} eq 'TODO' } @{$e->{amnesty}}) {
  3         15  
778 3         21 return 1;
779             }
780             }
781              
782 1226         3715 return 0;
783             }
784              
785             =head2 skip_step
786              
787             Accepts a step-context, a result-type, and a textual reason, exercises the
788             Harness's step start and step_done methods, and returns a skipped-test result.
789              
790             =cut
791              
792             sub skip_step {
793 17     17 1 52 my ( $self, $context, $type, $reason, $redispatch ) = @_;
794              
795 17 50       48 my $step_name = $redispatch ? 'sub_step' : 'step';
796 17         45 my $step_done_name = $step_name . '_done';
797              
798             # Pretend to start step execution
799 17         108 $context->harness->$step_name($context);
800              
801             # Create a result object
802 17         750 my $result = Test::BDD::Cucumber::Model::Result->new(
803             {
804             result => $type,
805             output => '1..0 # SKIP ' . $reason
806             }
807             );
808              
809             # Pretend we executed it
810 17 50       1151 $context->harness->add_result($result) unless $redispatch;
811 17         92 $context->harness->$step_done_name( $context, $result );
812 17         223 return $result;
813             }
814              
815             =head1 AUTHOR
816              
817             Peter Sergeant C
818              
819             =head1 LICENSE
820              
821             Copyright 2019-2023, Erik Huelsmann
822             Copyright 2011-2019, Peter Sergeant; Licensed under the same terms as Perl
823              
824             =cut
825              
826             1;