File Coverage

blib/lib/Test/BDD/Cucumber/Executor.pm
Criterion Covered Total %
statement 269 290 92.7
branch 68 94 72.3
condition 23 38 60.5
subroutine 42 42 100.0
pod 9 9 100.0
total 411 473 86.8


line stmt bran cond sub pod time code
1              
2 13     13   5137 use v5.14;
  13         57  
3 13     13   71 use warnings;
  13         26  
  13         600  
4              
5             package Test::BDD::Cucumber::Executor 0.86;
6              
7             =head1 NAME
8              
9             Test::BDD::Cucumber::Executor - Run through Feature and Harness objects
10              
11             =head1 VERSION
12              
13             version 0.86
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 13     13   1153 use Moo;
  13         23803  
  13         120  
23 13     13   14390 use MooX::HandlesVia;
  13         121961  
  13         86  
24 13     13   4511 use Types::Standard qw( Bool Str ArrayRef HashRef );
  13         390527  
  13         135  
25 13     13   14073 use List::Util qw/first any/;
  13         31  
  13         1085  
26 13     13   89 use Module::Runtime qw/use_module/;
  13         33  
  13         96  
27 13     13   2485 use utf8;
  13         67  
  13         103  
28 13     13   393 use Carp qw(carp croak);
  13         28  
  13         619  
29 13     13   4669 use Encode ();
  13         84394  
  13         431  
30              
31 13     13   675 use Test2::API qw/intercept/;
  13         72070  
  13         941  
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 13     13   5439 use Test2::Tools::Basic qw/ pass fail done_testing /;
  13         13557  
  13         1091  
39             # Needed for subtest() -- we don't want to import all its functions though
40             require Test::More;
41              
42 13     13   5988 use Test::BDD::Cucumber::StepFile ();
  13         55  
  13         537  
43 13     13   6583 use Test::BDD::Cucumber::StepContext;
  13         49  
  13         599  
44 13     13   6190 use Test::BDD::Cucumber::Util;
  13         65  
  13         435  
45 13     13   4523 use Test::BDD::Cucumber::Model::Result;
  13         41  
  13         510  
