From eca13bbe644767172691c12cd3800e5a2e96f7db Mon Sep 17 00:00:00 2001 From: Peter Sergeant Date: Wed, 31 Aug 2016 08:14:57 +0700 Subject: [PATCH 01/10] Feature sourcehandler --- .../features/step_definitions/basic_steps.pl | 2 +- lib/TAP/Parser/SourceHandler/Feature.pm | 71 +++++++++++++++++++ lib/Test/BDD/Cucumber/Harness/TestBuilder.pm | 60 +++++++++------- 3 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 lib/TAP/Parser/SourceHandler/Feature.pm diff --git a/examples/tagged-digest/features/step_definitions/basic_steps.pl b/examples/tagged-digest/features/step_definitions/basic_steps.pl index 474164a..f6c3315 100755 --- a/examples/tagged-digest/features/step_definitions/basic_steps.pl +++ b/examples/tagged-digest/features/step_definitions/basic_steps.pl @@ -29,5 +29,5 @@ 'hex' => 'hexdigest' }->{$type}; - is( S->{'object'}->$method, $expected ); + is( S->{'object'}->$method, $expected ); }; diff --git a/lib/TAP/Parser/SourceHandler/Feature.pm b/lib/TAP/Parser/SourceHandler/Feature.pm new file mode 100644 index 0000000..0fa95e4 --- /dev/null +++ b/lib/TAP/Parser/SourceHandler/Feature.pm @@ -0,0 +1,71 @@ +package TAP::Parser::SourceHandler::Feature; + +use strict; +use warnings; + +use base 'TAP::Parser::SourceHandler'; +use TAP::Parser::Iterator::Process; + +use Test::BDD::Cucumber::Loader; +use Test::BDD::Cucumber::Harness::TestBuilder; + +use Path::Class qw/file/; + +TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); + +sub can_handle { + my ( $class, $source ) = @_; + + #use Data::Printer; p $source; exit; + + if ( $source->meta->{'is_file'} + && $source->meta->{'file'}->{'basename'} =~ m/\.feature$/ ) + { + + my $dir = $source->meta->{'file'}->{'dir'}; + unless ( $source->{'cucumber_executors'}->{$dir} ) { + my ($executor) = Test::BDD::Cucumber::Loader->load($dir); + $source->{'cucumber_executors'}->{$dir} = $executor; + } + return 1; + } + + return 0; +} + +sub make_iterator { + my ( $class, $source ) = @_; + + my $tagspec = $source->config_for($class)->{'tagspec'}; + + my ($input_fh, $output_fh); + pipe $input_fh, $output_fh; + + #open( my $output_fh, '>', 'filename.tmp'); + my $tb = Test::Builder->create(); + $tb->output($output_fh); + + #open( my $input_fh, '<', 'filename.tmp'); + + my $it = TAP::Parser::Iterator::Stream->new($input_fh); + + my $harness = Test::BDD::Cucumber::Harness::TestBuilder->new( + { fail_skip => 1, + _tb_instance => $tb, + } + ); + + my $dir = $source->meta->{'file'}->{'dir'}; + my $executor = $source->{'cucumber_executors'}->{$dir} + || die "No executor instantiated for [$dir]"; + + my $feature = Test::BDD::Cucumber::Parser->parse_file( + $dir . $source->meta->{'file'}->{'basename'}, $tagspec ); + + $executor->execute( $feature, $harness ); + $tb->done_testing(); + + return $it; +} + +1; diff --git a/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm b/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm index 9aed4d8..726c5cd 100644 --- a/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm +++ b/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm @@ -24,24 +24,31 @@ use Test::More; extends 'Test::BDD::Cucumber::Harness'; has 'fail_skip' => ( is => 'rw', isa => 'Bool', default => 0 ); +has '_tb_instance' => ( is => 'rw', isa => 'Test::Builder' ); my $li = ' ' x 7; my $ni = ' ' x 4; my $si = ' ' x 9; my $di = ' ' x 17; +sub _tb { + my $self = shift; + return $self->_tb_instance || Test::Builder->new(); +} + sub feature { my ( $self, $feature ) = @_; - note "${li}Feature: " . $feature->name; - note "$li$ni" . $_->content for @{ $feature->satisfaction }; - note ""; + $self->_tb->note( "${li}Feature: " . $feature->name ); + $self->_tb->note( "$li$ni" . $_->content ) + for @{ $feature->satisfaction }; + $self->_tb->note(""); } sub scenario { my ( $self, $scenario, $dataset ) = @_; - note "$li${ni}Scenario: " . ( $scenario->name || '' ); + $self->_tb->note( "$li${ni}Scenario: " . ( $scenario->name || '' ) ); } -sub scenario_done { note ""; } +sub scenario_done { my $self = shift; $self->_tb->note(""); } sub step { } @@ -55,42 +62,43 @@ sub step_done { if ( $context->is_hook ) { $status ne 'undefined' - and $status ne 'pending' - and $status ne 'passing' - or return; + and $status ne 'pending' + and $status ne 'passing' + or return; $step_name = 'In ' . ucfirst( $context->verb ) . ' Hook'; } else { - $step_name = - $si . ucfirst( $step->verb_original ) . ' ' . $context->text; + $step_name + = $si . ucfirst( $step->verb_original ) . ' ' . $context->text; } if ( $status eq 'undefined' || $status eq 'pending' ) { if ( $self->fail_skip ) { if ( $status eq 'undefined' ) { - fail("No matcher for: $step_name"); + $self->_tb->ok( 0, "No matcher for: $step_name" ); } else { - fail("Test skipped due to failure in previous step"); + $self->_tb->ok( 0, + "Test skipped due to failure in previous step" ); } $self->_note_step_data($step); } else { - TODO: { todo_skip $step_name, 1 } + TODO: { todo_skip $step_name, 1 } $self->_note_step_data($step); } } elsif ( $status eq 'passing' ) { - pass($step_name); + $self->_tb->ok( 1, $step_name ); $self->_note_step_data($step); } else { - fail($step_name); + $self->_tb->ok( 0, $step_name ); $self->_note_step_data($step); if ( !$context->is_hook ) { - my $step_location = - ' in step at ' - . $step->line->document->filename - . ' line ' - . $step->line->number . '.'; - diag($step_location); + my $step_location + = ' in step at ' + . $step->line->document->filename + . ' line ' + . $step->line->number . '.'; + $self->_tb->diag($step_location); } - diag( $result->output ); + $self->_tb->diag( $result->output ); } } @@ -105,15 +113,15 @@ sub _note_step_data { note( $di . $_ ); } } else { - note $di . '"""'; + $self->_tb->note( $di . '"""' ); for (@step_data) { - note( $di . ' ' . $_ ); + $self->_tb->note( $di . ' ' . $_ ); } - note $di . '"""'; + $self->_tb->note( $di . '"""' ); } } -sub shutdown { done_testing(); } +sub shutdown { my $self = shift; $self->_tb->note( done_testing() ); } =head1 AUTHOR From 061d26609a75b87462a84ec87c83b870bc388bb9 Mon Sep 17 00:00:00 2001 From: Peter Sergeant Date: Thu, 1 Sep 2016 08:33:53 +0700 Subject: [PATCH 02/10] Latest `prove` feature magic --- lib/App/pherkin.pm | 11 ++++- lib/TAP/Parser/SourceHandler/Feature.pm | 47 +++++++++++++------- lib/Test/BDD/Cucumber/Harness/TestBuilder.pm | 2 +- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/lib/App/pherkin.pm b/lib/App/pherkin.pm index 110a973..93bd312 100644 --- a/lib/App/pherkin.pm +++ b/lib/App/pherkin.pm @@ -58,10 +58,11 @@ Returns a L object for all steps run. =cut -sub run { +sub _pre_run { my ( $self, @arguments ) = @_; - # localized features will have utf8 in them and options may output utf8 as well + # localized features will have utf8 in them and options may output utf8 as + # well binmode STDOUT, ':utf8'; my ($features_path) = $self->_process_arguments(@arguments); @@ -76,6 +77,12 @@ sub run { Test::BDD::Cucumber::Loader->load_steps( $executor, $_ ) for @{ $self->step_paths }; + return ( $executor, @features ); +} + +sub run { + my ( $self, @arguments ) = @_; + my ( $executor, @features ) = $self->_pre_run( @arguments ); return $self->_run_tests( $executor, @features ); } diff --git a/lib/TAP/Parser/SourceHandler/Feature.pm b/lib/TAP/Parser/SourceHandler/Feature.pm index 0fa95e4..bec80fb 100644 --- a/lib/TAP/Parser/SourceHandler/Feature.pm +++ b/lib/TAP/Parser/SourceHandler/Feature.pm @@ -3,9 +3,13 @@ package TAP::Parser::SourceHandler::Feature; use strict; use warnings; +use Path::Class qw/file/; + use base 'TAP::Parser::SourceHandler'; use TAP::Parser::Iterator::Process; +use App::pherkin; + use Test::BDD::Cucumber::Loader; use Test::BDD::Cucumber::Harness::TestBuilder; @@ -16,16 +20,24 @@ TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); sub can_handle { my ( $class, $source ) = @_; - #use Data::Printer; p $source; exit; - if ( $source->meta->{'is_file'} && $source->meta->{'file'}->{'basename'} =~ m/\.feature$/ ) { my $dir = $source->meta->{'file'}->{'dir'}; - unless ( $source->{'cucumber_executors'}->{$dir} ) { - my ($executor) = Test::BDD::Cucumber::Loader->load($dir); - $source->{'cucumber_executors'}->{$dir} = $executor; + unless ( $source->{'pherkins'}->{$dir} ) { + + my $pherkin = App::pherkin->new(); + my ( $executor, @features ) = $pherkin->_pre_run($dir); + + $source->{'pherkins'}->{$dir} = { + pherkin => $pherkin, + executor => $executor, + features => { + map { ( file( $_->document->filename ) . '' ) => $_ } + @features + } + }; } return 1; } @@ -38,15 +50,12 @@ sub make_iterator { my $tagspec = $source->config_for($class)->{'tagspec'}; - my ($input_fh, $output_fh); + my ( $input_fh, $output_fh ); pipe $input_fh, $output_fh; - #open( my $output_fh, '>', 'filename.tmp'); my $tb = Test::Builder->create(); $tb->output($output_fh); - #open( my $input_fh, '<', 'filename.tmp'); - my $it = TAP::Parser::Iterator::Stream->new($input_fh); my $harness = Test::BDD::Cucumber::Harness::TestBuilder->new( @@ -55,15 +64,21 @@ sub make_iterator { } ); - my $dir = $source->meta->{'file'}->{'dir'}; - my $executor = $source->{'cucumber_executors'}->{$dir} - || die "No executor instantiated for [$dir]"; + my $dir = $source->meta->{'file'}->{'dir'}; + my $runtime = $source->{'pherkins'}->{$dir} + || die "No pherkin instantiation for [$dir]"; + + my $executor = $runtime->{'executor'}; + my $pherkin = $runtime->{'pherkin'}; + $pherkin->harness($harness); + + my $filename = file( $dir . $source->meta->{'file'}->{'basename'} ) . ''; - my $feature = Test::BDD::Cucumber::Parser->parse_file( - $dir . $source->meta->{'file'}->{'basename'}, $tagspec ); + my $feature = $runtime->{'features'}->{$filename} + || die "Feature not pre-loaded: [$filename]; have: " + . ( join '; ', keys %{ $runtime->{'features'} } ); - $executor->execute( $feature, $harness ); - $tb->done_testing(); + $pherkin->_run_tests( $executor, $feature ); return $it; } diff --git a/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm b/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm index 726c5cd..acd4377 100644 --- a/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm +++ b/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm @@ -121,7 +121,7 @@ sub _note_step_data { } } -sub shutdown { my $self = shift; $self->_tb->note( done_testing() ); } +sub shutdown { my $self = shift; $self->_tb->note( $self->_tb->done_testing() ); } =head1 AUTHOR From 13448345480bdbbc02967433d44c001846afb26c Mon Sep 17 00:00:00 2001 From: Peter Sergeant Date: Sun, 4 Sep 2016 15:42:21 +0700 Subject: [PATCH 03/10] Pass-through options to prove --- lib/TAP/Parser/SourceHandler/Feature.pm | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/TAP/Parser/SourceHandler/Feature.pm b/lib/TAP/Parser/SourceHandler/Feature.pm index bec80fb..137a216 100644 --- a/lib/TAP/Parser/SourceHandler/Feature.pm +++ b/lib/TAP/Parser/SourceHandler/Feature.pm @@ -28,7 +28,22 @@ sub can_handle { unless ( $source->{'pherkins'}->{$dir} ) { my $pherkin = App::pherkin->new(); - my ( $executor, @features ) = $pherkin->_pre_run($dir); + + # Reformulate before passing to the cmd line parser + my @cmd_line; + my %options = %{ $source->config_for($class) }; + while ( my ( $key, $value ) = each %options ) { + + # Nasty hack + if ( length $key > 1 ) { + push( @cmd_line, "--$key", $value ); + } else { + push( @cmd_line, "-$key", $value ); + } + } + + my ( $executor, @features ) + = $pherkin->_pre_run( @cmd_line, $dir ); $source->{'pherkins'}->{$dir} = { pherkin => $pherkin, @@ -48,8 +63,6 @@ sub can_handle { sub make_iterator { my ( $class, $source ) = @_; - my $tagspec = $source->config_for($class)->{'tagspec'}; - my ( $input_fh, $output_fh ); pipe $input_fh, $output_fh; From afa4a7808d07515bb438e605e2ce496bdd1297a5 Mon Sep 17 00:00:00 2001 From: Peter Sergeant Date: Wed, 7 Sep 2016 14:55:59 +0700 Subject: [PATCH 04/10] Parallelization works? --- lib/App/pherkin.pm | 103 ++++++++++++------------ lib/TAP/Parser/SourceHandler/Feature.pm | 10 ++- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/lib/App/pherkin.pm b/lib/App/pherkin.pm index 93bd312..3cbcda1 100644 --- a/lib/App/pherkin.pm +++ b/lib/App/pherkin.pm @@ -15,7 +15,7 @@ use File::Spec; use Path::Class qw/file dir/; use Test::BDD::Cucumber::I18n - qw(languages langdef readable_keywords keyword_to_subname); + qw(languages langdef readable_keywords keyword_to_subname); use Test::BDD::Cucumber::Loader; use Moose; @@ -68,21 +68,22 @@ sub _pre_run { my ($features_path) = $self->_process_arguments(@arguments); $features_path ||= './features/'; - my ( $executor, @features ) = - Test::BDD::Cucumber::Loader->load( $features_path, $self->tag_scheme ); + my ( $executor, @features ) + = Test::BDD::Cucumber::Loader->load( $features_path, + $self->tag_scheme ); die "No feature files found in $features_path" unless @features; $executor->add_extensions($_) for @{ $self->extensions }; Test::BDD::Cucumber::Loader->load_steps( $executor, $_ ) - for @{ $self->step_paths }; + for @{ $self->step_paths }; return ( $executor, @features ); } sub run { - my ( $self, @arguments ) = @_; - my ( $executor, @features ) = $self->_pre_run( @arguments ); + my ( $self, @arguments ) = @_; + my ( $executor, @features ) = $self->_pre_run(@arguments); return $self->_run_tests( $executor, @features ); } @@ -114,7 +115,7 @@ sub _initialize_harness { } eval { use_module($harness_module) } - || die "Unable to load harness [$harness_module]: $@"; + || die "Unable to load harness [$harness_module]: $@"; $self->harness( $harness_module->new() ); } @@ -131,18 +132,16 @@ sub _find_config_file { map { ( "$_.yaml", "$_.yml" ) } ( # Relative locations - ( - map { file($_) } - qw!.pherkin config/pherkin ./.config/pherkin t/.pherkin! + ( map { file($_) } + qw!.pherkin config/pherkin ./.config/pherkin t/.pherkin! ), # Home locations - ( - map { dir($_)->file('.pherkin') } - grep { $_ } map { $ENV{$_} } qw/HOME USERPROFILE/ + ( map { dir($_)->file('.pherkin') } + grep {$_} map { $ENV{$_} } qw/HOME USERPROFILE/ ) ) - ) + ) { return $_ if -f $_; print "No config file found in $_\n" if $debug; @@ -153,8 +152,8 @@ sub _find_config_file { sub _load_config { my ( $self, $profile_name, $proposed_config_filename, $debug ) = @_; - my $config_filename = - $self->_find_config_file( $proposed_config_filename, $debug ); + my $config_filename + = $self->_find_config_file( $proposed_config_filename, $debug ); my $config_data_whole; # Check we can actually load some data from that file if required @@ -165,10 +164,10 @@ sub _load_config { if ($profile_name) { print "No configuration files found\n" if $debug; die -"Profile name [$profile_name] specified, but no configuration file found (use --debug-profiles to debug)"; + "Profile name [$profile_name] specified, but no configuration file found (use --debug-profiles to debug)"; } else { print "No configuration files found, and no profile specified\n" - if $debug; + if $debug; return; } } @@ -178,13 +177,13 @@ sub _load_config { # Check the config file has the right type of data at the profile name unless ( ref $config_data_whole eq 'HASH' ) { die -"Config file [$config_filename] doesn't return a hashref on parse, instead a [" - . ref($config_data_whole) . ']'; + "Config file [$config_filename] doesn't return a hashref on parse, instead a [" + . ref($config_data_whole) . ']'; } my $config_data = $config_data_whole->{$profile_name}; my $profile_problem = sub { return "Config file [$config_filename] profile [$profile_name]: " - . shift(); + . shift(); }; unless ($config_data) { die $profile_problem->("Profile not found"); @@ -202,14 +201,14 @@ sub _load_config { if ( my $reftype = ref $value ) { if ( $key ne 'extensions' ) { die $profile_problem->( -"Option $key is a [$reftype] but can only be a single value or ARRAY" + "Option $key is a [$reftype] but can only be a single value or ARRAY" ) unless $reftype eq 'ARRAY'; push( @arguments, $key, $_ ) for @$value; } else { die $profile_problem->( -"Option $key is a [$reftype] but can only be a HASH as '$key' is" -. " a special case - see the documentation for details") - unless $reftype eq 'HASH' && $key eq 'extensions'; + "Option $key is a [$reftype] but can only be a HASH as '$key' is" + . " a special case - see the documentation for details" + ) unless $reftype eq 'HASH' && $key eq 'extensions'; push( @arguments, $key, $value ); } } else { @@ -282,14 +281,14 @@ sub _process_arguments { # Load the configuration file my @configuration_options = $self->_load_config( map { $deref->($_) } - qw/profile config debug_profiles/ ); + qw/profile config debug_profiles/ ); # Merge those configuration items # First we need a list of matching keys my %keys = map { my ( $key_basis, $ref ) = @{ $options{$_} }; map { $_ => $ref } - map { s/=.+//; $_ } ( split( /\|/, $key_basis ), $_ ); + map { s/=.+//; $_ } ( split( /\|/, $key_basis ), $_ ); } keys %options; # Now let's go through each option. For arrays, we want the configuration @@ -302,38 +301,37 @@ sub _process_arguments { my ($value) = shift(@configuration_options); my $target = $keys{$key} || die "Unknown configuration option [$key]"; - if ( $key eq 'extensions' || $key eq 'extension' ) - { + if ( $key eq 'extensions' || $key eq 'extension' ) { die "Value of $key in config file expected to be HASH but isn't" if ref $value ne 'HASH'; # if the configuration of the extension is 'undef', then # none was defined. Replace it with an empty hashref, which # is what Moose's 'new()' method wants later on - my @e = map { [ $_, [ $value->{$_} || { } ] ] } keys %$value; + my @e = map { [ $_, [ $value->{$_} || {} ] ] } keys %$value; $value = \@e; my $array = $additions{ 0 + $target } ||= []; push( @$array, @$value ); print "Adding extensions near the front of $key" - if $deref->('debug_profiles'); + if $deref->('debug_profiles'); } elsif ( ref $target ne 'ARRAY' ) { # Only use it if we don't have something already if ( defined $$target ) { print -"Ignoring $key from config file because set on cmd line as $$target\n" - if $deref->('debug_profiles'); + "Ignoring $key from config file because set on cmd line as $$target\n" + if $deref->('debug_profiles'); } else { $$target = $value; print "Set $key to $target from config file\n" - if $deref->('debug_profiles'); + if $deref->('debug_profiles'); } } else { my $array = $additions{ 0 + $target } ||= []; push( @$array, $value ); print "Adding $value near the front of $key\n" - if $deref->('debug_profiles'); + if $deref->('debug_profiles'); } } for my $target ( values %options ) { @@ -364,7 +362,7 @@ sub _process_arguments { unshift @{ $deref->('includes') }, 'lib' if $deref->('lib'); unshift @{ $deref->('includes') }, 'blib/lib', 'blib/arch' - if $deref->('blib'); + if $deref->('blib'); # We may need some of the imported paths... lib->import( @{ $deref->('includes') } ); @@ -377,11 +375,10 @@ sub _process_arguments { my $instance = $c->new(@$a); push( @{ $self->extensions }, $instance ); - my $dir = file($INC{module_notional_filename($c)})->dir; - my @step_dirs = - map { File::Spec->rel2abs( $_, $dir ) } - @{$instance->step_directories}; - unshift( @{$deref->('steps')}, @step_dirs ); + my $dir = file( $INC{ module_notional_filename($c) } )->dir; + my @step_dirs = map { File::Spec->rel2abs( $_, $dir ) } + @{ $instance->step_directories }; + unshift( @{ $deref->('steps') }, @step_dirs ); } # Munge the output harness @@ -436,14 +433,14 @@ sub _print_languages { my @languages = languages(); - my $max_code_length = max map { length } @languages; - my $max_name_length = - max map { length( langdef($_)->{name} ) } @languages; - my $max_native_length = - max map { length( langdef($_)->{native} ) } @languages; + my $max_code_length = max map {length} @languages; + my $max_name_length + = max map { length( langdef($_)->{name} ) } @languages; + my $max_native_length + = max map { length( langdef($_)->{native} ) } @languages; - my $format = -"| %-${max_code_length}s | %-${max_name_length}s | %-${max_native_length}s |\n"; + my $format + = "| %-${max_code_length}s | %-${max_name_length}s | %-${max_native_length}s |\n"; for my $language ( sort @languages ) { my $langdef = langdef($language); @@ -458,15 +455,15 @@ sub _print_langdef { my $langdef = langdef($language); my @keywords = qw(feature background scenario scenario_outline examples - given when then and but); - my $max_length = - max map { length readable_keywords( $langdef->{$_} ) } @keywords; + given when then and but); + my $max_length + = max map { length readable_keywords( $langdef->{$_} ) } @keywords; my $format = "| %-16s | %-${max_length}s |\n"; for my $keyword ( qw(feature background scenario scenario_outline examples given when then and but ) - ) + ) { printf $format, $keyword, readable_keywords( $langdef->{$keyword} ); } @@ -474,7 +471,7 @@ sub _print_langdef { my $codeformat = "| %-16s | %-${max_length}s |\n"; for my $keyword (qw(given when then )) { printf $codeformat, $keyword . ' (code)', - readable_keywords( $langdef->{$keyword}, \&keyword_to_subname ); + readable_keywords( $langdef->{$keyword}, \&keyword_to_subname ); } exit; diff --git a/lib/TAP/Parser/SourceHandler/Feature.pm b/lib/TAP/Parser/SourceHandler/Feature.pm index 137a216..a1423f7 100644 --- a/lib/TAP/Parser/SourceHandler/Feature.pm +++ b/lib/TAP/Parser/SourceHandler/Feature.pm @@ -6,7 +6,6 @@ use warnings; use Path::Class qw/file/; use base 'TAP::Parser::SourceHandler'; -use TAP::Parser::Iterator::Process; use App::pherkin; @@ -20,6 +19,8 @@ TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); sub can_handle { my ( $class, $source ) = @_; + #use Data::Printer; p $source; + if ( $source->meta->{'is_file'} && $source->meta->{'file'}->{'basename'} =~ m/\.feature$/ ) { @@ -70,6 +71,11 @@ sub make_iterator { $tb->output($output_fh); my $it = TAP::Parser::Iterator::Stream->new($input_fh); + fork and return $it; + + warn "Executing $source from $$"; + sleep 5; + my $harness = Test::BDD::Cucumber::Harness::TestBuilder->new( { fail_skip => 1, @@ -93,7 +99,7 @@ sub make_iterator { $pherkin->_run_tests( $executor, $feature ); - return $it; + exit; } 1; From dbe41e1196a50fbc4bbc0ac080890c3257ad0bb3 Mon Sep 17 00:00:00 2001 From: Erik Huelsmann Date: Sat, 10 Sep 2016 01:43:49 +0200 Subject: [PATCH 05/10] 'do' seems to require an absolute path in order to make @definitions stick --- lib/Test/BDD/Cucumber/StepFile.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Test/BDD/Cucumber/StepFile.pm b/lib/Test/BDD/Cucumber/StepFile.pm index d0f37f2..b00dc3c 100644 --- a/lib/Test/BDD/Cucumber/StepFile.pm +++ b/lib/Test/BDD/Cucumber/StepFile.pm @@ -9,6 +9,7 @@ Test::BDD::Cucumber::StepFile - Functions for creating and loading Step Definiti use strict; use warnings; use Carp qw/croak/; +use File::Spec qw/rel2abs/; use Test::BDD::Cucumber::I18n qw(languages langdef keyword_to_subname); require Exporter; @@ -145,7 +146,7 @@ sub load { my ( $class, $filename ) = @_; { local @definitions; - do $filename; + do File::Spec->rel2abs($filename); die "Step file [$filename] failed to load: $@" if $@; return @definitions; } From 4a5cf9a7d939651877927568ac50002ccccefce2 Mon Sep 17 00:00:00 2001 From: Erik Huelsmann Date: Sat, 10 Sep 2016 17:04:21 +0200 Subject: [PATCH 06/10] Explicitly closing handles unblocks my 'prove' command --- lib/TAP/Parser/SourceHandler/Feature.pm | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/TAP/Parser/SourceHandler/Feature.pm b/lib/TAP/Parser/SourceHandler/Feature.pm index a08417e..285a9f0 100644 --- a/lib/TAP/Parser/SourceHandler/Feature.pm +++ b/lib/TAP/Parser/SourceHandler/Feature.pm @@ -7,6 +7,8 @@ use Path::Class qw/file/; use base 'TAP::Parser::SourceHandler'; +use TAP::Parser::Iterator::Stream; + use App::pherkin; use Test::BDD::Cucumber::Loader; @@ -70,9 +72,13 @@ sub make_iterator { my $tb = Test::Builder->create(); $tb->output($output_fh); - my $it = TAP::Parser::Iterator::Stream->new($input_fh); - fork and return $it; + my $pid = fork; + if ($pid) { + close $output_fh; + return TAP::Parser::Iterator::Stream->new($input_fh); + } + close $input_fh; my $harness = Test::BDD::Cucumber::Harness::TestBuilder->new( { fail_skip => 1, _tb_instance => $tb, @@ -95,6 +101,7 @@ sub make_iterator { $pherkin->_run_tests( $executor, $feature ); + close $output_fh; exit; } From 8aaf4997914ca330c71bdfbd17f57692fe1d3a4a Mon Sep 17 00:00:00 2001 From: Erik Huelsmann Date: Sat, 10 Sep 2016 17:05:00 +0200 Subject: [PATCH 07/10] Document why we need rel2abs() --- lib/Test/BDD/Cucumber/StepFile.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Test/BDD/Cucumber/StepFile.pm b/lib/Test/BDD/Cucumber/StepFile.pm index b00dc3c..665fc94 100644 --- a/lib/Test/BDD/Cucumber/StepFile.pm +++ b/lib/Test/BDD/Cucumber/StepFile.pm @@ -146,6 +146,8 @@ sub load { my ( $class, $filename ) = @_; { local @definitions; + + # Debian Jessie with security patches requires an absolute path do File::Spec->rel2abs($filename); die "Step file [$filename] failed to load: $@" if $@; return @definitions; From c6c259c98bfd4fa764852015ce32cbc8ea211d48 Mon Sep 17 00:00:00 2001 From: Peter Sergeant Date: Sun, 11 Sep 2016 16:50:21 +0700 Subject: [PATCH 08/10] No sleep or warn needed :-/ --- lib/TAP/Parser/SourceHandler/Feature.pm | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/TAP/Parser/SourceHandler/Feature.pm b/lib/TAP/Parser/SourceHandler/Feature.pm index a1423f7..a08417e 100644 --- a/lib/TAP/Parser/SourceHandler/Feature.pm +++ b/lib/TAP/Parser/SourceHandler/Feature.pm @@ -73,10 +73,6 @@ sub make_iterator { my $it = TAP::Parser::Iterator::Stream->new($input_fh); fork and return $it; - warn "Executing $source from $$"; - sleep 5; - - my $harness = Test::BDD::Cucumber::Harness::TestBuilder->new( { fail_skip => 1, _tb_instance => $tb, From 563200bd0ab2d4bb624db5cd05b58a46ef008c76 Mon Sep 17 00:00:00 2001 From: Erik Huelsmann Date: Tue, 31 Jan 2017 00:15:01 +0100 Subject: [PATCH 09/10] * Add documentation for the TAP integration and the side-effects on hooks --- lib/Test/BDD/Cucumber/Extension.pm | 12 +++- lib/Test/BDD/Cucumber/Manual/Architecture.pod | 5 ++ lib/Test/BDD/Cucumber/Manual/Integration.pod | 62 +++++++++++++++++-- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/lib/Test/BDD/Cucumber/Extension.pm b/lib/Test/BDD/Cucumber/Extension.pm index e9a36b5..05b2b3d 100644 --- a/lib/Test/BDD/Cucumber/Extension.pm +++ b/lib/Test/BDD/Cucumber/Extension.pm @@ -41,15 +41,23 @@ sub step_directories { return []; } =head2 pre_execute() -Invoked by App::pherkin before executing any features. This callback +Invoked by C before executing any features. This callback allows generic extension setup. Reports errors by calling croak(). +Note: When the C plugin for C + is used, there are no guarantees at this point that this hook is called + exactly once (or even just once per feature directory). + =head2 post_execute() -Invoked by App::pherkin after executing all features. This callback +Invoked by C after executing all features. This callback allows generic extension teardown and cleanup. Reports errors by calling croak(). +Note: When the C plugin for C + is used, there are no guarantees at this point that this hook is called + at all (be it per feature directory or per C run). + =cut sub pre_execute { return; } diff --git a/lib/Test/BDD/Cucumber/Manual/Architecture.pod b/lib/Test/BDD/Cucumber/Manual/Architecture.pod index 9babcc6..0da0dab 100644 --- a/lib/Test/BDD/Cucumber/Manual/Architecture.pod +++ b/lib/Test/BDD/Cucumber/Manual/Architecture.pod @@ -51,6 +51,11 @@ configuration from the pherkin.yaml configuration file, which works similar to L. +Note: when using extensions in combination with the +C plugin for C, there is no +guarantee that the C and C hooks execute +exactly once or even execute at all. +This is a current limitation to be lifted in a future release. =head1 AUTHOR diff --git a/lib/Test/BDD/Cucumber/Manual/Integration.pod b/lib/Test/BDD/Cucumber/Manual/Integration.pod index bb4626a..070ec37 100644 --- a/lib/Test/BDD/Cucumber/Manual/Integration.pod +++ b/lib/Test/BDD/Cucumber/Manual/Integration.pod @@ -2,7 +2,7 @@ package Test::BDD::Cucumber::Manual::Integration; =head1 NAME -Test::BDD::Cucumber::Manual::Integration - Integrating with Test::Builder +Test::BDD::Cucumber::Manual::Integration - Test suite integration options =head1 DESCRIPTION @@ -10,10 +10,24 @@ How to use Test::BDD::Cucumber in your test suite =head1 OVERVIEW -You may well want your Cucumber tests to be executed as part of your standard -test-suite. Luckily, this is SUPER easy. +Test::BDD::Cucumber offers two options to integrate your tests with your +test framework: -=head1 WELL-COMMENTED EXAMPLE + 1. Creation of a .t file which fires off your selected .feature files + (Test::Builder integration) + 2. Integration with C which will run your .feature + files as it does .t files + +The benefits from using the latter approach is that all C's advanced +features like parallel testing, randomized order, C<--state>ful runs, etc +are also available without efforts of the test developer. + + +=head1 Test::Builder integration -- a documented example + +The code below needs to be stored in a C<.t> file in the C or C +directory. When done that way, the tests are integrated into C +as generated from C after C. #!perl @@ -32,17 +46,53 @@ test-suite. Luckily, this is SUPER easy. # The features are returned in @features, and the executor is created with the # step definitions loaded. my ( $executor, @features ) = Test::BDD::Cucumber::Loader->load( - 't/cucumber_core_features/' ); + 't/cucumber_core_features/' ); # Create a Harness to execute against. TestBuilder harness prints TAP my $harness = Test::BDD::Cucumber::Harness::TestBuilder->new({}); # For each feature found, execute it, using the Harness to print results $executor->execute( $_, $harness ) for @features; - + # Shutdown gracefully $harness->shutdown(); + +=head1 prove integration + +With Test::BDD::Cucumber installed in the Perl search path (PERL5LIB) +comes the possibility to run the .feature files with a C command +directly, by specifying + + $ prove -r + --source Feature + --feature-option extensions=.feature + --feature-option tags=~@wip + --ext=.feature + t/ + +This command registers a C plugin named C associated with +the C<.feature> extension. Additionally, it passes a tag filter to exclude +@wip tagged features and scenarios from being run. + +When executed, the command searches the C directory recursively for files +with the C<.feature> extension. For each directory holding at least one +C<.feature> file, the step files are loaded from the C +subdirectory. + +The command above will find and run I C<.feature> files. When you want +to run your regular C<.t> files as well as Test::BDD::Cucumber's +C<.feature> files, run the following command: + + $ prove -r --source Perl --ext=.t + --source Feature + --feature-option extensions=.feature + --feature-option tags=~@wip + --ext=.feature + t/ + + + =cut 1; From bad1a179ced83ff86a9d6ca9b277455aacbd1d7b Mon Sep 17 00:00:00 2001 From: Erik Huelsmann Date: Mon, 6 Feb 2017 08:43:20 +0100 Subject: [PATCH 10/10] * Wrap call to 'note()' in local Test::Builder --- lib/Test/BDD/Cucumber/Harness/TestBuilder.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm b/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm index acd4377..122bae4 100644 --- a/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm +++ b/lib/Test/BDD/Cucumber/Harness/TestBuilder.pm @@ -110,7 +110,7 @@ sub _note_step_data { if ( ref( $step->data ) eq 'ARRAY' ) { for (@step_data) { - note( $di . $_ ); + $self->_tb->note( $di . $_ ); } } else { $self->_tb->note( $di . '"""' );