File Coverage

lib/App/pherkin.pm
Criterion Covered Total %
statement 267 310 86.1
branch 84 118 71.1
condition 20 34 58.8
subroutine 32 34 94.1
pod 1 1 100.0
total 404 497 81.2


line stmt bran cond sub pod time code
1 14     14   606733 use v5.14;
  14         50  
2 14     14   78 use warnings;
  14         39  
  14         1068  
3              
4             package App::pherkin 0.87;
5              
6              
7 14     14   5335 use lib;
  14         8921  
  14         84  
8 14     14   10267 use Getopt::Long;
  14         216795  
  14         82  
9 14     14   8666 use Module::Runtime qw(use_module module_notional_filename);
  14         23324  
  14         124  
10 14     14   1011 use List::Util qw(max);
  14         29  
  14         1610  
11 14     14   7596 use Pod::Usage;
  14         1069128  
  14         3627  
12 14     14   8717 use FindBin qw($RealBin $Script);
  14         20474  
  14         1888  
13 14     14   7341 use YAML qw( LoadFile );
  14         115039  
  14         1035  
14 14     14   9105 use Data::Dumper;
  14         126384  
  14         1123  
15 14     14   124 use File::Spec;
  14         25  
  14         418  
16 14     14   6285 use Path::Class qw/file dir/;
  14         571642  
  14         1256  
17              
18 14     14   7661 use Cucumber::TagExpressions;
  14         596343  
  14         735  
19              
20             use Test::BDD::Cucumber::I18n
21 14     14   7311 qw(languages langdef readable_keywords keyword_to_subname);
  14         50  
  14         1476  
22 14     14   6926 use Test::BDD::Cucumber::Loader;
  14         79  
  14         648  
23              
24 14     14   104 use Moo;
  14         28  
  14         83  
25 14     14   7547 use Types::Standard qw( ArrayRef Bool Str );
  14         35  
  14         180  
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.87
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 14     14   56 my ( $self, @arguments ) = @_;
73              
74             # localized features will have utf8 in them and options may output utf8 as
75             # well
76 14         89 binmode STDOUT, ':utf8';
77              
78 14         186 my ($features_path) = $self->_process_arguments(@arguments);
79 14   50     63 $features_path ||= './features/';
80              
81 14         186 my ( $executor, @features ) =
82             Test::BDD::Cucumber::Loader->load( $features_path );
83 14 50       162 die "No feature files found in $features_path" unless @features;
84              
85 14         603 $executor->matching( $self->matching );
86 14         909 $executor->add_extensions($_) for @{ $self->extensions };
  14         349  
87 14         240 $_->pre_execute($self) for @{ $self->extensions };
  14         270  
88              
89             Test::BDD::Cucumber::Loader->load_steps( $executor, $_ )
90 14         114 for @{ $self->step_paths };
  14         356  