46 13     13   2938 use Test::BDD::Cucumber::Errors qw/parse_error_from_line/;
  13         31  
  13         34352  
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 44     44 1 577 my ( $self, @steps ) = @_;
119              
120             # Map the steps to be lower case...
121 44         140 for (@steps) {
122 139         255 my ( $verb, $match, $meta, $code );
123              
124 139 100       309 if (@$_ == 3) {
125 46         108 ( $verb, $match, $code ) = @$_;
126 46         78 $meta = {};
127             }
128             else {
129 93         212 ( $verb, $match, $meta, $code ) = @$_;
130             }
131 139         275 $verb = lc $verb;
132              
133 139 100       315 unless ( ref($match) ) {
134 44         124 $match =~ s/:\s*$//;
135 44         103 $match = quotemeta($match);
136 44         648 $match = qr/^$match:?/i;
137             }
138              
139 139 100 100     537 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         11 unshift( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  6         20  
144             } else {
145 133         239 push( @{ $self->{'steps'}->{$verb} }, [ $match, $meta, $code ] );
  133         653  
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 31     31 1 407 my ( $self, $feature, $harness, $tag_spec ) = @_;
162 31         92 my $feature_stash = {};
163              
164 31         164 $harness->feature($feature);
165 31 100       1452 my @background =
166             ( $feature->background ? ( background => $feature->background ) : () );
167              
168             # Get all scenarios
169 31         466 my @scenarios = @{ $feature->scenarios() };
  31         538  
170              
171 31         294 $_->pre_feature( $feature, $feature_stash ) for @{ $self->extensions };
  31         180  
172 31         112 for my $outline (@scenarios) {
173              
174             # Execute the scenario itself
175 96         698 $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 31         70 for reverse @{ $self->extensions };
  31         132  
188              
189 31         204 $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   29 my ($spec, @tagged_components) = @_;
216 12         22 state $deprecation_warned = 0;
217              
218 12 50       51 if ($spec->isa('Cucumber::TagExpressions::ExpressionNode')) {
219             return grep {
220 12         24 $spec->evaluate( @{ $_->tags } )
  12         18  
  12         235  
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 96     96 1 293 my ( $self, $options ) = @_;
233             my ( $feature, $feature_stash, $harness, $outline, $background, $tagspec )
234 96         382 = @$options{qw/ feature feature_stash harness scenario background tagspec /};
235              
236             # Multiply out Scenario Outlines as appropriate
237 96         158 my @datasets = @{ $outline->datasets };
  96         2117  
238 96 100       1073 if (not @datasets) {
239 81 100 100     315 if (not $tagspec or _match_tags( $tagspec, $outline )) {
240 75         759 $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 81         1153 return;
253             }
254              
255 15 50       57 if ($tagspec) {
256 0         0 @datasets = _match_tags( $tagspec, @datasets );
257 0 0       0 return unless @datasets;
258             }
259              
260              
261 15         51 foreach my $rows (@datasets) {
262              
263 15         30 foreach my $row (@{$rows->data}) {
  15         277  
264              
265 38   100     336 my $name = $outline->{name} || "";
266             $name =~ s/\Q<$_>\E/$row->{$_}/g
267 38         1242 for (keys %$row);
268 38         162 local $outline->{name} = $name;
269              
270 38         290 $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 155     155   420 my ( $self, $options ) = @_;
310             my ( $feature, $feature_stash, $harness, $outline,
311             $scenario_stash, $scenario_state, $dataset, $context_defaults )
312             = @$options{
313 155         584 qw/ feature feature_stash harness scenario scenario_stash
314             scenario_state dataset context_defaults
315             /
316             };
317              
318              
319 155         307 foreach my $step ( @{ $outline->steps } ) {
  155         3567  
320              
321             # Multiply out any placeholders
322 441         10840 my $text =
323             $self->add_placeholders( $step->text, $dataset, $step->line );
324 441         1435 my $data = $step->data;
325 441 100       1829 $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 441   100     8841 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 441         90640 $scenario_state->{'short_circuit'}, 0 );
350              
351             # If it didn't pass, short-circuit the rest
352 441 100       3412 unless ( $result->result eq 'passing' ) {
353 26         133 $scenario_state->{'short_circuit'}++;
354             }
355              
356             }
357              
358 155         451 return;
359             }
360              
361              
362             sub _execute_hook_steps {
363 248     248   669 my ( $self, $phase, $context_defaults, $scenario_state ) = @_;
364 248         629 my $want_short = ($phase eq 'before');
365              
366 248 100       417 for my $step ( @{ $self->{'steps'}->{$phase} || [] } ) {
  248         1340  
367              
368 192         5916 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       32731 ($want_short ? $scenario_state->{'short_circuit'} : 0),
375             0 );
376              
377             # If it didn't pass, short-circuit the rest
378 192 50       1468 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 248         633 return;
386             }
387              
388              
389             sub execute_scenario {
390 124     124 1 356 my ( $self, $options ) = @_;
391             my ( $feature, $feature_stash, $harness, $outline, $background_obj,
392             $scenario_stash, $dataset )
393             = @$options{
394 124         498 qw/ feature feature_stash harness scenario background scenario_stash
395             dataset
396             /
397             };
398 124         247 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 124   100     1269 transformers => $self->{'steps'}->{'transform'} || [],
418             );
419 124         349 $context_defaults{stash}->{scenario} = $scenario_stash;
420              
421             $harness->scenario( $outline, $dataset,
422 124         683 $scenario_stash->{'longest_step_line'} );
423              
424             $_->pre_scenario( $outline, $feature_stash, $scenario_stash )
425 124         4017 for @{ $self->extensions };
  124         603  
426              
427 124         528 $self->_execute_hook_steps( 'before', \%context_defaults, $scenario_state );
428              
429 124 100       354 if ($background_obj) {
430             $harness->background( $outline, $dataset,
431 31         244 $scenario_stash->{'longest_step_line'} );
432 31         253 $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         195 $harness->background_done( $outline, $dataset );
444             }
445              
446             $self->_execute_steps(
447             {
448 124         1113 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 124         797 $self->_execute_hook_steps( 'after', \%context_defaults, $scenario_state );
459              
460             $_->post_scenario( $outline, $feature_stash, $scenario_stash,
461             $scenario_state->{'short_circuit'} )
462 124         235 for reverse @{ $self->extensions };
  124         482  
463              
464 124         640 $harness->scenario_done( $outline, $dataset );
465              
466 124         1999 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 502     502 1 12191 my ( $self, $text, $dataset, $line ) = @_;
478 502         1708 my $quoted_text = Test::BDD::Cucumber::Util::bs_quote($text);
479 502         1562 $quoted_text =~ s/(<([^>]+)>)/
480 85 50       620 exists $dataset->{$2} ? $dataset->{$2} :
481             die parse_error_from_line( "No mapping to placeholder $1", $line )
482             /eg;
483 502         1364 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 88 my ($self, $tbl, $dataset, $line) = @_;
497             my @rv = map {
498 7         24 my $row = $_;
  17         33  
499             my %inner_rv =
500 17         62 map { $_ => $self->add_placeholders($row->{$_}, $dataset, $line)
  39         104  
501             } keys %$row;
502 17         60 \%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 453     453 1 1700 my ( $self, $context, $short_circuit, $redispatch ) = @_;
522              
523             # Short-circuit if we need to
524 453 100       1237 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 440         737 my $stepdef;
530 440         1032 my $text = $context->text;
531 440 50       9450 if ($self->matching eq 'first') {
532 1578     1578   8681 $stepdef = first { $text =~ $_->[0] }
533 440 100       2053 @{ $self->{'steps'}->{ $context->verb } || [] },
534 440 100       4767 @{ $self->{'steps'}->{'step'} || [] };
  440         2643  
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 440 100       2149 unless ($stepdef) {
565 4         36 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         15 return $result;
572             }
573              
574 436         733 $_->pre_step( $stepdef, $context ) for @{ $self->extensions };
  436         1391  
575 436         1433 my $result = $self->dispatch( $context, $stepdef, 0, $redispatch );
576             $_->post_step( $stepdef, $context,
577             ( $result->result ne 'passing' ), $result )
578 436         895 for reverse @{ $self->extensions };
  436         1682  
579 436         1120 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 628     628 1 1731 my ( $self, $context, $stepdef, $short_circuit, $redispatch ) = @_;
595              
596 628 50       1460 return $self->skip_step( $context, 'pending',
597             "Short-circuited from previous tests", $redispatch )
598             if $short_circuit;
599              
600             # Execute the step definition
601 628         1629 my ( $regular_expression, $meta, $coderef ) = @$stepdef;
602              
603 628 100       1378 my $step_name = $redispatch ? 'sub_step' : 'step';
604 628         1311 my $step_done_name = $step_name . '_done';
605              
606             # Say we're about to start it up
607 628         2965 $context->harness->$step_name($context);
608              
609 628         3956 my @match_locations;
610 628         983 my $stash_keys = join ';', sort keys %{$context->stash};
  628         3596  
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 628         592737 my $text = $context->text;
629              
630             # Save the matches
631 628         8161 $context->matches( [ $text =~ $regular_expression ] );
632              
633             # Save the location of matched subgroups for highlighting hijinks
634 628         2847 my @starts = @-;
635 628         2529 my @ends = @+;
636              
637             # Store the string position of matches for highlighting
638 628         1500 @match_locations = map { [ $_, shift @ends ] } @starts;
  1046         3351  
639              
640             # OK, actually execute
641 628         1065 local $@;
642 628         1129 eval {
643 13     13   151 no warnings 'redefine';
  13         32  
  13         15533  
644              
645             local *Test::BDD::Cucumber::StepFile::_S = sub {
646 702         4836 return $context->stash->{'scenario'};
647 628         3461 };
648             local *Test::BDD::Cucumber::StepFile::_C = sub {
649 265         874 return $context;
650 628         2046 };
651              
652 628         2535 $coderef->($context)
653             };
654 628 50       157450 if ($@) {
655 0         0 fail("Step ran to completion", "Exception: ", $@);
656             }
657             else {
658 628         1987 pass("Step ran to completion");
659             }
660              
661 628         195231 done_testing();
662 628     628   230023 });
663 628         3994 };
664              
665 628         1253527 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 1256     1256   5853 (first { $_->isa('Test2::Event::Subtest') }
675 628         3517 @$events)->{subevents}),
676             });
677             warn qq|Unsupported: Step modified C->stash instead of C->stash->{scenario} or C->stash->{feature}|
678 628 50       107191 if $stash_keys ne (join ';', sort keys %{$context->stash});
  628         5187  
679              
680 628         2701 my @clean_matches =
681             $self->_extract_match_strings( $context->text, \@match_locations );
682 628 100       1944 @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 628 100       3587 $context->harness->add_result($result) unless $redispatch;
688 628         2965 $context->harness->$step_done_name( $context, $result, \@clean_matches );
689 628         28092 return $result;
690             }
691              
692             sub _extract_match_strings {
693 628     628   1660 my ( $self, $text, $locations ) = @_;
694              
695             # Clean up the match locations
696             my @match_locations = grep {
697 1046 100 100     5216 ( $_->[0] != $_->[1] ) && # No zero-length matches
698             # And nothing that matched the full string
699             ( !( ( $_->[0] == 0 ) && ( ( $_->[1] == length $text ) ) ) )
700             } grep {
701 628 50 33     1238 defined $_ && ref $_ && defined $_->[0] && defined $_->[1]
  1046   33     7330  
702             } @$locations;
703              
704 628 100       1889 return unless @match_locations;
705              
706             my %range =
707 325         668 map { $_ => 1 } map { $_->[0] .. ($_->[1] - 1) } @match_locations;
  3371         7129  
  407         1355  
708              
709             # Walk the string, splitting
710 325         1228 my @parts = ( [ 0, '' ] );
711 325         1170 for ( 0 .. ( ( length $text ) - 1 ) ) {
712 10149   100     22062 my $to_highlight = $range{$_} || 0;
713 10149         15244 my $character = substr( $text, $_, 1 );
714              
715 10149 100       18032 if ( $parts[-1]->[0] != $to_highlight ) {
716 654         1454 push( @parts, [ $to_highlight, '' ] );
717             }
718              
719 10149         16159 $parts[-1]->[1] .= $character;
720             }
721              
722 325         1392 return @parts;
723             }
724              
725             sub _test_output {
726 628     628   1383 my ($self, $events) = @_;
727 628         3734 my $fmt = Test2::Formatter::TAP->new();
728 10     10   95 open my $stdout, '>:encoding(UTF-8)', \my $out_text;
  10     9   26  
  10         89  
  9         7348  
  9         23  
  9         52  
  628         124030  
729 628         45067 my $idx = 0;
730              
731 628         2457 $fmt->set_handles([ $stdout, $stdout ]);
732 628         13606 $self->_test_output_from_subevents($events, $fmt, \$idx);
733 628         79232 close $stdout;
734              
735 628         2421 return Encode::decode('utf8', $out_text);
736             }
737              
738             sub _test_output_from_subevents {
739 628     628   1698 my ($self, $events, $fmt, $idx) = @_;
740              
741 628         1555 for my $event (@$events) {
742 1664 50       46221 if ($event->{subevents}) {
743             $self->_test_output_from_subevents(
744 0         0 $event->{subevents}, $fmt, $idx);
745             }
746             else {
747 1664         4683 $fmt->write($event, $$idx++);
748             }
749             }
750             }
751              
752             sub _test_status {
753 628     628   1380 my $self = shift;
754 628         1028 my $events = shift;
755              
756 628 100       3562 if (any { defined $_->{effective_pass}
757 1256 100   1256   5039 and ! $_->{effective_pass} } @$events) {
758 6         25 return 'failing';
759             }
760             else {
761 622 100       1607 return $self->_test_status_from_subevents($events) ? 'pending' : 'passing';
762             }
763             }
764              
765             sub _test_status_from_subevents {
766 1244     1244   1937 my $self = shift;
767 1244         1734 my $events = shift;
768              
769 1244         2238 for my $e (@$events) {
770 2865 100 66     8242 if (exists $e->{subevents}) {
    100 33        
      33        
771             $self->_test_status_from_subevents($e->{subevents})
772 622 100       1545 and return 1;
773             }
774             elsif (defined $e->{amnesty}
775             and $e->{effective_pass}
776             and (not $e->{pass})
777 3     3   14 and any { $_->{tag} eq 'TODO' } @{$e->{amnesty}}) {
  3         13  
778 3         20 return 1;
779             }
780             }
781              
782 1238         3676 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 55 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         118 $context->harness->$step_name($context);
800              
801             # Create a result object
802 17         763 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       1092 $context->harness->add_result($result) unless $redispatch;
811 17         85 $context->harness->$step_done_name( $context, $result );
812 17         254 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;