diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..efb27011 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,41 @@ +language: perl +notifications: + on_success: never + on_failure: always +# irc: "irc.perl.org#makemaker" + email: false +sudo: false +perl: + - "blead" + - "5.6.2" + - "5.8.1" + - "5.8.5" + - "5.8.7" + - "5.8.8" + - "5.8.9" + - "5.10.0" + - "5.10.1" + - "5.12.0" + - "5.12" + - "5.14.0" + - "5.14" + - "5.16.0" + - "5.16" + - "5.18.0" + - "5.18" + - "5.20" +matrix: + allow_failures: + - perl: "blead" + - perl: "5.6.2" +before_install: + - git clone git://github.com/haarg/perl-travis-helper ~/perl-travis-helper + - source ~/perl-travis-helper/init + - build-perl + - perl -V +install: + - true +script: + - perl Makefile.PL + - make test + - make disttest diff --git a/MANIFEST b/MANIFEST index f046872b..e9f0611c 100644 --- a/MANIFEST +++ b/MANIFEST @@ -37,6 +37,8 @@ lib/TAP/Parser/Grammar.pm lib/TAP/Parser/Iterator.pm lib/TAP/Parser/Iterator/Array.pm lib/TAP/Parser/Iterator/Process.pm +lib/TAP/Parser/Iterator/Process/Unix.pm +lib/TAP/Parser/Iterator/Process/Windows.pm lib/TAP/Parser/Iterator/Stream.pm lib/TAP/Parser/IteratorFactory.pm lib/TAP/Parser/Multiplexer.pm diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..d5b71e76 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,15 @@ +version: 1.0.{build} +skip_tags: true +clone_depth: 1 +init: + - git config --global core.autocrlf input +# Mingw and Cygwin now builtin: http://www.appveyor.com/updates/2015/05/30 +#os: MinGW + +install: + - perl -V + - C:\MinGW\bin\mingw32-make -v + - echo %PATH% +build_script: +#do not let gmake find sh.exe (usually part of git for windows) in %PATH% +- set PATH=C:\windows\system32;C:\Perl\site\bin;C:\Perl\bin;C:\windows;C:\MinGW\bin; && set ACTIVEPERL_CONFIG_DISABLE=1 && perl Makefile.PL MAKE=gmake && mingw32-make test diff --git a/lib/TAP/Formatter/Base.pm b/lib/TAP/Formatter/Base.pm index a5a78d1c..522a2ce2 100644 --- a/lib/TAP/Formatter/Base.pm +++ b/lib/TAP/Formatter/Base.pm @@ -396,9 +396,7 @@ sub _summary_test_header { } sub _output { - my $self = shift; - - print { $self->stdout } @_; + print { shift->stdout } @_; } sub _failure_output { diff --git a/lib/TAP/Formatter/Console/Session.pm b/lib/TAP/Formatter/Console/Session.pm index 672a0834..f760680f 100644 --- a/lib/TAP/Formatter/Console/Session.pm +++ b/lib/TAP/Formatter/Console/Session.pm @@ -128,10 +128,8 @@ sub _closures { my $planned = $parser->tests_planned || '?'; $plan = "/$planned "; } - $output = $formatter->_get_output_method($parser); if ( $show_count and $is_test ) { - my $number = $result->number; my $now = CORE::time; # Print status roughly once per second. @@ -139,6 +137,8 @@ sub _closures { # $last_status_printed starting with the value 0, which $now # will never be. (Unless someone sets their clock to 1970) if ( $last_status_printed != $now ) { + my $number = $result->number; + $output = $formatter->_get_output_method($parser); $formatter->$output("\r$pretty$number$plan"); $last_status_printed = $now; } diff --git a/lib/TAP/Harness.pm b/lib/TAP/Harness.pm index d9ede6b9..72f3d522 100644 --- a/lib/TAP/Harness.pm +++ b/lib/TAP/Harness.pm @@ -2,7 +2,6 @@ package TAP::Harness; use strict; use warnings; -use Carp; use File::Spec; use File::Path; @@ -87,7 +86,7 @@ BEGIN { trap => sub { shift; shift }, ); - for my $method ( sort keys %VALIDATION_FOR ) { + for my $method ( keys %VALIDATION_FOR ) { no strict 'refs'; if ( $method eq 'lib' || $method eq 'switches' ) { *{$method} = sub { @@ -432,7 +431,7 @@ Any keys for which the value is C will be ignored. $self->SUPER::_initialize( $arg_for, \@legal_callback ); my %arg_for = %$arg_for; # force a shallow copy - for my $name ( sort keys %VALIDATION_FOR ) { + for my $name ( keys %VALIDATION_FOR ) { my $property = delete $arg_for{$name}; if ( defined $property ) { my $validate = $VALIDATION_FOR{$name}; @@ -475,8 +474,8 @@ Any keys for which the value is C will be ignored. ); } - if ( my @props = sort keys %arg_for ) { - $self->_croak("Unknown arguments to TAP::Harness::new (@props)"); + if ( my @props = keys %arg_for ) { + $self->_croak('Unknown arguments to TAP::Harness::new ('.join(' ',sort @props).')'); } return $self; diff --git a/lib/TAP/Object.pm b/lib/TAP/Object.pm index 84dfe88c..b845d31b 100644 --- a/lib/TAP/Object.pm +++ b/lib/TAP/Object.pm @@ -50,9 +50,7 @@ L method. Returns a new object. =cut sub new { - my $class = shift; - my $self = bless {}, $class; - return $self->_initialize(@_); + return bless({}, shift)->_initialize(@_); } =head2 Instance Methods @@ -84,7 +82,7 @@ May also be called as a I method. =cut sub _croak { - my $proto = shift; + shift; require Carp; Carp::croak(@_); return; @@ -103,7 +101,7 @@ May also be called as a I method. =cut sub _confess { - my $proto = shift; + shift; require Carp; Carp::confess(@_); return; diff --git a/lib/TAP/Parser.pm b/lib/TAP/Parser.pm index 4b6f9d88..5f6a2a23 100644 --- a/lib/TAP/Parser.pm +++ b/lib/TAP/Parser.pm @@ -15,8 +15,6 @@ use TAP::Parser::SourceHandler::File (); use TAP::Parser::SourceHandler::RawTAP (); use TAP::Parser::SourceHandler::Handle (); -use Carp qw( confess ); - use base 'TAP::Base'; =encoding utf8 @@ -932,7 +930,7 @@ sub pragma { return $self->{pragma}->{$pragma} unless @_; - if ( my $state = shift ) { + if ( shift ) { $self->{pragma}->{$pragma} = 1; } else { @@ -1038,8 +1036,7 @@ failed, any TODO tests unexpectedly succeeded, or any parse errors occurred. sub has_problems { my $self = shift; return - $self->failed - || $self->parse_errors + $self->{has_problems} || ( !$self->ignore_exit && ( $self->wait || $self->exit ) ); } @@ -1145,6 +1142,7 @@ sub parse_errors { @{ shift->{parse_errors} } } sub _add_error { my ( $self, $error ) = @_; push @{ $self->{parse_errors} } => $error; + $self->{has_problems} = 1; return $self; } @@ -1239,8 +1237,10 @@ sub _make_state_table { push @{ $self->{skipped} } => $number if $test->has_skip; - push @{ $self->{ $test->is_ok ? 'passed' : 'failed' } } => - $number; + push @{ $self->{ $test->is_ok ? + 'passed' + : (($self->{has_problems} = 1), 'failed') } + } => $number; push @{ $self->{ $test->is_actual_ok @@ -1340,9 +1340,9 @@ sub _make_state_table { my $st = { %state_globals, %{ $states{$name} } }; # Add defaults - for my $next ( sort keys %{$st} ) { + for my $next ( keys %$st ) { if ( my $default = $state_defaults{$next} ) { - for my $def ( sort keys %{$default} ) { + for my $def ( keys %$default ) { $st->{$next}->{$def} ||= $default->{$def}; } } @@ -1408,7 +1408,7 @@ sub _iter { } } else { - confess("Unhandled token type: $type\n"); + $self->_confess("Unhandled token type: $type\n"); } } return $token; @@ -1520,12 +1520,13 @@ sub _finish { $self->is_good_plan(0) unless defined $self->is_good_plan; unless ( $self->parse_errors ) { + my $tests_run = $self->tests_run; # Optimise storage where possible - if ( $self->tests_run == @{$self->{passed}} ) { - $self->{passed} = $self->tests_run; + if ( $tests_run == @{$self->{passed}} ) { + $self->{passed} = $tests_run; } - if ( $self->tests_run == @{$self->{actual_passed}} ) { - $self->{actual_passed} = $self->tests_run; + if ( $tests_run == @{$self->{actual_passed}} ) { + $self->{actual_passed} = $tests_run; } } diff --git a/lib/TAP/Parser/Aggregator.pm b/lib/TAP/Parser/Aggregator.pm index 57452450..5c359cea 100644 --- a/lib/TAP/Parser/Aggregator.pm +++ b/lib/TAP/Parser/Aggregator.pm @@ -198,8 +198,7 @@ Among other times it records the start time for the test run. =cut sub start { - my $self = shift; - $self->{start_time} = Benchmark->new; + shift->{start_time} = Benchmark->new; } =head3 C @@ -209,8 +208,7 @@ Call C immediately after adding all test results to the aggregator. =cut sub stop { - my $self = shift; - $self->{end_time} = Benchmark->new; + shift->{end_time} = Benchmark->new; } =head3 C @@ -224,10 +222,8 @@ afterwards. sub elapsed { my $self = shift; - - require Carp; - Carp::croak - q{Can't call elapsed without first calling start and then stop} + $self->_croak( + q{Can't call elapsed without first calling start and then stop}) unless defined $self->{start_time} && defined $self->{end_time}; return timediff( $self->{end_time}, $self->{start_time} ); } diff --git a/lib/TAP/Parser/Iterator.pm b/lib/TAP/Parser/Iterator.pm index 114aba6b..b336f549 100644 --- a/lib/TAP/Parser/Iterator.pm +++ b/lib/TAP/Parser/Iterator.pm @@ -61,6 +61,8 @@ Iterate raw input without applying any fixes for quirky input syntax. =cut +if ( $^O eq 'VMS' ) { + eval <<'END' ; sub next { my $self = shift; my $line = $self->next_raw; @@ -75,6 +77,11 @@ sub next { return $line; } +END +} +else { + eval 'sub next { shift->next_raw(@_) }'; +} sub next_raw { require Carp; @@ -125,17 +132,8 @@ Return the C status for this iterator. =cut -sub wait { - require Carp; - my $msg = Carp::longmess('abstract method called directly!'); - $_[0]->_croak($msg); -} - -sub exit { - require Carp; - my $msg = Carp::longmess('abstract method called directly!'); - $_[0]->_croak($msg); -} +#can not call abstract base method, next_raw is a fatal stub +*exit = *wait = *next_raw; 1; diff --git a/lib/TAP/Parser/Iterator/Process.pm b/lib/TAP/Parser/Iterator/Process.pm index 923de9aa..55befd9c 100644 --- a/lib/TAP/Parser/Iterator/Process.pm +++ b/lib/TAP/Parser/Iterator/Process.pm @@ -4,11 +4,10 @@ use strict; use warnings; use Config; -use IO::Handle; use base 'TAP::Parser::Iterator'; -my $IS_WIN32 = ( $^O =~ /^(MS)?Win32$/ ); +use constant IS_WIN32 => !!( $^O =~ /^(MS)?Win32$/ ); =head1 NAME @@ -22,6 +21,10 @@ Version 3.35 our $VERSION = '3.35'; +our $can_fork; + +$can_fork = !! $Config{d_fork} unless defined $can_fork; + =head1 SYNOPSIS use TAP::Parser::Iterator::Process; @@ -77,22 +80,21 @@ Get the exit status for this iterator's process. =cut { - - no warnings 'uninitialized'; - # get around a catch22 in the test suite that causes failures on Win32: - local $SIG{__DIE__} = undef; - eval { require POSIX; &POSIX::WEXITSTATUS(0) }; - if ($@) { - *_wait2exit = sub { $_[1] >> 8 }; + my $class; + if(!IS_WIN32 || (eval { require TAP::Parser::Iterator::Process::Windows }, $@)) { + require TAP::Parser::Iterator::Process::Unix; + $class = 'TAP::Parser::Iterator::Process::Unix'; + } else { + $class = 'TAP::Parser::Iterator::Process::Windows'; } - else { - *_wait2exit = sub { POSIX::WEXITSTATUS( $_[1] ) } + sub new { + shift; + return $class->SUPER::new(@_); } } sub _use_open3 { - my $self = shift; - return unless $Config{d_fork} || $IS_WIN32; + return unless $can_fork || IS_WIN32; for my $module (qw( IPC::Open3 IO::Select )) { eval "use $module"; return if $@; @@ -111,244 +113,22 @@ sub _use_open3 { } } -# new() implementation supplied by TAP::Object - -sub _initialize { - my ( $self, $args ) = @_; - - my @command = @{ delete $args->{command} || [] } - or die "Must supply a command to execute"; - - $self->{command} = [@command]; - - # Private. Used to frig with chunk size during testing. - my $chunk_size = delete $args->{_chunk_size} || 65536; - - my $merge = delete $args->{merge}; - my ( $pid, $err, $sel ); - - if ( my $setup = delete $args->{setup} ) { - $setup->(@command); - } - - my $out = IO::Handle->new; - - if ( $self->_use_open3 ) { - - # HOTPATCH {{{ - my $xclose = \&IPC::Open3::xclose; - no warnings; - local *IPC::Open3::xclose = sub { - my $fh = shift; - no strict 'refs'; - return if ( fileno($fh) == fileno(STDIN) ); - $xclose->($fh); - }; - - # }}} - - if ($IS_WIN32) { - $err = $merge ? '' : '>&STDERR'; - eval { - $pid = open3( - '<&STDIN', $out, $merge ? '' : $err, - @command - ); - }; - die "Could not execute (@command): $@" if $@; - if ( $] >= 5.006 ) { - binmode($out, ":crlf"); - } - } - else { - $err = $merge ? '' : IO::Handle->new; - eval { $pid = open3( '<&STDIN', $out, $err, @command ); }; - die "Could not execute (@command): $@" if $@; - $sel = $merge ? undef : IO::Select->new( $out, $err ); - } - } - else { - $err = ''; - my $command - = join( ' ', map { $_ =~ /\s/ ? qq{"$_"} : $_ } @command ); - open( $out, "$command|" ) - or die "Could not execute ($command): $!"; - } - - $self->{out} = $out; - $self->{err} = $err; - $self->{sel} = $sel; - $self->{pid} = $pid; - $self->{exit} = undef; - $self->{chunk_size} = $chunk_size; - - if ( my $teardown = delete $args->{teardown} ) { - $self->{teardown} = sub { - $teardown->(@command); - }; - } - - return $self; -} - =head3 C Upgrade the input stream to handle UTF8. =cut -sub handle_unicode { - my $self = shift; - - if ( $self->{sel} ) { - if ( _get_unicode() ) { - - # Make sure our iterator has been constructed and... - my $next = $self->{_next} ||= $self->_next; - - # ...wrap it to do UTF8 casting - $self->{_next} = sub { - my $line = $next->(); - return decode_utf8($line) if defined $line; - return; - }; - } - } - else { - if ( $] >= 5.008 ) { - eval 'binmode($self->{out}, ":utf8")'; - } - } - -} - ############################################################################## sub wait { shift->{wait} } sub exit { shift->{exit} } -sub _next { - my $self = shift; - - if ( my $out = $self->{out} ) { - if ( my $sel = $self->{sel} ) { - my $err = $self->{err}; - my @buf = (); - my $partial = ''; # Partial line - my $chunk_size = $self->{chunk_size}; - return sub { - return shift @buf if @buf; - - READ: - while ( my @ready = $sel->can_read ) { - for my $fh (@ready) { - my $got = sysread $fh, my ($chunk), $chunk_size; - - if ( $got == 0 ) { - $sel->remove($fh); - } - elsif ( $fh == $err ) { - print STDERR $chunk; # echo STDERR - } - else { - $chunk = $partial . $chunk; - $partial = ''; - - # Make sure we have a complete line - unless ( substr( $chunk, -1, 1 ) eq "\n" ) { - my $nl = rindex $chunk, "\n"; - if ( $nl == -1 ) { - $partial = $chunk; - redo READ; - } - else { - $partial = substr( $chunk, $nl + 1 ); - $chunk = substr( $chunk, 0, $nl ); - } - } - - push @buf, split /\n/, $chunk; - return shift @buf if @buf; - } - } - } - - # Return partial last line - if ( length $partial ) { - my $last = $partial; - $partial = ''; - return $last; - } - - $self->_finish; - return; - }; - } - else { - return sub { - if ( defined( my $line = <$out> ) ) { - chomp $line; - return $line; - } - $self->_finish; - return; - }; - } - } - else { - return sub { - $self->_finish; - return; - }; - } -} - sub next_raw { my $self = shift; return ( $self->{_next} ||= $self->_next )->(); } -sub _finish { - my $self = shift; - - my $status = $?; - - # Avoid circular refs - $self->{_next} = sub {return} - if $] >= 5.006; - - # If we have a subprocess we need to wait for it to terminate - if ( defined $self->{pid} ) { - if ( $self->{pid} == waitpid( $self->{pid}, 0 ) ) { - $status = $?; - } - } - - ( delete $self->{out} )->close if $self->{out}; - - # If we have an IO::Select we also have an error handle to close. - if ( $self->{sel} ) { - ( delete $self->{err} )->close; - delete $self->{sel}; - } - else { - $status = $?; - } - - # Sometimes we get -1 on Windows. Presumably that means status not - # available. - $status = 0 if $IS_WIN32 && $status == -1; - - $self->{wait} = $status; - $self->{exit} = $self->_wait2exit($status); - - if ( my $teardown = $self->{teardown} ) { - $teardown->(); - } - - return $self; -} - =head3 C Return a list of filehandles that may be used upstream in a select() @@ -357,11 +137,6 @@ handle based should return an empty list. =cut -sub get_select_handles { - my $self = shift; - return grep $_, ( $self->{out}, $self->{err} ); -} - 1; =head1 ATTRIBUTION diff --git a/lib/TAP/Parser/Iterator/Process/Unix.pm b/lib/TAP/Parser/Iterator/Process/Unix.pm new file mode 100644 index 00000000..ca4bf660 --- /dev/null +++ b/lib/TAP/Parser/Iterator/Process/Unix.pm @@ -0,0 +1,290 @@ +package TAP::Parser::Iterator::Process::Unix; + +use strict; +use warnings; + +use IO::Handle; + +use base 'TAP::Parser::Iterator::Process'; + +use constant IS_WIN32 => !!( $^O =~ /^(MS)?Win32$/ ); + +=head1 NAME + +TAP::Parser::Iterator::Process::Unix - Unix-y process-based TAP sources + +=head1 VERSION + +Version 3.35 + +=cut + +our $VERSION = '3.35'; + +=head1 DESCRIPTION + +This class implements a process iterator for Unix type OSes using only core +modules. It is also used as a fallback on Windows if the Windows process +iterator can't be used. This module shouldn't be used directly, create +L objects instead which picks Windows or Unix. + +=cut + +{ + + no warnings 'uninitialized'; + # get around a catch22 in the test suite that causes failures on Win32: + local $SIG{__DIE__} = undef; + eval { require POSIX; &POSIX::WEXITSTATUS(0) }; + if ($@) { + *_wait2exit = sub { $_[1] >> 8 }; + } + else { + *_wait2exit = sub { POSIX::WEXITSTATUS( $_[1] ) } + } +} + +sub _initialize { + my ( $self, $args ) = @_; + + my @command = @{ delete $args->{command} || [] } + or die "Must supply a command to execute"; + + $self->{command} = [@command]; + + # Private. Used to frig with chunk size during testing. + my $chunk_size = delete $args->{_chunk_size} || 65536; + + my $merge = delete $args->{merge}; + my ( $pid, $err, $sel ); + + if ( my $setup = delete $args->{setup} ) { + $setup->(@command); + } + + my $out = IO::Handle->new; + + if ( $self->_use_open3 ) { + + # HOTPATCH {{{ + my $xclose = \&IPC::Open3::xclose; + no warnings; + local *IPC::Open3::xclose = sub { + my $fh = shift; + no strict 'refs'; + return if ( fileno($fh) == fileno(STDIN) ); + $xclose->($fh); + }; + + # }}} + + if (IS_WIN32) { + $err = $merge ? '' : '>&STDERR'; + eval { + $pid = IPC::Open3::open3( + '<&STDIN', $out, $merge ? '' : $err, + @command + ); + }; + die "Could not execute (@command): $@" if $@; + if ( $] >= 5.006 ) { + binmode($out, ":crlf"); + } + } + else { + $err = $merge ? '' : IO::Handle->new; + eval { $pid = IPC::Open3::open3( '<&STDIN', $out, $err, @command ); }; + die "Could not execute (@command): $@" if $@; + $sel = $merge ? undef : IO::Select->new( $out, $err ); + } + } + else { + $err = ''; + my $command + = join( ' ', map { $_ =~ /\s/ ? qq{"$_"} : $_ } @command ); + open( $out, "$command|" ) + or die "Could not execute ($command): $!"; + } + + $self->{out} = $out; + $self->{err} = $err; + $self->{sel} = $sel; + $self->{pid} = $pid; + $self->{exit} = undef; + $self->{chunk_size} = $chunk_size; + + if ( my $teardown = delete $args->{teardown} ) { + $self->{teardown} = sub { + $teardown->(@command); + }; + } + + return $self; +} + +sub handle_unicode { + my $self = shift; + + if ( $self->{sel} ) { + package TAP::Parser::Iterator::Process; + if ( _get_unicode() ) { + + # Make sure our iterator has been constructed and... + my $next = $self->{_next} ||= $self->_next; + + # ...wrap it to do UTF8 casting + $self->{_next} = sub { + my $line = $next->(); + return decode_utf8($line) if defined $line; + return; + }; + } + package TAP::Parser::Iterator::Process::Unix; + } + else { + if ( $] >= 5.008 ) { + eval 'binmode($self->{out}, ":utf8")'; + } + } + +} + +############################################################################## + +sub _next { + my $self = shift; + + if ( my $out = $self->{out} ) { + if ( my $sel = $self->{sel} ) { + my $err = $self->{err}; + my @buf = (); + my $partial = ''; # Partial line + my $chunk_size = $self->{chunk_size}; + return sub { + return shift @buf if @buf; + + READ: + while ( my @ready = $sel->can_read ) { + for my $fh (@ready) { + my $got = sysread $fh, my ($chunk), $chunk_size; + + if ( $got == 0 ) { + $sel->remove($fh); + } + elsif ( $fh == $err ) { + print STDERR $chunk; # echo STDERR + } + else { + $chunk = $partial . $chunk; + $partial = ''; + + # Make sure we have a complete line + unless ( substr( $chunk, -1, 1 ) eq "\n" ) { + my $nl = rindex $chunk, "\n"; + if ( $nl == -1 ) { + $partial = $chunk; + redo READ; + } + else { + $partial = substr( $chunk, $nl + 1 ); + $chunk = substr( $chunk, 0, $nl ); + } + } + + push @buf, split /\n/, $chunk; + return shift @buf if @buf; + } + } + } + + # Return partial last line + if ( length $partial ) { + my $last = $partial; + $partial = ''; + return $last; + } + + $self->_finish; + return; + }; + } + else { + return sub { + if ( defined( my $line = <$out> ) ) { + chomp $line; + return $line; + } + $self->_finish; + return; + }; + } + } + else { + return sub { + $self->_finish; + return; + }; + } +} + +sub _finish { + my $self = shift; + + my $status = $?; + + # Avoid circular refs + $self->{_next} = sub {return} + if $] >= 5.006; + + # If we have a subprocess we need to wait for it to terminate + if ( defined $self->{pid} ) { + if ( $self->{pid} == waitpid( $self->{pid}, 0 ) ) { + $status = $?; + } + } + + ( delete $self->{out} )->close if $self->{out}; + + # If we have an IO::Select we also have an error handle to close. + if ( $self->{sel} ) { + ( delete $self->{err} )->close; + delete $self->{sel}; + } + else { + $status = $?; + } + + # Sometimes we get -1 on Windows. Presumably that means status not + # available. + $status = 0 if IS_WIN32 && $status == -1; + + $self->{wait} = $status; + $self->{exit} = $self->_wait2exit($status); + + if ( my $teardown = $self->{teardown} ) { + $teardown->(); + } + + return $self; +} + +sub get_select_handles { + my $self = shift; + return grep $_, ( $self->{out}, $self->{err} ); +} + +1; + +=head1 ATTRIBUTION + +This is the original implementation of L. + +=head1 SEE ALSO + +L, +L, +L, +L, + +=cut + diff --git a/lib/TAP/Parser/Iterator/Process/Windows.pm b/lib/TAP/Parser/Iterator/Process/Windows.pm new file mode 100644 index 00000000..e83726dc --- /dev/null +++ b/lib/TAP/Parser/Iterator/Process/Windows.pm @@ -0,0 +1,187 @@ +package TAP::Parser::Iterator::Process::Windows; + +use strict; +use warnings; + +use Win32::APipe; + +use base 'TAP::Parser::Iterator::Process'; + +=head1 NAME + +TAP::Parser::Iterator::Process::Windows - Windows process-based TAP sources + +=head1 VERSION + +Version 3.35 + +=cut + +our $VERSION = '3.35'; + +BEGIN { # making accessors + __PACKAGE__->mk_methods('opaque'); +} + +=head1 DESCRIPTION + +This class implements an experimental process iterator for Windows. It +requires L XS module to be installed. This module shouldn't be +used directly, create L objects instead which +picks Windows or Unix. + +=cut + +sub _initialize { + my ( $self, $args ) = @_; + + my @command = @{ delete $args->{command} || [] } + or die "Must supply a command to execute"; + + $self->{command} = [@command]; + $self->{buf} = []; + $self->{partial} = ''; + + # Private. Used to frig with chunk size during testing. + my $chunk_size = delete $args->{_chunk_size} || 65536; + + my $merge = delete $args->{merge}; + + if ( my $setup = delete $args->{setup} ) { + $setup->(@command); + } + + my $command = join( ' ', map { $_ =~ /\s/ ? qq{"$_"} : $_ } @command ); + my $err = Win32::APipe::run($command, $self, $merge, $self->{pid}); + die "Could not execute ($command): Win32 Err $err" if $err != 0; #0=ERROR_SUCCESS + + $self->{exit} = undef; + $self->{chunk_size} = $chunk_size; + + if ( my $teardown = delete $args->{teardown} ) { + $self->{teardown} = sub { + $teardown->(@command); + }; + } + + return $self; +} + +sub handle_unicode { + my $self = shift; + + package TAP::Parser::Iterator::Process; + + if ( _get_unicode() ) { + + # Make sure our iterator has been constructed and... + my $next = $self->{_next} ||= $self->_next; + + # ...wrap it to do UTF8 casting + $self->{_next} = sub { + my $line = $next->(); + return decode_utf8($line) if defined $line; + return; + }; + } + package TAP::Parser::Iterator::Process::Windows; +} + +sub _next { + my $self = shift; + + return sub { + #non-blocking quick return of a line + return shift @{$self->{buf}} if @{$self->{buf}}; + #sanity test against hang forever reading on a finished stream + return undef if $self->{done}; + if ($self->{disable_read}) { + warn "Multiplexered Iterator::Process::Windows read attempted, " + ."Multiplexer should be pushing data into the iterator"; + exit 1; #no catching exceptions, this is a bug if executes + } + my $opaque; + + #a ->next() on any particular Iterator::Process object, pumps + #events/buffers/lines into ALL Iterator::Process objects until a line is + #availble for the particular Iterator::Process object + + #if we get random other Iterator::Process objects, we have to process their + #data buffers and queue it into @{$self->{buf}}, this loop turns the async + #IO with async reads completing in a random order, into sync IO for the + #caller, when the caller moves onto other Iterator::Process objects and + #calls ->next() on those other Iterator::Process objects, ->next() will be + #non-blocking since @{$self->{buf}} has elements + do { + my $chunk; + READ: + $opaque = Win32::APipe::next($chunk); + goto READ unless $opaque->add_chunk($chunk); + } while ($opaque != $self); + + return shift @{$self->{buf}} + }; +} + +# returns true if atleast 1 line was added to @{$self->{buf}}, if false you +# must add another chunk (do another async read) since there wasn't enough data +# or the data happens to not have a newline in it upto this point + +sub add_chunk { + my ($self, $chunk) = @_; + if(ref($chunk)) { + $self->{done} = 1; + # Avoid circular refs + $self->{_next} = sub {return} + if $] >= 5.006; + #$block->{ExitCode} = 0xc0000005; + #always return the raw Win32 error code, even tho on unix this will be 0 if a "signal" ended the process + $self->{exit} = $chunk->{ExitCode}; #this might a negative Win32 STATUS_* code, like STATUS_ACCESS_VIOLATION + #do we set coredump bit and when? + $self->{wait} = Win32::APipe::status_to_sig($chunk->{ExitCode}); + + if(! $self->{wait}) { #process probably naturally exited + $self->{wait} = $self->{exit} << 8; #signal zero is implicit here + } + if ( my $teardown = $self->{teardown} ) { + $teardown->(); + } + #this might be undef or a partial line before the proc abnormally exited + push @{$self->{buf}}, (length $self->{partial} ? $self->{partial} : undef); + } else { + $chunk = $self->{partial} . $chunk; + $self->{partial} = ''; + + # Make sure we have a complete line + unless ( substr( $chunk, -1, 2 ) eq "\r\n" ) { + my $nl = rindex $chunk, "\r\n"; + if ( $nl == -1 ) { + $self->{partial} = $chunk; + return 0; + } + else { + $self->{partial} = substr( $chunk, $nl + 2 ); + $chunk = substr( $chunk, 0, $nl ); + } + } + push @{$self->{buf}}, split /\r\n/, $chunk; + } + return 1; +} + +sub get_select_handles { + return; +} + +1; + +=head1 SEE ALSO + +L, +L, +L, +L, +L, + +=cut + diff --git a/lib/TAP/Parser/IteratorFactory.pm b/lib/TAP/Parser/IteratorFactory.pm index d4e29ffa..56830d34 100644 --- a/lib/TAP/Parser/IteratorFactory.pm +++ b/lib/TAP/Parser/IteratorFactory.pm @@ -3,7 +3,6 @@ package TAP::Parser::IteratorFactory; use strict; use warnings; -use Carp qw( confess ); use File::Basename qw( fileparse ); use base 'TAP::Object'; @@ -71,7 +70,7 @@ List of handlers that have been registered. sub register_handler { my ( $class, $dclass ) = @_; - confess("$dclass must implement can_handle & make_iterator methods!") + TAP::Object->_confess("$dclass must implement can_handle & make_iterator methods!") unless UNIVERSAL::can( $dclass, 'can_handle' ) && UNIVERSAL::can( $dclass, 'make_iterator' ); @@ -240,7 +239,7 @@ Ties are handled by choosing the first handler. sub detect_source { my ( $self, $source ) = @_; - confess('no raw source ref defined!') unless defined $source->raw; + $self->_confess('no raw source ref defined!') unless defined $source->raw; # find a list of handlers that can handle this source: my %handlers; @@ -258,7 +257,7 @@ sub detect_source { # error: can't detect source my $raw_source_short = substr( ${ $source->raw }, 0, 50 ); - confess("Cannot detect source of '$raw_source_short'!"); + $self->_confess("Cannot detect source of '$raw_source_short'!"); return; } diff --git a/lib/TAP/Parser/Multiplexer.pm b/lib/TAP/Parser/Multiplexer.pm index d002272a..6aca591d 100644 --- a/lib/TAP/Parser/Multiplexer.pm +++ b/lib/TAP/Parser/Multiplexer.pm @@ -61,6 +61,7 @@ sub _initialize { $self->{select} = IO::Select->new; $self->{avid} = []; # Parsers that can't select $self->{count} = 0; + $self->{w32_count} = 0; return $self; } @@ -80,6 +81,7 @@ the next result. sub add { my ( $self, $parser, $stash ) = @_; + my $iterator; if ( SELECT_OK && ( my @handles = $parser->get_select_handles ) ) { my $sel = $self->{select}; @@ -94,6 +96,23 @@ sub add { $self->{count}++; } + elsif (IS_WIN32 + && ($iterator = $parser->_iterator)->isa('TAP::Parser::Iterator::Process::Windows')) { + #By this point the Win32 proc was already started. + #When the parser obj is new()ed, the proc starts. No read events on a + #PP level have occured yet (there is an async read in progress tho) + $iterator->opaque([ $parser, $stash ]); + #Iterator::Process::Windows obj is now push-only, pulls are fatal + #Allowing reads on a async queue multiplexer registered iterater can + #result in some procs getting much more read/data poping off the queue + #than other procs leadings to stalls/blocks in other procs randomly. + #Also anti-hang "you can't next() when there is no work" die sometimes + #occurs if multiplexerd I::P::W obj executes a queue pop in + #I::P::W::next_raw() queue although that is fixable in theory with + #additional global state. So just never have 2 competing event loops. + $iterator->{disable_read} = 1; + $self->{w32_count}++; + } else { push @{ $self->{avid} }, [ $parser, $stash ]; } @@ -110,7 +129,7 @@ when their input is exhausted. sub parsers { my $self = shift; - return $self->{count} + scalar @{ $self->{avid} }; + return $self->{count} + $self->{w32_count} + scalar @{ $self->{avid} }; } sub _iter { @@ -119,6 +138,7 @@ sub _iter { my $sel = $self->{select}; my $avid = $self->{avid}; my @ready = (); + my $iterator; return sub { @@ -129,6 +149,36 @@ sub _iter { shift @$avid unless defined $result; return ( $parser, $stash, $result ); } + if ($self->{w32_count}) { + { + my $chunk; #either a string or a hash ref with end of stream info + #keep pulling TAP lines out of same iterator until iterator's + #buffer exhausted + unless($iterator) { + $iterator = Win32::APipe::next($chunk); + #not enough data, pretend we never saw it and wait for + #another data block iterator + undef($iterator), redo unless $iterator->add_chunk($chunk); + } + my ( $parser, $stash ) = @{$iterator->opaque}; + #sanity test/assert, maybe remove one day + die 'atleast 1 line should already exist, the $parser->next() is non-blocking' + if @{$iterator->{buf}} == 0 && !$iterator->{done}; + my $result = $parser->next; + + #nuke circ ref if end of stream + unless (defined $result){ + $iterator->opaque(undef); + $self->{w32_count}--; + undef($iterator); + } + #iterator is out of data, but not finished, needs another read + elsif (@{$iterator->{buf}} == 0) { + undef($iterator); + } + return ( $parser, $stash, $result ); + } + } unless (@ready) { return unless $sel->count; diff --git a/lib/TAP/Parser/Scheduler.pm b/lib/TAP/Parser/Scheduler.pm index ed3ef513..118a24e1 100644 --- a/lib/TAP/Parser/Scheduler.pm +++ b/lib/TAP/Parser/Scheduler.pm @@ -3,7 +3,6 @@ package TAP::Parser::Scheduler; use strict; use warnings; -use Carp; use TAP::Parser::Scheduler::Job; use TAP::Parser::Scheduler::Spinner; @@ -129,13 +128,13 @@ We implement our own glob-style pattern matching. Here are the patterns it suppo sub new { my $class = shift; - croak "Need a number of key, value pairs" if @_ % 2; + TAP::Object->_croak("Need a number of key, value pairs") if @_ % 2; my %args = @_; - my $tests = delete $args{tests} || croak "Need a 'tests' argument"; + my $tests = delete $args{tests} || TAP::Object->_croak("Need a 'tests' argument"); my $rules = delete $args{rules} || { par => '**' }; - croak "Unknown arg(s): ", join ', ', sort keys %args + TAP::Object->_croak("Unknown arg(s): ", join ', ', sort keys %args) if keys %args; # Turn any simple names into a name, description pair. TODO: Maybe @@ -176,11 +175,11 @@ sub _set_rules { sub _rule_clause { my ( $self, $rule, $tests ) = @_; - croak 'Rule clause must be a hash' + $self->_croak('Rule clause must be a hash') unless 'HASH' eq ref $rule; my @type = keys %$rule; - croak 'Rule clause must have exactly one key' + $self->_croak('Rule clause must have exactly one key') unless @type == 1; my %handlers = ( @@ -191,7 +190,7 @@ sub _rule_clause { ); my $handler = $handlers{ $type[0] } - || croak 'Unknown scheduler type: ', $type[0]; + || $self->_croak('Unknown scheduler type: ', $type[0]); my $val = $rule->{ $type[0] }; return $handler->( diff --git a/lib/TAP/Parser/Scheduler/Job.pm b/lib/TAP/Parser/Scheduler/Job.pm index ae0b59b9..014106c2 100644 --- a/lib/TAP/Parser/Scheduler/Job.pm +++ b/lib/TAP/Parser/Scheduler/Job.pm @@ -2,7 +2,6 @@ package TAP::Parser::Scheduler::Job; use strict; use warnings; -use Carp; =head1 NAME diff --git a/lib/TAP/Parser/Scheduler/Spinner.pm b/lib/TAP/Parser/Scheduler/Spinner.pm index a739164a..00a17f4b 100644 --- a/lib/TAP/Parser/Scheduler/Spinner.pm +++ b/lib/TAP/Parser/Scheduler/Spinner.pm @@ -2,7 +2,6 @@ package TAP::Parser::Scheduler::Spinner; use strict; use warnings; -use Carp; =head1 NAME diff --git a/t/000-load.t b/t/000-load.t index 07ebce2e..dab0b602 100644 --- a/t/000-load.t +++ b/t/000-load.t @@ -66,8 +66,10 @@ sub lib_matcher { sub filter_lib { my $matcher = lib_matcher(LIBS); + #TAP::Parser::Iterator::Process::Windows + #is loadable only if it usable on a particular system, so dont test it return map { s{$matcher}{}; $_ } - grep {m{$matcher.+?\.pm$}} sort @_; + grep {m{$matcher(?!TAP/Parser/Iterator/Process/Windows).+?\.pm$}} sort @_; } sub mod_to_file { diff --git a/t/compat/test-harness-compat.t b/t/compat/test-harness-compat.t index e4d369ac..c31b2ba0 100644 --- a/t/compat/test-harness-compat.t +++ b/t/compat/test-harness-compat.t @@ -142,14 +142,17 @@ my $TEST_DIR = 't/sample-tests'; 'name' => "$TEST_DIR/too_many", 'wstat' => '1024' }, - "$TEST_DIR/vms_nit" => { - 'canon' => 1, - 'estat' => '', - 'failed' => 1, - 'max' => 2, - 'name' => "$TEST_DIR/vms_nit", - 'wstat' => '' - } + ( $^O eq 'VMS' ? + ("$TEST_DIR/vms_nit" => { + 'canon' => 1, + 'estat' => '', + 'failed' => 1, + 'max' => 2, + 'name' => "$TEST_DIR/vms_nit", + 'wstat' => '' + }) + : () + ) }, 'todo' => { "$TEST_DIR/todo_inline" => { @@ -162,12 +165,12 @@ my $TEST_DIR = 't/sample-tests'; } }, 'totals' => { - 'bad' => 12, + 'bad' => (12-($^O eq 'VMS' ? 0 : 1)), 'bonus' => 1, 'files' => 27, - 'good' => 15, + 'good' => (15+($^O eq 'VMS' ? 0 : 1)), 'max' => 76, - 'ok' => 78, + 'ok' => (78+($^O eq 'VMS' ? 0 : 1)), 'skipped' => 2, 'sub_skipped' => 2, 'tests' => 27, @@ -727,31 +730,35 @@ my $TEST_DIR = 't/sample-tests'; 'todo' => 0 } }, - 'vms_nit' => { - 'failed' => { - "$TEST_DIR/vms_nit" => { - 'canon' => 1, - 'estat' => '', - 'failed' => 1, - 'max' => 2, - 'name' => "$TEST_DIR/vms_nit", - 'wstat' => '' + ( $^O eq 'VMS' ? + ('vms_nit' => { + 'failed' => { + "$TEST_DIR/vms_nit" => { + 'canon' => 1, + 'estat' => '', + 'failed' => 1, + 'max' => 2, + 'name' => "$TEST_DIR/vms_nit", + 'wstat' => '' + } + }, + 'skip_if' => sub { $^O ne 'VMS' }, + 'todo' => {}, + 'totals' => { + 'bad' => 1, + 'bonus' => 0, + 'files' => 1, + 'good' => 0, + 'max' => 2, + 'ok' => 1, + 'skipped' => 0, + 'sub_skipped' => 0, + 'tests' => 1, + 'todo' => 0 } - }, - 'todo' => {}, - 'totals' => { - 'bad' => 1, - 'bonus' => 0, - 'files' => 1, - 'good' => 0, - 'max' => 2, - 'ok' => 1, - 'skipped' => 0, - 'sub_skipped' => 0, - 'tests' => 1, - 'todo' => 0 - } - } + }) + : () + ) }; my $num_tests = ( keys %$results ) * $PER_LOOP; diff --git a/t/iterators.t b/t/iterators.t index 57e20401..649accd0 100644 --- a/t/iterators.t +++ b/t/iterators.t @@ -9,6 +9,7 @@ use Test::More tests => 76; use File::Spec; use TAP::Parser; use TAP::Parser::Iterator::Array; +use IO::Handle; #Harness doesn't always load IO::Handle anymore use Config; sub array_ref_from { @@ -79,7 +80,7 @@ my @schedule = ( ); sub _can_open3 { - return $Config{d_fork}; + return $TAP::Parser::Iterator::Process::can_fork; } for my $test (@schedule) { @@ -155,16 +156,20 @@ for my $test (@schedule) { { + my $iterator; # coverage test for VMS case - my $iterator = make_iterator( - [ 'not ', - 'ok 1 - I hate VMS', - ] - ); + SKIP : { + skip('Not VMS', 1) if $^O ne 'VMS'; + $iterator = make_iterator( + [ 'not ', + 'ok 1 - I hate VMS', + ] + ); - is $iterator->next, 'not ok 1 - I hate VMS', - 'coverage of VMS line-splitting case'; + is $iterator->next, 'not ok 1 - I hate VMS', + 'coverage of VMS line-splitting case'; + } # coverage test for VMS case - nothing after 'not' diff --git a/t/lib/NoFork.pm b/t/lib/NoFork.pm index 0225e962..365d8c4a 100644 --- a/t/lib/NoFork.pm +++ b/t/lib/NoFork.pm @@ -3,8 +3,8 @@ package NoFork; BEGIN { *CORE::GLOBAL::fork = sub { die "you should not fork" }; } -use Config; -tied(%Config)->{d_fork} = 0; # blatant lie + +$TAP::Parser::Iterator::Process::can_fork = 0; =begin TEST diff --git a/t/multiplexer.t b/t/multiplexer.t index 2e55b12d..475c53b8 100644 --- a/t/multiplexer.t +++ b/t/multiplexer.t @@ -133,7 +133,7 @@ my @schedule = ( 'ok 5', ] ] - } 1 .. 3 + } 1 .. 6 ); }, } @@ -153,6 +153,9 @@ for my $test (@schedule) { # use Data::Dumper; # diag Dumper( { stash => $stash, result => $result } ); + my @err = $parser->parse_errors(); + ok(!@err, "$name: Parser has no parse errors"); + diag @err if @err; if ( defined $result ) { my $expect = ( shift @$stash ) || ' OOPS '; my $got = $result->raw; diff --git a/t/nofork.t b/t/nofork.t index a062792e..e8643e22 100755 --- a/t/nofork.t +++ b/t/nofork.t @@ -10,9 +10,9 @@ BEGIN { use strict; use warnings; -use Config; +use TAP::Parser::Iterator::Process; use Test::More ( - $Config{d_fork} + $TAP::Parser::Iterator::Process::can_fork ? 'no_plan' : ( 'skip_all' => 'your system already has no fork' ) ); diff --git a/t/proverun.t b/t/proverun.t index f8aa07aa..a8b2160d 100644 --- a/t/proverun.t +++ b/t/proverun.t @@ -107,8 +107,8 @@ package main; }; # Patch TAP::Formatter::Console; - my $orig_output = \&TAP::Formatter::Console::_output; - *TAP::Formatter::Console::_output = sub { + my $orig_output = \&TAP::Formatter::Base::_output; + *TAP::Formatter::Base::_output = sub { # push @call_log, [ '_output', @_ ]; }; diff --git a/t/regression.t b/t/regression.t index 7e1388e3..bd7c15d8 100644 --- a/t/regression.t +++ b/t/regression.t @@ -3196,7 +3196,7 @@ my %HANDLER_FOR = ( FALSE, sub { no warnings; !shift }, ); -my $can_open3 = ( $Config{d_fork} || $IsWin32 ) ? 1 : 0; +my $can_open3 = ( $TAP::Parser::Iterator::Process::can_fork || $IsWin32 ) ? 1 : 0; for my $hide_fork ( 0 .. $can_open3 ) { if ($hide_fork) {