91              
92 14         184 return ( $executor, @features );
93             }
94              
95             sub _post_run {
96 14     14   36 my $self = shift;
97              
98 14         30 $_->post_execute() for reverse @{ $self->extensions };
  14         426  
99             }
100              
101              
102             sub run {
103 14     14 1 6553 my ( $self, @arguments ) = @_;
104 14         211 my ( $executor, @features ) = $self->_pre_run(@arguments);
105              
106 14 100       317 if ( $self->match_only ) {
107 1 50       28 $self->_make_executor_match_only($executor) if $self->match_only;
108 1         5 $self->_rename_feature_steps( @features );
109             }
110              
111 14         218 my $result = $self->_run_tests( $executor, @features );
112 14         79 $self->_post_run;
113 14         2327 return $result;
114             }
115              
116             sub _run_tests {
117 14     14   50 my ( $self, $executor, @features ) = @_;
118              
119 14         96 my $harness = $self->harness;
120 14         288 $harness->startup();
121              
122 14         22 my $tag_spec;
123 14 100       420 if ( $self->tags ) {
124 4         113 $tag_spec = Cucumber::TagExpressions->parse( $self->tags );
125             }
126              
127 14         9729 $executor->execute( $_, $harness, $tag_spec ) for @features;
128              
129 14         349 $harness->shutdown();
130              
131 14         114 my $exit_code = 0;
132 14         133 my $result = $harness->result->result;
133 14 100       1720 if ($result eq 'failing') {
    100          
134 1         5 $exit_code = 2;
135             }
136             elsif ($self->strict) {
137 1 50 33     15 if ($result eq 'pending'
138             or $result eq 'undefined') {
139 1         2 $exit_code = 1;
140             }
141             }
142              
143 14         235 return $exit_code;
144             }
145              
146             sub _initialize_harness {
147 25     25   453 my ( $self, $harness_module ) = @_;
148              
149 25         70 my $harness_args_string = undef;
150 25         99 ( $harness_module, $harness_args_string ) = split /\(/, $harness_module, 2;
151            
152 25 100       134 unless ( $harness_module =~ m/::/ ) {
153 21         66 $harness_module = "Test::BDD::Cucumber::Harness::" . $harness_module;
154             }
155              
156 25 50       55 eval { use_module($harness_module) }
  25         135  
157             || die "Unable to load harness [$harness_module]: $@";
158              
159 25 100       694 if ( $harness_args_string ) {
160 1         3 my %harness_args;
161 1 50       75 eval "%harness_args = ($harness_args_string; 1"
162             or die $@;
163 1         36 $self->harness( $harness_module->new( %harness_args ) );
164             } else {
165 24         352 $self->harness( $harness_module->new() );
166             }
167            
168             }
169              
170             sub _find_config_file {
171 31     31   368411 my ( $self, $config_filename, $debug ) = @_;
172              
173 31 100       139 return $config_filename if $config_filename;
174              
175 21   100     153 for (
176             ( $ENV{'PHERKIN_CONFIG'} || () ),
177              
178             # Allow .yaml or .yml for all of the below
179 105         5871 map { ( "$_.yaml", "$_.yml" ) } (
180              
181             # Relative locations
182 84         6705 ( map { file($_) }
183             qw!.pherkin config/pherkin ./.config/pherkin t/.pherkin!
184             ),
185              
186             # Home locations
187 21         96 ( map { dir($_)->file('.pherkin') }
188 42         96 grep {$_} map { $ENV{$_} } qw/HOME USERPROFILE/
  42         1609  
189             )
190             )
191             )
192             {
193 201 100       2462 return $_ if -f $_;
194 200 50       1075 print "No config file found in $_\n" if $debug;
195             }
196 20         275 return undef;
197             }
198              
199             sub _replace_helper {
200 3     3   11 my $inval = shift;
201              
202 3   66     31 return $ENV{$inval} // "Environment variable $inval not defined";
203             }
204              
205             sub _resolve_envvars {
206 170     170   220 my ( $config_data ) = shift;
207              
208 170 100       220 if (ref $config_data) {
209 92 100       3210 if (ref $config_data eq 'HASH') {
    50          
210             return {
211             map {
212 66         194 $_ => _resolve_envvars( $config_data->{$_} )
  123         203  
213             } keys %$config_data
214             };
215             }
216             elsif (ref $config_data eq 'ARRAY') {
217 26         46 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         169 $config_data =~ s/(?
  3         21  
226             # remove any escaping double dollar-signs
227 78         121 $config_data =~ s/\$(\$\{[a-zA-Z0-9_]+\})/$1/g;
228             }
229              
230 78         341 return $config_data;
231             }
232              
233             sub _load_config {
234 29     29   9767 my ( $self, $profile_name, $proposed_config_filename, $debug ) = @_;
235              
236 29         137 my $config_filename
237             = $self->_find_config_file( $proposed_config_filename, $debug );
238 29         122 my $config_data_whole;
239              
240             # Check we can actually load some data from that file if required
241 29 100       123 if ($config_filename) {
242 9 50       57 print "Found [$config_filename], reading...\n" if $debug;
243 9         104 $config_data_whole = LoadFile($config_filename);
244 8 50       106997 $config_data_whole = _resolve_envvars( $config_data_whole )
245             if $config_data_whole;
246             } else {
247 20 50       56 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 20 50       57 print "No configuration files found, and no profile specified\n"
253             if $debug;
254 20         72 return;
255             }
256             }
257              
258 8 100       78 $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       32 unless ( ref $config_data_whole eq 'HASH' ) {
262 1         16 die
263             "Config file [$config_filename] doesn't return a hashref on parse, instead a ["
264             . ref($config_data_whole) . ']';
265             }
266 7         30 my $config_data = $config_data_whole->{$profile_name};
267             my $profile_problem = sub {
268 3     3   43 return "Config file [$config_filename] profile [$profile_name]: "
269             . shift();
270 7         75 };
271 7 100       23 unless ($config_data) {
272 1         4 die $profile_problem->("Profile not found");
273             }
274 6 100       25 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       11 print "Using profile [$profile_name]\n" if $debug;
278              
279             # Transform it in to an argument list
280 5         15 my @arguments;
281 5         34 for my $key ( sort keys %$config_data ) {
282 9         15 my $value = $config_data->{$key};
283              
284 9 100       23 if ( my $reftype = ref $value ) {
285 6 100       19 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         14 push( @arguments, $key, $_ ) for @$value;
290             } else {
291 2 50 33     16 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         12 push( @arguments, $key, $value );
296             }
297             } else {
298 3         9 push( @arguments, $key, $value );
299             }
300             }
301              
302 4 50       17 if ($debug) {
303 0         0 print "Arguments to add: " . ( join ' ', @arguments ) . "\n";
304             }
305              
306 4         109 return @arguments;
307             }
308              
309             sub _process_arguments {
310 20     20   529 my ( $self, @args ) = @_;
311 20         72 local @ARGV = @args;
312              
313             # Allow -Ilib, -bl
314 20         157 Getopt::Long::Configure( 'bundling', 'pass_through' );
315              
316 20         1464 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 20         96 my $x;
  320         381  
342 320 100       728 $_->[1] = \$x unless defined $_->[1];
343 320         658 ( $_->[0] => $_->[1] );
344             } values %options
345             );
346              
347             my $deref = sub {
348 374     374   630 my $key = shift;
349 374         750 my $value = $options{$key}->[1];
350 374 100       3042 return ( ref $value eq 'ARRAY' ) ? $value : $$value;
351 20         33849 };
352              
353 20 50       73 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 20 50       80 -verbose => 1,
363             -input => "$RealBin/$Script",
364             ) if $deref->('help');
365              
366 20         47 my @parsed_extensions;
367 20         42 for my $e ( @{ $deref->('extensions') } ) {
  20         47  
368 2         4 my $e_args = "()";
369 2 50       14 $e_args = $1 if $e =~ s/\((.+)\)$//;
370 2         105 my @e_args = eval $e_args;
371 2 50       8 die "Bad arguments in [$e]: $@" if $@;
372              
373 2         7 push( @parsed_extensions, [ $e, \@e_args ] );
374             }
375 20         61 $options{extensions}->[1] = \@parsed_extensions;
376              
377             # Load the configuration file
378 20         63 my @configuration_options = $self->_load_config( map { $deref->($_) }
  60         118  
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 20         121 my ( $key_basis, $ref ) = @{ $options{$_} };
  320         393  
  320         598  
385 860         1869 map { $_ => $ref }
386 320         560 map { s/=.+//; $_ } ( split( /\|/, $key_basis ), $_ );
  860         1343  
  860         1271  
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 20         150 my %additions;
394 20         83 while (@configuration_options) {
395 5         7 my ($key) = shift(@configuration_options);
396 5         6 my ($value) = shift(@configuration_options);
397 5   50     11 my $target = $keys{$key} || die "Unknown configuration option [$key]";
398              
399 5 100 66     23 if ( $key eq 'extensions' || $key eq 'extension' ) {
    100          
400 1 50       3 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     5 my @e = map { [ $_, [ $value->{$_} || {} ] ] } keys %$value;
  1         6  
407 1         2 $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       6 if ( defined $$target ) {
416 1 50       5 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       3 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         3 push( @$array, $value );
428 2 50       4 print "Adding $value near the front of $key\n"
429             if $deref->('debug_profiles');
430             }
431             }
432 20         75 for my $target ( values %options ) {
433 320 100       688 next unless ref $target->[1] eq 'ARRAY';
434 60         116 my $key = $target->[1] + 0;
435 60 100       92 unshift( @{ $target->[1] }, @{ $additions{$key} || [] } );
  60         134  
  60         255  
436             }
437              
438 20 50       78 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 20 50       57 if ( my $i18n = $deref->('i18n') ) {
454 0 0       0 _print_langdef($i18n) unless $i18n eq 'help';
455 0         0 _print_languages();
456             }
457              
458 20 100       54 unshift @{ $deref->('includes') }, 'lib' if $deref->('lib');
  3         5  
459 20 100       52 unshift @{ $deref->('includes') }, 'blib/lib', 'blib/arch'
  2         6  
460             if $deref->('blib');
461              
462             # We may need some of the imported paths...
463 20         44 lib->import( @{ $deref->('includes') } );
  20         48  
464              
465             # Load any extensions
466 20         1080 for my $e ( @{ $deref->('extensions') } ) {
  20         52  
467 3         11 my ( $c, $a ) = @$e;
468 3         18 use_module $c;
469              
470 3         3783 my $instance = $c->new(@$a);
471 3         2178 push( @{ $self->extensions }, $instance );
  3         85  
472              
473 3         30 my $dir = file( $INC{ module_notional_filename($c) } )->dir;
474 3         57 my @step_dirs = map { File::Spec->rel2abs( $_, $dir ) }
475 3         508 @{ $instance->step_directories };
  3         11  
476 3         253 unshift( @{ $deref->('steps') }, @step_dirs );
  3         11  
477             }
478              
479             # Munge the output harness
480 20   100     76 $self->_initialize_harness( $deref->('output') || "TermColor" );
481              
482             # Store any extra step paths
483 20         680 $self->step_paths( $deref->('steps') );
484              
485 20   100     676 $self->tags( $deref->('tags') // '' );
486              
487 20 50       655 $self->matching( $deref->('matching') )
488             if $deref->('matching');
489              
490             # Match only?
491 20         61 $self->match_only( $deref->('match_only') );
492              
493 20 100       855 $self->strict( $deref->('strict') )
494             if $deref->('strict');
495              
496 20         652 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   12 my ($self, $executor) = @_;
549              
550             my $match_sub = sub {
551 3     3   7 my $context = shift;
552 3         29 $Test::Builder::Test->ok( 1, "Test matched" );
553 3         1637 return 1;
554 1         6 };
555              
556 1         2 for my $verb ( keys %{$executor->steps} ) {
  1         24  
557 3         15 for my $step_tuple ( @{ $executor->steps->{$verb} } ) {
  3         82  
558 3         30 $step_tuple->[2] = $match_sub;
559             }
560             }
561              
562 1         5 return 1;
563             }
564              
565             sub _rename_feature_steps {
566 1     1   3 my ($self, @features) = @_;
567              
568 1         2 my %steps;
569 1         3 for my $feature ( @features ) {
570 1         25 for my $scenario ( $feature->background, @{ $feature->scenarios } ) {
  1         29  
571 2 100       11 next unless $scenario;
572 1         2 for my $step ( @{ $scenario->steps } ) {
  1         23  
573 3         19 $steps{ $step . '' } = $step;
574             }
575             }
576             }
577              
578 1         3 for my $step_object ( values %steps ) {
579 3   33     210 $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;