File Coverage

lib/App/pherkin.pm
Criterion Covered Total %
statement 264 310 85.1
branch 80 118 67.8
condition 18 34 52.9
subroutine 32 34 94.1
pod 1 1 100.0
total 395 497 79.4


line stmt bran cond sub pod time code
1 5     5   360812 use v5.14;
  5         48  
2 5     5   28 use warnings;
  5         10  
  5         208  
3              
4             package App::pherkin 0.86;
5              
6              
7 5     5   936 use lib;
  5         1356  
  5         32  
8 5     5   3914 use Getopt::Long;
  5         55571  
  5         23  
9 5     5   2309 use Module::Runtime qw(use_module module_notional_filename);
  5         5301  
  5         38  
10 5     5   315 use List::Util qw(max);
  5         11  
  5         303  
11 5     5   2709 use Pod::Usage;
  5         238540  
  5         717  
12 5     5   2930 use FindBin qw($RealBin $Script);
  5         5491  
  5         558  
13 5     5   2171 use YAML qw( LoadFile );
  5         34740  
  5         297  
14 5     5   3125 use Data::Dumper;
  5         32181  
  5         305  
15 5     5   40 use File::Spec;
  5         16  
  5         161  
16 5     5   1867 use Path::Class qw/file dir/;
  5         146916  
  5         326  
17              
18 5     5   2441 use Cucumber::TagExpressions;
  5         134862  
  5         237  
19              
20             use Test::BDD::Cucumber::I18n
21 5     5   1908 qw(languages langdef readable_keywords keyword_to_subname);
  5         13  
  5         538  
22 5     5   2022 use Test::BDD::Cucumber::Loader;
  5         22  
  5         206  
23              
24 5     5   32 use Moo;
  5         14  
  5         22  
25 5     5   1891 use Types::Standard qw( ArrayRef Bool Str );
  5         13  
  5         31  
