File Coverage

lib/App/pherkin.pm
Criterion Covered Total %
statement 274 322 85.0
branch 78 120 65.0
condition 19 35 54.2
subroutine 32 34 94.1
pod 1 1 100.0
total 404 512 78.9


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