26             has 'step_paths' => ( is => 'rw', isa => ArrayRef, default => sub { [] } );
27             has 'extensions' => ( is => 'rw', isa => ArrayRef, default => sub { [] } );
28             has 'tags' => ( is => 'rw', isa => Str, default => '' );
29             has 'match_only' => ( is => 'rw', isa => Bool, default => 0 );
30             has 'matching' => ( is => 'rw', isa => Str, default => 'first');
31             has 'strict' => ( is => 'rw', isa => Bool, default => 0 );
32              
33             has 'harness' => ( is => 'rw' );
34              
35             =head1 NAME
36              
37             App::pherkin - Run Cucumber tests from the command line
38              
39             =head1 VERSION
40              
41             version 0.86
42              
43             =head1 SYNOPSIS
44              
45             pherkin
46             pherkin some/path/features/
47              
48             =head1 DESCRIPTION
49              
50             C will search the directory specified (or C<./features/>) for
51             feature files (any file matching C<*.feature>) and step definition files (any
52             file matching C<*_steps.pl>), loading the step definitions and then executing
53             the features.
54              
55             Steps that pass will be printed in green, those that fail in red, and those
56             for which there is no step definition as yellow (for TODO), assuming you're
57             using the default output harness.
58              
59             =head1 METHODS
60              
61             =head2 run
62              
63             The C class, which is what the C command uses, makes
64             use of the C method, which accepts currently a single path as a string,
65             or nothing.
66              
67             Returns a L object for all steps run.
68              
69             =cut
70              
71             sub _pre_run {
72 5     5   17 my ( $self, @arguments ) = @_;
73              
74             # localized features will have utf8 in them and options may output utf8 as
75             # well
76 5         29 binmode STDOUT, ':utf8';
77              
78 5         23 my ($features_path) = $self->_process_arguments(@arguments);
79 5   50     19 $features_path ||= './features/';
80              
81 5         53 my ( $executor, @features ) =
82             Test::BDD::Cucumber::Loader->load( $features_path );
83 5 50       17 die "No feature files found in $features_path" unless @features;
84              
85 5         106 $executor->matching( $self->matching );
86 5         269 $executor->add_extensions($_) for @{ $self->extensions };
  5         92  
87 5         132 $_->pre_execute($self) for @{ $self->extensions };
  5         78  
88              
89             Test::BDD::Cucumber::Loader->load_steps( $executor, $_ )
90 5         31 for @{ $self->step_paths };
  5         80  
91              
92 5         52 return ( $executor, @features );
93             }
94              
95             sub _post_run {
96 5     5   23 my $self = shift;
97              
98 5         12 $_->post_execute() for reverse @{ $self->extensions };
  5         95  
99             }
100              
101              
102             sub run {
103 5     5 1 5407 my ( $self, @arguments ) = @_;
104 5         23 my ( $executor, @features ) = $self->_pre_run(@arguments);
105              
106 5 100       83 if ( $self->match_only ) {
107 1 50       24 $self->_make_executor_match_only($executor) if $self->match_only;
108 1         6 $self->_rename_feature_steps( @features );
109             }
110              
111 5         95 my $result = $self->_run_tests( $executor, @features );
112 5         25 $self->_post_run;
113 5         176 return $result;
114             }
115              
116             sub _run_tests {
117 5     5   15 my ( $self, $executor, @features ) = @_;
118              
119 5         20 my $harness = $self->harness;
120 5         48 $harness->startup();
121              
122 5         9 my $tag_spec;
123 5 100       87 if ( $self->tags ) {
124 4         85 $tag_spec = Cucumber::TagExpressions->parse( $self->tags );
125             }
126              
127 5         7424 $executor->execute( $_, $harness, $tag_spec ) for @features;
128              
129 5         115 $harness->shutdown();
130              
131 5         47 my $exit_code = 0;
132 5         33 my $result = $harness->result->result;
133 5 50       449 if ($result eq 'failing') {
    50          
134 0         0 $exit_code = 2;
135             }
136             elsif ($self->strict) {
137 0 0 0     0 if ($result eq 'pending'
138             or $result eq 'undefined') {
139 0         0 $exit_code = 1;
140             }
141             }
142              
143 5         101 return $exit_code;
144             }
145              
146             sub _initialize_harness {
147 16     16   387 my ( $self, $harness_module ) = @_;
148              
149 16         33 my $harness_args_string = undef;
150 16         95 ( $harness_module, $harness_args_string ) = split /\(/, $harness_module, 2;
151            
152 16 100       65 unless ( $harness_module =~ m/::/ ) {
153 12         44 $harness_module = "Test::BDD::Cucumber::Harness::" . $harness_module;
154             }
155              
156 16 50       48 eval { use_module($harness_module) }
  16         66  
157             || die "Unable to load harness [$harness_module]: $@";
158              
159 16 100       476 if ( $harness_args_string ) {
160 1         3 my %harness_args;
161 1 50       54 eval "%harness_args = ($harness_args_string; 1"
162             or die $@;
163 1         25 $self->harness( $harness_module->new( %harness_args ) );
164             } else {
165 15         232 $self->harness( $harness_module->new() );
166             }
167            
168             }
169              
170             sub _find_config_file {
171 22     22   164 my ( $self, $config_filename, $debug ) = @_;
172              
173 22 100       77 return $config_filename if $config_filename;
174              
175 12   100     73 for (
176             ( $ENV{'PHERKIN_CONFIG'} || () ),
177              
178             # Allow .yaml or .yml for all of the below
179 60         3349 map { ( "$_.yaml", "$_.yml" ) } (
180              
181             # Relative locations
182 48         3070 ( map { file($_) }
183             qw!.pherkin config/pherkin ./.config/pherkin t/.pherkin!
184             ),
185              
186             # Home locations
187 12         40 ( map { dir($_)->file('.pherkin') }
188 24         57 grep {$_} map { $ENV{$_} } qw/HOME USERPROFILE/
  24         907  
189             )
190             )
191             )
192             {
193 111 100       1543 return $_ if -f $_;
194 110 50       369 print "No config file found in $_\n" if $debug;
195             }
196 11         128 return undef;
197             }
198              
199             sub _replace_helper {
200 3     3   6 my $inval = shift;
201              
202 3   66     20 return $ENV{$inval} // "Environment variable $inval not defined";
203             }
204              
205             sub _resolve_envvars {
206 170     170   236 my ( $config_data ) = shift;
207              
208 170 100       279 if (ref $config_data) {
209 92 100       191 if (ref $config_data eq 'HASH') {
    50          
210             return {
211             map {
212 66         178 $_ => _resolve_envvars( $config_data->{$_} )
  123         262  
213             } keys %$config_data
214             };
215             }
216             elsif (ref $config_data eq 'ARRAY') {
217 26         43 return [ map { _resolve_envvars( $_ ) } @$config_data ];
  39         64  
218             }
219             else {
220 0         0 die 'Unhandled reference type in configuration data';
221             }
222             }
223             else {
224             # replace (in-place) ${ENVVAR_NAME} sequences with the envvar value
225 78         127 $config_data =~ s/(?
  3         11  
226             # remove any escaping double dollar-signs
227 78         122 $config_data =~ s/\$(\$\{[a-zA-Z0-9_]+\})/$1/g;
228             }
229              
230 78         323 return $config_data;
231             }
232              
233             sub _load_config {
234 20     20   8216 my ( $self, $profile_name, $proposed_config_filename, $debug ) = @_;
235              
236 20         59 my $config_filename
237             = $self->_find_config_file( $proposed_config_filename, $debug );
238 20         81 my $config_data_whole;
239              
240             # Check we can actually load some data from that file if required
241 20 100       51 if ($config_filename) {
242 9 50       56 print "Found [$config_filename], reading...\n" if $debug;
243 9         26 $config_data_whole = LoadFile($config_filename);
244 8 50       89134 $config_data_whole = _resolve_envvars( $config_data_whole )
245             if $config_data_whole;
246             } else {
247 11 50       29 if ($profile_name) {
248 0 0       0 print "No configuration files found\n" if $debug;
249 0         0 die
250             "Profile name [$profile_name] specified, but no configuration file found (use --debug-profiles to debug)";
251             } else {
252 11 50       26 print "No configuration files found, and no profile specified\n"
253             if $debug;
254 11         39 return;
255             }
256             }
257              
258 8 100       59 $profile_name = 'default' unless defined $profile_name;
259              
260             # Check the config file has the right type of data at the profile name
261 8 100       23 unless ( ref $config_data_whole eq 'HASH' ) {
262 1         5 die
263             "Config file [$config_filename] doesn't return a hashref on parse, instead a ["
264             . ref($config_data_whole) . ']';
265             }
266 7         18 my $config_data = $config_data_whole->{$profile_name};
267             my $profile_problem = sub {
268 3     3   15 return "Config file [$config_filename] profile [$profile_name]: "
269             . shift();
270 7         44 };
271 7 100       22 unless ($config_data) {
272 1         4 die $profile_problem->("Profile not found");
273             }
274 6 100       17 unless ( ( my $reftype = ref $config_data ) eq 'HASH' ) {
275 1         5 die $profile_problem->("[$reftype] but needs to be a HASH");
276             }
277 5 50       16 print "Using profile [$profile_name]\n" if $debug;
278              
279             # Transform it in to an argument list
280 5         12 my @arguments;
281 5         27 for my $key ( sort keys %$config_data ) {
282 9         20 my $value = $config_data->{$key};
283              
284 9 100       22 if ( my $reftype = ref $value ) {
285 6 100       15 if ( $key ne 'extensions' ) {
286 4 100       16 die $profile_problem->(
287             "Option $key is a [$reftype] but can only be a single value or ARRAY"
288             ) unless $reftype eq 'ARRAY';
289 3         26 push( @arguments, $key, $_ ) for @$value;
290             } else {
291 2 50 33     14 die $profile_problem->(
292             "Option $key is a [$reftype] but can only be a HASH as '$key' is"
293             . " a special case - see the documentation for details"
294             ) unless $reftype eq 'HASH' && $key eq 'extensions';
295 2         9 push( @arguments, $key, $value );
296             }
297             } else {
298 3         10 push( @arguments, $key, $value );
299             }
300             }
301              
302 4 50       13 if ($debug) {
303 0         0 print "Arguments to add: " . ( join ' ', @arguments ) . "\n";
304             }
305              
306 4         61 return @arguments;
307             }
308              
309             sub _process_arguments {
310 11     11   460 my ( $self, @args ) = @_;
311 11         36 local @ARGV = @args;
312              
313             # Allow -Ilib, -bl
314 11         64 Getopt::Long::Configure( 'bundling', 'pass_through' );
315              
316 11         659 my %options = (
317              
318             # Relating to other configuration options
319             config => ['g|config=s'],
320             profile => ['p|profile=s'],
321             debug_profiles => ['debug-profiles'],
322              
323             # Standard
324             help => [ 'h|help|?' ],
325             version => [ 'version' ],
326             includes => [ 'I=s@', [] ],
327             lib => [ 'l|lib' ],
328             blib => [ 'b|blib' ],
329             output => [ 'o|output=s' ],
330             strict => [ 'strict' ],
331             steps => [ 's|steps=s@', [] ],
332             tags => [ 't|tags=s' ],
333             i18n => [ 'i18n=s' ],
334             extensions => [ 'e|extension=s@', [] ],
335             matching => [ 'matching=s' ],
336             match_only => [ 'm|match' ],
337             );
338              
339             GetOptions(
340             map {
341 11         48 my $x;
  176         213  
342 176 100       359 $_->[1] = \$x unless defined $_->[1];
343 176         329 ( $_->[0] => $_->[1] );
344             } values %options
345             );
346              
347             my $deref = sub {
348 211     211   339 my $key = shift;
349 211         345 my $value = $options{$key}->[1];
350 211 100       1415 return ( ref $value eq 'ARRAY' ) ? $value : $$value;
351 11         15839 };
352              
353 11 50       36 if ( $deref->('version') ) {
354 0         0 my ($vol, $dirs, $file) = File::Spec->splitpath( $0 );
355 0   0     0 my $version = $App::pherkin::VERSION || '(development)';
356 0         0 print "$file $version\n";
357              
358 0         0 exit 0;
359             }
360              
361             pod2usage(
362 11 50       32 -verbose => 1,
363             -input => "$RealBin/$Script",
364             ) if $deref->('help');
365              
366 11         22 my @parsed_extensions;
367 11         21 for my $e ( @{ $deref->('extensions') } ) {
  11         25  
368 2         5 my $e_args = "()";
369 2 50       17 $e_args = $1 if $e =~ s/\((.+)\)$//;
370 2         110 my @e_args = eval $e_args;
371 2 50       8 die "Bad arguments in [$e]: $@" if $@;
372              
373 2         9 push( @parsed_extensions, [ $e, \@e_args ] );
374             }
375 11         37 $options{extensions}->[1] = \@parsed_extensions;
376              
377             # Load the configuration file
378 11         29 my @configuration_options = $self->_load_config( map { $deref->($_) }
  33         55  
379             qw/profile config debug_profiles/ );
380              
381             # Merge those configuration items
382             # First we need a list of matching keys
383             my %keys = map {
384 11         66 my ( $key_basis, $ref ) = @{ $options{$_} };
  176         246  
  176         362  
385 473         1033 map { $_ => $ref }
386 176         388 map { s/=.+//; $_ } ( split( /\|/, $key_basis ), $_ );
  473         822  
  473         786  
387             } keys %options;
388              
389             # Now let's go through each option. For arrays, we want the configuration
390             # options to appear in order at the front. So if configuration had 1, 2,
391             # and command line options were 3, 4, we want: 1, 2, 3, 4. This is not
392             # straight forward.
393 11         62 my %additions;
394 11         45 while (@configuration_options) {
395 5         9 my ($key) = shift(@configuration_options);
396 5         10 my ($value) = shift(@configuration_options);
397 5   50     14 my $target = $keys{$key} || die "Unknown configuration option [$key]";
398              
399 5 100 66     24 if ( $key eq 'extensions' || $key eq 'extension' ) {
    100          
400 1 50       5 die "Value of $key in config file expected to be HASH but isn't"
401             if ref $value ne 'HASH';
402              
403             # if the configuration of the extension is 'undef', then
404             # none was defined. Replace it with an empty hashref, which
405             # is what Moo's 'new()' method wants later on
406 1   50     4 my @e = map { [ $_, [ $value->{$_} || {} ] ] } keys %$value;
  1         7  
407 1         3 $value = \@e;
408 1   50     10 my $array = $additions{ 0 + $target } ||= [];
409 1         4 push( @$array, @$value );
410 1 50       4 print "Adding extensions near the front of $key"
411             if $deref->('debug_profiles');
412             } elsif ( ref $target ne 'ARRAY' ) {
413              
414             # Only use it if we don't have something already
415 2 100       8 if ( defined $$target ) {
416 1 50       4 print
417             "Ignoring $key from config file because set on cmd line as $$target\n"
418             if $deref->('debug_profiles');
419             } else {
420 1         2 $$target = $value;
421 1 50       4 print "Set $key to $target from config file\n"
422             if $deref->('debug_profiles');
423             }
424              
425             } else {
426 2   100     11 my $array = $additions{ 0 + $target } ||= [];
427 2         6 push( @$array, $value );
428 2 50       4 print "Adding $value near the front of $key\n"
429             if $deref->('debug_profiles');
430             }
431             }
432 11         44 for my $target ( values %options ) {
433 176 100       355 next unless ref $target->[1] eq 'ARRAY';
434 33         65 my $key = $target->[1] + 0;
435 33 100       43 unshift( @{ $target->[1] }, @{ $additions{$key} || [] } );
  33         63  
  33         161  
436             }
437              
438 11 50       42 if ( $deref->('debug_profiles') ) {
439 0         0 print "Values are:\n";
440 0         0 for ( sort keys %options ) {
441 0         0 printf( " %16s: ", $_ );
442 0         0 my $value = $deref->($_);
443 0 0       0 if ( ref $value ) {
444 0         0 print join ', ', @$value;
445             } else {
446 0 0       0 print( ( defined $value ) ? $value : '[undefined]' );
447             }
448 0         0 print "\n";
449             }
450 0         0 exit;
451             }
452              
453 11 50       27 if ( my $i18n = $deref->('i18n') ) {
454 0 0       0 _print_langdef($i18n) unless $i18n eq 'help';
455 0         0 _print_languages();
456             }
457              
458 11 100       24 unshift @{ $deref->('includes') }, 'lib' if $deref->('lib');
  3         6  
459 11 100       28 unshift @{ $deref->('includes') }, 'blib/lib', 'blib/arch'
  2         5  
460             if $deref->('blib');
461              
462             # We may need some of the imported paths...
463 11         26 lib->import( @{ $deref->('includes') } );
  11         20  
464              
465             # Load any extensions
466 11         1004 for my $e ( @{ $deref->('extensions') } ) {
  11         29  
467 3         13 my ( $c, $a ) = @$e;
468 3         15 use_module $c;
469              
470 3         3151 my $instance = $c->new(@$a);
471 3         2381 push( @{ $self->extensions }, $instance );
  3         61  
472              
473 3         29 my $dir = file( $INC{ module_notional_filename($c) } )->dir;
474 3         46 my @step_dirs = map { File::Spec->rel2abs( $_, $dir ) }
475 3         439 @{ $instance->step_directories };
  3         10  
476 3         286 unshift( @{ $deref->('steps') }, @step_dirs );
  3         10  
477             }
478              
479             # Munge the output harness
480 11   50     31 $self->_initialize_harness( $deref->('output') || "TermColor" );
481              
482             # Store any extra step paths
483 11         277 $self->step_paths( $deref->('steps') );
484              
485 11   100     326 $self->tags( $deref->('tags') // '' );
486              
487 11 50       360 $self->matching( $deref->('matching') )
488             if $deref->('matching');
489              
490             # Match only?
491 11         32 $self->match_only( $deref->('match_only') );
492              
493 11 50       295 $self->strict( $deref->('strict') )
494             if $deref->('strict');
495              
496 11         211 return ( pop @ARGV );
497             }
498              
499             sub _print_languages {
500              
501 0     0   0 my @languages = languages();
502              
503 0         0 my $max_code_length = max map {length} @languages;
  0         0  
504             my $max_name_length
505 0         0 = max map { length( langdef($_)->{name} ) } @languages;
  0         0  
506             my $max_native_length
507 0         0 = max map { length( langdef($_)->{native} ) } @languages;
  0         0  
508              
509 0         0 my $format
510             = "| %-${max_code_length}s | %-${max_name_length}s | %-${max_native_length}s |\n";
511              
512 0         0 for my $language ( sort @languages ) {
513 0         0 my $langdef = langdef($language);
514 0         0 printf $format, $language, $langdef->{name}, $langdef->{native};
515             }
516 0         0 exit;
517             }
518              
519             sub _print_langdef {
520 0     0   0 my ($language) = @_;
521              
522 0         0 my $langdef = langdef($language);
523              
524 0         0 my @keywords = qw(feature background scenario scenarioOutline examples
525             given when then and but);
526             my $max_length
527 0         0 = max map { length readable_keywords( $langdef->{$_} ) } @keywords;
  0         0  
528              
529 0         0 my $format = "| %-16s | %-${max_length}s |\n";
530 0         0 for my $keyword (
531             qw(feature background scenario scenarioOutline
532             examples given when then and but )
533             )
534             {
535 0         0 printf $format, $keyword, readable_keywords( $langdef->{$keyword} );
536             }
537              
538 0         0 my $codeformat = "| %-16s | %-${max_length}s |\n";
539 0         0 for my $keyword (qw(given when then )) {
540             printf $codeformat, $keyword . ' (code)',
541 0         0 readable_keywords( $langdef->{$keyword}, \&keyword_to_subname );
542             }
543              
544 0         0 exit;
545             }
546              
547             sub _make_executor_match_only {
548 1     1   11 my ($self, $executor) = @_;
549              
550             my $match_sub = sub {
551 3     3   118 my $context = shift;
552 3         18 $Test::Builder::Test->ok( 1, "Test matched" );
553 3         1267 return 1;
554 1         6 };
555              
556 1         2 for my $verb ( keys %{$executor->steps} ) {
  1         19  
557 3         12 for my $step_tuple ( @{ $executor->steps->{$verb} } ) {
  3         47  
558 3         59 $step_tuple->[2] = $match_sub;
559             }
560             }
561              
562 1         4 return 1;
563             }
564              
565             sub _rename_feature_steps {
566 1     1   4 my ($self, @features) = @_;
567              
568 1         1 my %steps;
569 1         3 for my $feature ( @features ) {
570 1         26 for my $scenario ( $feature->background, @{ $feature->scenarios } ) {
  1         22  
571 2 100       11 next unless $scenario;
572 1         2 for my $step ( @{ $scenario->steps } ) {
  1         19  
573 3         18 $steps{ $step . '' } = $step;
574             }
575             }
576             }
577              
578 1         4 for my $step_object ( values %steps ) {
579 3   33     150 $step_object->verb_original(
580             'MATCH MODE: ' . ( $step_object->verb_original || $step_object->verb )
581             );
582             }
583             }
584              
585             =head1 AUTHOR
586              
587             Peter Sergeant C
588              
589             =head1 LICENSE
590              
591             Copyright 2019-2023, Erik Huelsmann
592             Copyright 2011-2019, Peter Sergeant; Licensed under the same terms as Perl
593              
594             =cut
595              
596             1;