diff options
author | Norbert Preining <preining@logic.at> | 2010-05-12 16:54:37 +0000 |
---|---|---|
committer | Norbert Preining <preining@logic.at> | 2010-05-12 16:54:37 +0000 |
commit | 661c41a09e39a182865e0b51e34cc995a0dc96e8 (patch) | |
tree | 2f79bb1406e22fdcb2587be8ffda6c0c609d7932 /Master/tlpkg/tlperl/lib/TAP | |
parent | b645030efc22e13c2498a1522083634ab91b2de1 (diff) |
move tlperl.straw to tlperl
git-svn-id: svn://tug.org/texlive/trunk@18210 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/tlpkg/tlperl/lib/TAP')
46 files changed, 11719 insertions, 0 deletions
diff --git a/Master/tlpkg/tlperl/lib/TAP/Base.pm b/Master/tlpkg/tlperl/lib/TAP/Base.pm new file mode 100755 index 00000000000..62a2c372118 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Base.pm @@ -0,0 +1,129 @@ +package TAP::Base; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object; + +@ISA = qw(TAP::Object); + +=head1 NAME + +TAP::Base - Base class that provides common functionality to L<TAP::Parser> +and L<TAP::Harness> + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +use constant GOT_TIME_HIRES => do { + eval 'use Time::HiRes qw(time);'; + $@ ? 0 : 1; +}; + +=head1 SYNOPSIS + + package TAP::Whatever; + + use TAP::Base; + + use vars qw($VERSION @ISA); + @ISA = qw(TAP::Base); + + # ... later ... + + my $thing = TAP::Whatever->new(); + + $thing->callback( event => sub { + # do something interesting + } ); + +=head1 DESCRIPTION + +C<TAP::Base> provides callback management. + +=head1 METHODS + +=head2 Class Methods + +=cut + +sub _initialize { + my ( $self, $arg_for, $ok_callback ) = @_; + + my %ok_map = map { $_ => 1 } @$ok_callback; + + $self->{ok_callbacks} = \%ok_map; + + if ( my $cb = delete $arg_for->{callbacks} ) { + while ( my ( $event, $callback ) = each %$cb ) { + $self->callback( $event, $callback ); + } + } + + return $self; +} + +=head3 C<callback> + +Install a callback for a named event. + +=cut + +sub callback { + my ( $self, $event, $callback ) = @_; + + my %ok_map = %{ $self->{ok_callbacks} }; + + $self->_croak('No callbacks may be installed') + unless %ok_map; + + $self->_croak( "Callback $event is not supported. Valid callbacks are " + . join( ', ', sort keys %ok_map ) ) + unless exists $ok_map{$event}; + + push @{ $self->{code_for}{$event} }, $callback; + + return; +} + +sub _has_callbacks { + my $self = shift; + return keys %{ $self->{code_for} } != 0; +} + +sub _callback_for { + my ( $self, $event ) = @_; + return $self->{code_for}{$event}; +} + +sub _make_callback { + my $self = shift; + my $event = shift; + + my $cb = $self->_callback_for($event); + return unless defined $cb; + return map { $_->(@_) } @$cb; +} + +=head3 C<get_time> + +Return the current time using Time::HiRes if available. + +=cut + +sub get_time { return time() } + +=head3 C<time_is_hires> + +Return true if the time returned by get_time is high resolution (i.e. if Time::HiRes is available). + +=cut + +sub time_is_hires { return GOT_TIME_HIRES } + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/Base.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/Base.pm new file mode 100755 index 00000000000..43ac59621e9 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/Base.pm @@ -0,0 +1,449 @@ +package TAP::Formatter::Base; + +use strict; +use TAP::Base (); +use POSIX qw(strftime); + +use vars qw($VERSION @ISA); + +my $MAX_ERRORS = 5; +my %VALIDATION_FOR; + +BEGIN { + @ISA = qw(TAP::Base); + + %VALIDATION_FOR = ( + directives => sub { shift; shift }, + verbosity => sub { shift; shift }, + normalize => sub { shift; shift }, + timer => sub { shift; shift }, + failures => sub { shift; shift }, + comments => sub { shift; shift }, + errors => sub { shift; shift }, + color => sub { shift; shift }, + jobs => sub { shift; shift }, + show_count => sub { shift; shift }, + stdout => sub { + my ( $self, $ref ) = @_; + $self->_croak("option 'stdout' needs a filehandle") + unless ( ref $ref || '' ) eq 'GLOB' + or eval { $ref->can('print') }; + return $ref; + }, + ); + + my @getter_setters = qw( + _longest + _printed_summary_header + _colorizer + ); + + __PACKAGE__->mk_methods( @getter_setters, keys %VALIDATION_FOR ); +} + +=head1 NAME + +TAP::Formatter::Base - Base class for harness output delegates + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This provides console orientated output formatting for TAP::Harness. + +=head1 SYNOPSIS + + use TAP::Formatter::Console; + my $harness = TAP::Formatter::Console->new( \%args ); + +=cut + +sub _initialize { + my ( $self, $arg_for ) = @_; + $arg_for ||= {}; + + $self->SUPER::_initialize($arg_for); + my %arg_for = %$arg_for; # force a shallow copy + + $self->verbosity(0); + + for my $name ( keys %VALIDATION_FOR ) { + my $property = delete $arg_for{$name}; + if ( defined $property ) { + my $validate = $VALIDATION_FOR{$name}; + $self->$name( $self->$validate($property) ); + } + } + + if ( my @props = keys %arg_for ) { + $self->_croak( + "Unknown arguments to " . __PACKAGE__ . "::new (@props)" ); + } + + $self->stdout( \*STDOUT ) unless $self->stdout; + + if ( $self->color ) { + require TAP::Formatter::Color; + $self->_colorizer( TAP::Formatter::Color->new ); + } + + return $self; +} + +sub verbose { shift->verbosity >= 1 } +sub quiet { shift->verbosity <= -1 } +sub really_quiet { shift->verbosity <= -2 } +sub silent { shift->verbosity <= -3 } + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my %args = ( + verbose => 1, + ) + my $harness = TAP::Formatter::Console->new( \%args ); + +The constructor returns a new C<TAP::Formatter::Console> object. If +a L<TAP::Harness> is created with no C<formatter> a +C<TAP::Formatter::Console> is automatically created. If any of the +following options were given to TAP::Harness->new they well be passed to +this constructor which accepts an optional hashref whose allowed keys are: + +=over 4 + +=item * C<verbosity> + +Set the verbosity level. + +=item * C<verbose> + +Printing individual test results to STDOUT. + +=item * C<timer> + +Append run time for each test to output. Uses L<Time::HiRes> if available. + +=item * C<failures> + +Show test failures (this is a no-op if C<verbose> is selected). + +=item * C<comments> + +Show test comments (this is a no-op if C<verbose> is selected). + +=item * C<quiet> + +Suppressing some test output (mostly failures while tests are running). + +=item * C<really_quiet> + +Suppressing everything but the tests summary. + +=item * C<silent> + +Suppressing all output. + +=item * C<errors> + +If parse errors are found in the TAP output, a note of this will be made +in the summary report. To see all of the parse errors, set this argument to +true: + + errors => 1 + +=item * C<directives> + +If set to a true value, only test results with directives will be displayed. +This overrides other settings such as C<verbose>, C<failures>, or C<comments>. + +=item * C<stdout> + +A filehandle for catching standard output. + +=item * C<color> + +If defined specifies whether color output is desired. If C<color> is not +defined it will default to color output if color support is available on +the current platform and output is not being redirected. + +=item * C<jobs> + +The number of concurrent jobs this formatter will handle. + +=item * C<show_count> + +Boolean value. If false, disables the C<X/Y> test count which shows up while +tests are running. + +=back + +Any keys for which the value is C<undef> will be ignored. + +=cut + +# new supplied by TAP::Base + +=head3 C<prepare> + +Called by Test::Harness before any test output is generated. + +This is an advisory and may not be called in the case where tests are +being supplied to Test::Harness by an iterator. + +=cut + +sub prepare { + my ( $self, @tests ) = @_; + + my $longest = 0; + + for my $test (@tests) { + $longest = length $test if length $test > $longest; + } + + $self->_longest($longest); +} + +sub _format_now { strftime "[%H:%M:%S]", localtime } + +sub _format_name { + my ( $self, $test ) = @_; + my $name = $test; + my $periods = '.' x ( $self->_longest + 2 - length $test ); + $periods = " $periods "; + + if ( $self->timer ) { + my $stamp = $self->_format_now(); + return "$stamp $name$periods"; + } + else { + return "$name$periods"; + } + +} + +=head3 C<open_test> + +Called to create a new test session. A test session looks like this: + + my $session = $formatter->open_test( $test, $parser ); + while ( defined( my $result = $parser->next ) ) { + $session->result($result); + exit 1 if $result->is_bailout; + } + $session->close_test; + +=cut + +sub open_test { + die "Unimplemented."; +} + +sub _output_success { + my ( $self, $msg ) = @_; + $self->_output($msg); +} + +=head3 C<summary> + + $harness->summary( $aggregate ); + +C<summary> prints the summary report after all tests are run. The argument is +an aggregate. + +=cut + +sub summary { + my ( $self, $aggregate ) = @_; + + return if $self->silent; + + my @t = $aggregate->descriptions; + my $tests = \@t; + + my $runtime = $aggregate->elapsed_timestr; + + my $total = $aggregate->total; + my $passed = $aggregate->passed; + + if ( $self->timer ) { + $self->_output( $self->_format_now(), "\n" ); + } + + # TODO: Check this condition still works when all subtests pass but + # the exit status is nonzero + + if ( $aggregate->all_passed ) { + $self->_output_success("All tests successful.\n"); + } + + # ~TODO option where $aggregate->skipped generates reports + if ( $total != $passed or $aggregate->has_problems ) { + $self->_output("\nTest Summary Report"); + $self->_output("\n-------------------\n"); + for my $test (@$tests) { + $self->_printed_summary_header(0); + my ($parser) = $aggregate->parsers($test); + $self->_output_summary_failure( + 'failed', + [ ' Failed test: ', ' Failed tests: ' ], + $test, $parser + ); + $self->_output_summary_failure( + 'todo_passed', + " TODO passed: ", $test, $parser + ); + + # ~TODO this cannot be the default + #$self->_output_summary_failure( 'skipped', " Tests skipped: " ); + + if ( my $exit = $parser->exit ) { + $self->_summary_test_header( $test, $parser ); + $self->_failure_output(" Non-zero exit status: $exit\n"); + } + elsif ( my $wait = $parser->wait ) { + $self->_summary_test_header( $test, $parser ); + $self->_failure_output(" Non-zero wait status: $wait\n"); + } + + if ( my @errors = $parser->parse_errors ) { + my $explain; + if ( @errors > $MAX_ERRORS && !$self->errors ) { + $explain + = "Displayed the first $MAX_ERRORS of " + . scalar(@errors) + . " TAP syntax errors.\n" + . "Re-run prove with the -p option to see them all.\n"; + splice @errors, $MAX_ERRORS; + } + $self->_summary_test_header( $test, $parser ); + $self->_failure_output( + sprintf " Parse errors: %s\n", + shift @errors + ); + for my $error (@errors) { + my $spaces = ' ' x 16; + $self->_failure_output("$spaces$error\n"); + } + $self->_failure_output($explain) if $explain; + } + } + } + my $files = @$tests; + $self->_output("Files=$files, Tests=$total, $runtime\n"); + my $status = $aggregate->get_status; + $self->_output("Result: $status\n"); +} + +sub _output_summary_failure { + my ( $self, $method, $name, $test, $parser ) = @_; + + # ugly hack. Must rethink this :( + my $output = $method eq 'failed' ? '_failure_output' : '_output'; + + if ( my @r = $parser->$method() ) { + $self->_summary_test_header( $test, $parser ); + my ( $singular, $plural ) + = 'ARRAY' eq ref $name ? @$name : ( $name, $name ); + $self->$output( @r == 1 ? $singular : $plural ); + my @results = $self->_balanced_range( 40, @r ); + $self->$output( sprintf "%s\n" => shift @results ); + my $spaces = ' ' x 16; + while (@results) { + $self->$output( sprintf "$spaces%s\n" => shift @results ); + } + } +} + +sub _summary_test_header { + my ( $self, $test, $parser ) = @_; + return if $self->_printed_summary_header; + my $spaces = ' ' x ( $self->_longest - length $test ); + $spaces = ' ' unless $spaces; + my $output = $self->_get_output_method($parser); + $self->$output( + sprintf "$test$spaces(Wstat: %d Tests: %d Failed: %d)\n", + $parser->wait, $parser->tests_run, scalar $parser->failed + ); + $self->_printed_summary_header(1); +} + +sub _output { + my $self = shift; + + print { $self->stdout } @_; +} + +sub _failure_output { + my $self = shift; + + $self->_output(@_); +} + +sub _balanced_range { + my ( $self, $limit, @range ) = @_; + @range = $self->_range(@range); + my $line = ""; + my @lines; + my $curr = 0; + while (@range) { + if ( $curr < $limit ) { + my $range = ( shift @range ) . ", "; + $line .= $range; + $curr += length $range; + } + elsif (@range) { + $line =~ s/, $//; + push @lines => $line; + $line = ''; + $curr = 0; + } + } + if ($line) { + $line =~ s/, $//; + push @lines => $line; + } + return @lines; +} + +sub _range { + my ( $self, @numbers ) = @_; + + # shouldn't be needed, but subclasses might call this + @numbers = sort { $a <=> $b } @numbers; + my ( $min, @range ); + + for my $i ( 0 .. $#numbers ) { + my $num = $numbers[$i]; + my $next = $numbers[ $i + 1 ]; + if ( defined $next && $next == $num + 1 ) { + if ( !defined $min ) { + $min = $num; + } + } + elsif ( defined $min ) { + push @range => "$min-$num"; + undef $min; + } + else { + push @range => $num; + } + } + return @range; +} + +sub _get_output_method { + my ( $self, $parser ) = @_; + return $parser->has_problems ? '_failure_output' : '_output'; +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/Color.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/Color.pm new file mode 100755 index 00000000000..75fcb8c73e2 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/Color.pm @@ -0,0 +1,148 @@ +package TAP::Formatter::Color; + +use strict; +use vars qw($VERSION @ISA); + +use constant IS_WIN32 => ( $^O =~ /^(MS)?Win32$/ ); + +@ISA = qw(TAP::Object); + +my $NO_COLOR; + +BEGIN { + $NO_COLOR = 0; + + if (IS_WIN32) { + eval 'use Win32::Console'; + if ($@) { + $NO_COLOR = $@; + } + else { + my $console = Win32::Console->new( STD_OUTPUT_HANDLE() ); + + # eval here because we might not know about these variables + my $fg = eval '$FG_LIGHTGRAY'; + my $bg = eval '$BG_BLACK'; + + *set_color = sub { + my ( $self, $output, $color ) = @_; + + my $var; + if ( $color eq 'reset' ) { + $fg = eval '$FG_LIGHTGRAY'; + $bg = eval '$BG_BLACK'; + } + elsif ( $color =~ /^on_(.+)$/ ) { + $bg = eval '$BG_' . uc($1); + } + else { + $fg = eval '$FG_' . uc($color); + } + + # In case of colors that aren't defined + $self->set_color('reset') + unless defined $bg && defined $fg; + + $console->Attr( $bg | $fg ); + }; + } + } + else { + eval 'use Term::ANSIColor'; + if ($@) { + $NO_COLOR = $@; + } + else { + *set_color = sub { + my ( $self, $output, $color ) = @_; + $output->( color($color) ); + }; + } + } + + if ($NO_COLOR) { + *set_color = sub { }; + } +} + +=head1 NAME + +TAP::Formatter::Color - Run Perl test scripts with color + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +Note that this harness is I<experimental>. You may not like the colors I've +chosen and I haven't yet provided an easy way to override them. + +This test harness is the same as L<TAP::Harness>, but test results are output +in color. Passing tests are printed in green. Failing tests are in red. +Skipped tests are blue on a white background and TODO tests are printed in +white. + +If L<Term::ANSIColor> cannot be found (or L<Win32::Console> if running +under Windows) tests will be run without color. + +=head1 SYNOPSIS + + use TAP::Formatter::Color; + my $harness = TAP::Formatter::Color->new( \%args ); + $harness->runtests(@tests); + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +The constructor returns a new C<TAP::Formatter::Color> object. If +L<Term::ANSIColor> is not installed, returns undef. + +=cut + +# new() implementation supplied by TAP::Object + +sub _initialize { + my $self = shift; + + if ($NO_COLOR) { + + # shorten that message a bit + ( my $error = $NO_COLOR ) =~ s/ in \@INC .*//s; + warn "Note: Cannot run tests in color: $error\n"; + return; # abort object construction + } + + return $self; +} + +############################################################################## + +=head3 C<can_color> + + Test::Formatter::Color->can_color() + +Returns a boolean indicating whether or not this module can actually +generate colored output. This will be false if it could not load the +modules needed for the current platform. + +=cut + +sub can_color { + return !$NO_COLOR; +} + +=head3 C<set_color> + +Set the output color. + +=cut + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/Console.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/Console.pm new file mode 100755 index 00000000000..ff6587ef8ac --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/Console.pm @@ -0,0 +1,91 @@ +package TAP::Formatter::Console; + +use strict; +use TAP::Formatter::Base (); +use POSIX qw(strftime); + +use vars qw($VERSION @ISA); + +@ISA = qw(TAP::Formatter::Base); + +=head1 NAME + +TAP::Formatter::Console - Harness output delegate for default console output + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This provides console orientated output formatting for TAP::Harness. + +=head1 SYNOPSIS + + use TAP::Formatter::Console; + my $harness = TAP::Formatter::Console->new( \%args ); + +=head2 C<< open_test >> + +See L<TAP::Formatter::Base> + +=cut + +sub open_test { + my ( $self, $test, $parser ) = @_; + + my $class + = $self->jobs > 1 + ? 'TAP::Formatter::Console::ParallelSession' + : 'TAP::Formatter::Console::Session'; + + eval "require $class"; + $self->_croak($@) if $@; + + my $session = $class->new( + { name => $test, + formatter => $self, + parser => $parser, + show_count => $self->show_count, + } + ); + + $session->header; + + return $session; +} + +# Use _colorizer delegate to set output color. NOP if we have no delegate +sub _set_colors { + my ( $self, @colors ) = @_; + if ( my $colorizer = $self->_colorizer ) { + my $output_func = $self->{_output_func} ||= sub { + $self->_output(@_); + }; + $colorizer->set_color( $output_func, $_ ) for @colors; + } +} + +sub _output_success { + my ( $self, $msg ) = @_; + $self->_set_colors('green'); + $self->_output($msg); + $self->_set_colors('reset'); +} + +sub _failure_output { + my $self = shift; + $self->_set_colors('red'); + my $out = join '', @_; + my $has_newline = chomp $out; + $self->_output($out); + $self->_set_colors('reset'); + $self->_output($/) + if $has_newline; +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/Console/ParallelSession.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/Console/ParallelSession.pm new file mode 100755 index 00000000000..a85a82cc4d9 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/Console/ParallelSession.pm @@ -0,0 +1,202 @@ +package TAP::Formatter::Console::ParallelSession; + +use strict; +use File::Spec; +use File::Path; +use TAP::Formatter::Console::Session; +use Carp; + +use constant WIDTH => 72; # Because Eric says +use vars qw($VERSION @ISA); + +@ISA = qw(TAP::Formatter::Console::Session); + +my %shared; + +sub _initialize { + my ( $self, $arg_for ) = @_; + + $self->SUPER::_initialize($arg_for); + my $formatter = $self->formatter; + + # Horrid bodge. This creates our shared context per harness. Maybe + # TAP::Harness should give us this? + my $context = $shared{$formatter} ||= $self->_create_shared_context; + push @{ $context->{active} }, $self; + + return $self; +} + +sub _create_shared_context { + my $self = shift; + return { + active => [], + tests => 0, + fails => 0, + }; +} + +=head1 NAME + +TAP::Formatter::Console::ParallelSession - Harness output delegate for parallel console output + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This provides console orientated output formatting for L<TAP::Harness> +when run with multiple L<TAP::Harness/jobs>. + +=head1 SYNOPSIS + +=cut + +=head1 METHODS + +=head2 Class Methods + +=head3 C<header> + +Output test preamble + +=cut + +sub header { +} + +sub _clear_ruler { + my $self = shift; + $self->formatter->_output( "\r" . ( ' ' x WIDTH ) . "\r" ); +} + +my $now = 0; +my $start; + +my $trailer = '... )==='; +my $chop_length = WIDTH - length $trailer; + +sub _output_ruler { + my ( $self, $refresh ) = @_; + my $new_now = time; + return if $new_now == $now and !$refresh; + $now = $new_now; + $start ||= $now; + my $formatter = $self->formatter; + return if $formatter->really_quiet; + + my $context = $shared{$formatter}; + + my $ruler = sprintf '===( %7d;%d ', $context->{tests}, $now - $start; + + for my $active ( @{ $context->{active} } ) { + my $parser = $active->parser; + my $tests = $parser->tests_run; + my $planned = $parser->tests_planned || '?'; + + $ruler .= sprintf '%' . length($planned) . "d/$planned ", $tests; + } + chop $ruler; # Remove a trailing space + $ruler .= ')==='; + + if ( length $ruler > WIDTH ) { + $ruler =~ s/(.{$chop_length}).*/$1$trailer/o; + } + else { + $ruler .= '=' x ( WIDTH - length($ruler) ); + } + $formatter->_output("\r$ruler"); +} + +=head3 C<result> + + Called by the harness for each line of TAP it receives . + +=cut + +sub result { + my ( $self, $result ) = @_; + my $formatter = $self->formatter; + + # my $really_quiet = $formatter->really_quiet; + # my $show_count = $self->_should_show_count; + + if ( $result->is_test ) { + my $context = $shared{$formatter}; + $context->{tests}++; + + my $active = $context->{active}; + if ( @$active == 1 ) { + + # There is only one test, so use the serial output format. + return $self->SUPER::result($result); + } + + $self->_output_ruler( $self->parser->tests_run == 1 ); + } + elsif ( $result->is_bailout ) { + $formatter->_failure_output( + "Bailout called. Further testing stopped: " + . $result->explanation + . "\n" ); + } +} + +=head3 C<clear_for_close> + +=cut + +sub clear_for_close { + my $self = shift; + my $formatter = $self->formatter; + return if $formatter->really_quiet; + my $context = $shared{$formatter}; + if ( @{ $context->{active} } == 1 ) { + $self->SUPER::clear_for_close; + } + else { + $self->_clear_ruler; + } +} + +=head3 C<close_test> + +=cut + +sub close_test { + my $self = shift; + my $name = $self->name; + my $parser = $self->parser; + my $formatter = $self->formatter; + my $context = $shared{$formatter}; + + $self->SUPER::close_test; + + my $active = $context->{active}; + + my @pos = grep { $active->[$_]->name eq $name } 0 .. $#$active; + + die "Can't find myself" unless @pos; + splice @$active, $pos[0], 1; + + if ( @$active > 1 ) { + $self->_output_ruler(1); + } + elsif ( @$active == 1 ) { + + # Print out "test/name.t ...." + $active->[0]->SUPER::header; + } + else { + + # $self->formatter->_output("\n"); + delete $shared{$formatter}; + } +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/Console/Session.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/Console/Session.pm new file mode 100755 index 00000000000..d125e3d14d4 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/Console/Session.pm @@ -0,0 +1,219 @@ +package TAP::Formatter::Console::Session; + +use strict; +use TAP::Formatter::Session; + +use vars qw($VERSION @ISA); + +@ISA = qw(TAP::Formatter::Session); + +my @ACCESSOR; + +BEGIN { + my @CLOSURE_BINDING = qw( header result clear_for_close close_test ); + + for my $method (@CLOSURE_BINDING) { + no strict 'refs'; + *$method = sub { + my $self = shift; + return ( $self->{_closures} ||= $self->_closures )->{$method} + ->(@_); + }; + } +} + +=head1 NAME + +TAP::Formatter::Console::Session - Harness output delegate for default console output + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This provides console orientated output formatting for TAP::Harness. + +=cut + +sub _get_output_result { + my $self = shift; + + my @color_map = ( + { test => sub { $_->is_test && !$_->is_ok }, + colors => ['red'], + }, + { test => sub { $_->is_test && $_->has_skip }, + colors => [ + 'white', + 'on_blue' + ], + }, + { test => sub { $_->is_test && $_->has_todo }, + colors => ['yellow'], + }, + ); + + my $formatter = $self->formatter; + my $parser = $self->parser; + + return $formatter->_colorizer + ? sub { + my $result = shift; + for my $col (@color_map) { + local $_ = $result; + if ( $col->{test}->() ) { + $formatter->_set_colors( @{ $col->{colors} } ); + last; + } + } + $formatter->_output( $self->_format_for_output($result) ); + $formatter->_set_colors('reset'); + } + : sub { + $formatter->_output( $self->_format_for_output(shift) ); + }; +} + +sub _closures { + my $self = shift; + + my $parser = $self->parser; + my $formatter = $self->formatter; + my $pretty = $formatter->_format_name( $self->name ); + my $show_count = $self->show_count; + + my $really_quiet = $formatter->really_quiet; + my $quiet = $formatter->quiet; + my $verbose = $formatter->verbose; + my $directives = $formatter->directives; + my $failures = $formatter->failures; + my $comments = $formatter->comments; + + my $output_result = $self->_get_output_result; + + my $output = '_output'; + my $plan = ''; + my $newline_printed = 0; + + my $last_status_printed = 0; + + return { + header => sub { + $formatter->_output($pretty) + unless $really_quiet; + }, + + result => sub { + my $result = shift; + + if ( $result->is_bailout ) { + $formatter->_failure_output( + "Bailout called. Further testing stopped: " + . $result->explanation + . "\n" ); + } + + return if $really_quiet; + + my $is_test = $result->is_test; + + # These are used in close_test - but only if $really_quiet + # is false - so it's safe to only set them here unless that + # relationship changes. + + if ( !$plan ) { + 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. + # We will always get the first number as a side effect of + # $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 ) { + $formatter->$output("\r$pretty$number$plan"); + $last_status_printed = $now; + } + } + + if (!$quiet + && ( $verbose + || ( $is_test && $failures && !$result->is_ok ) + || ( $comments && $result->is_comment ) + || ( $directives && $result->has_directive ) ) + ) + { + unless ($newline_printed) { + $formatter->_output("\n"); + $newline_printed = 1; + } + $output_result->($result); + $formatter->_output("\n"); + } + }, + + clear_for_close => sub { + my $spaces + = ' ' x length( '.' . $pretty . $plan . $parser->tests_run ); + $formatter->$output("\r$spaces"); + }, + + close_test => sub { + if ( $show_count && !$really_quiet ) { + $self->clear_for_close; + $formatter->$output("\r$pretty"); + } + + # Avoid circular references + $self->parser(undef); + $self->{_closures} = {}; + + return if $really_quiet; + + if ( my $skip_all = $parser->skip_all ) { + $formatter->_output("skipped: $skip_all\n"); + } + elsif ( $parser->has_problems ) { + $self->_output_test_failure($parser); + } + else { + my $time_report = ''; + if ( $formatter->timer ) { + my $start_time = $parser->start_time; + my $end_time = $parser->end_time; + if ( defined $start_time and defined $end_time ) { + my $elapsed = $end_time - $start_time; + $time_report + = $self->time_is_hires + ? sprintf( ' %8d ms', $elapsed * 1000 ) + : sprintf( ' %8s s', $elapsed || '<1' ); + } + } + + $formatter->_output("ok$time_report\n"); + } + }, + }; +} + +=head2 C<< clear_for_close >> + +=head2 C<< close_test >> + +=head2 C<< header >> + +=head2 C<< result >> + +=cut + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/File.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/File.pm new file mode 100755 index 00000000000..6cca9f2f56d --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/File.pm @@ -0,0 +1,58 @@ +package TAP::Formatter::File; + +use strict; +use TAP::Formatter::Base (); +use TAP::Formatter::File::Session; +use POSIX qw(strftime); + +use vars qw($VERSION @ISA); + +@ISA = qw(TAP::Formatter::Base); + +=head1 NAME + +TAP::Formatter::File - Harness output delegate for file output + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This provides file orientated output formatting for TAP::Harness. + +=head1 SYNOPSIS + + use TAP::Formatter::File; + my $harness = TAP::Formatter::File->new( \%args ); + +=head2 C<< open_test >> + +See L<TAP::Formatter::base> + +=cut + +sub open_test { + my ( $self, $test, $parser ) = @_; + + my $session = TAP::Formatter::File::Session->new( + { name => $test, + formatter => $self, + parser => $parser, + } + ); + + $session->header; + + return $session; +} + +sub _should_show_count { + return 0; +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/File/Session.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/File/Session.pm new file mode 100755 index 00000000000..93a1aaed269 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/File/Session.pm @@ -0,0 +1,110 @@ +package TAP::Formatter::File::Session; + +use strict; +use TAP::Formatter::Session; + +use vars qw($VERSION @ISA); + +@ISA = qw(TAP::Formatter::Session); + +=head1 NAME + +TAP::Formatter::File::Session - Harness output delegate for file output + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This provides file orientated output formatting for L<TAP::Harness>. +It is particularly important when running with parallel tests, as it +ensures that test results are not interleaved, even when run +verbosely. + +=cut + +=head1 METHODS + +=head2 result + +Stores results for later output, all together. + +=cut + +sub result { + my $self = shift; + my $result = shift; + + my $parser = $self->parser; + my $formatter = $self->formatter; + + if ( $result->is_bailout ) { + $formatter->_failure_output( + "Bailout called. Further testing stopped: " + . $result->explanation + . "\n" ); + return; + } + + if (!$formatter->quiet + && ( $formatter->verbose + || ( $result->is_test && $formatter->failures && !$result->is_ok ) + || ( $formatter->comments && $result->is_comment ) + || ( $result->has_directive && $formatter->directives ) ) + ) + { + $self->{results} .= $self->_format_for_output($result) . "\n"; + } +} + +=head2 close_test + +When the test file finishes, outputs the summary, together. + +=cut + +sub close_test { + my $self = shift; + + # Avoid circular references + $self->parser(undef); + + my $parser = $self->parser; + my $formatter = $self->formatter; + my $pretty = $formatter->_format_name( $self->name ); + + return if $formatter->really_quiet; + if ( my $skip_all = $parser->skip_all ) { + $formatter->_output( $pretty . "skipped: $skip_all\n" ); + } + elsif ( $parser->has_problems ) { + $formatter->_output( + $pretty . ( $self->{results} ? "\n" . $self->{results} : "\n" ) ); + $self->_output_test_failure($parser); + } + else { + my $time_report = ''; + if ( $formatter->timer ) { + my $start_time = $parser->start_time; + my $end_time = $parser->end_time; + if ( defined $start_time and defined $end_time ) { + my $elapsed = $end_time - $start_time; + $time_report + = $self->time_is_hires + ? sprintf( ' %8d ms', $elapsed * 1000 ) + : sprintf( ' %8s s', $elapsed || '<1' ); + } + } + + $formatter->_output( $pretty + . ( $self->{results} ? "\n" . $self->{results} : "" ) + . "ok$time_report\n" ); + } +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Formatter/Session.pm b/Master/tlpkg/tlperl/lib/TAP/Formatter/Session.pm new file mode 100755 index 00000000000..4bff7111522 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Formatter/Session.pm @@ -0,0 +1,183 @@ +package TAP::Formatter::Session; + +use strict; +use TAP::Base; + +use vars qw($VERSION @ISA); + +@ISA = qw(TAP::Base); + +my @ACCESSOR; + +BEGIN { + + @ACCESSOR = qw( name formatter parser show_count ); + + for my $method (@ACCESSOR) { + no strict 'refs'; + *$method = sub { shift->{$method} }; + } +} + +=head1 NAME + +TAP::Formatter::Session - Abstract base class for harness output delegate + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my %args = ( + formatter => $self, + ) + my $harness = TAP::Formatter::Console::Session->new( \%args ); + +The constructor returns a new C<TAP::Formatter::Console::Session> object. + +=over 4 + +=item * C<formatter> + +=item * C<parser> + +=item * C<name> + +=item * C<show_count> + +=back + +=cut + +sub _initialize { + my ( $self, $arg_for ) = @_; + $arg_for ||= {}; + + $self->SUPER::_initialize($arg_for); + my %arg_for = %$arg_for; # force a shallow copy + + for my $name (@ACCESSOR) { + $self->{$name} = delete $arg_for{$name}; + } + + if ( !defined $self->show_count ) { + $self->{show_count} = 1; # defaults to true + } + if ( $self->show_count ) { # but may be a damned lie! + $self->{show_count} = $self->_should_show_count; + } + + if ( my @props = sort keys %arg_for ) { + $self->_croak( + "Unknown arguments to " . __PACKAGE__ . "::new (@props)" ); + } + + return $self; +} + +=head3 C<header> + +Output test preamble + +=head3 C<result> + +Called by the harness for each line of TAP it receives. + +=head3 C<close_test> + +Called to close a test session. + +=head3 C<clear_for_close> + +Called by C<close_test> to clear the line showing test progress, or the parallel +test ruler, prior to printing the final test result. + +=cut + +sub header { } + +sub result { } + +sub close_test { } + +sub clear_for_close { } + +sub _should_show_count { + my $self = shift; + return + !$self->formatter->verbose + && -t $self->formatter->stdout + && !$ENV{HARNESS_NOTTY}; +} + +sub _format_for_output { + my ( $self, $result ) = @_; + return $self->formatter->normalize ? $result->as_string : $result->raw; +} + +sub _output_test_failure { + my ( $self, $parser ) = @_; + my $formatter = $self->formatter; + return if $formatter->really_quiet; + + my $tests_run = $parser->tests_run; + my $tests_planned = $parser->tests_planned; + + my $total + = defined $tests_planned + ? $tests_planned + : $tests_run; + + my $passed = $parser->passed; + + # The total number of fails includes any tests that were planned but + # didn't run + my $failed = $parser->failed + $total - $tests_run; + my $exit = $parser->exit; + + if ( my $exit = $parser->exit ) { + my $wstat = $parser->wait; + my $status = sprintf( "%d (wstat %d, 0x%x)", $exit, $wstat, $wstat ); + $formatter->_failure_output("Dubious, test returned $status\n"); + } + + if ( $failed == 0 ) { + $formatter->_failure_output( + $total + ? "All $total subtests passed " + : 'No subtests run ' + ); + } + else { + $formatter->_failure_output("Failed $failed/$total subtests "); + if ( !$total ) { + $formatter->_failure_output("\nNo tests run!"); + } + } + + if ( my $skipped = $parser->skipped ) { + $passed -= $skipped; + my $test = 'subtest' . ( $skipped != 1 ? 's' : '' ); + $formatter->_output( + "\n\t(less $skipped skipped $test: $passed okay)"); + } + + if ( my $failed = $parser->todo_passed ) { + my $test = $failed > 1 ? 'tests' : 'test'; + $formatter->_output( + "\n\t($failed TODO $test unexpectedly succeeded)"); + } + + $formatter->_output("\n"); +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Harness.pm b/Master/tlpkg/tlperl/lib/TAP/Harness.pm new file mode 100755 index 00000000000..4a8dffd7370 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Harness.pm @@ -0,0 +1,948 @@ +package TAP::Harness; + +use strict; +use Carp; + +use File::Spec; +use File::Path; +use IO::Handle; + +use TAP::Base; + +use vars qw($VERSION @ISA); + +@ISA = qw(TAP::Base); + +=head1 NAME + +TAP::Harness - Run test scripts with statistics + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +$ENV{HARNESS_ACTIVE} = 1; +$ENV{HARNESS_VERSION} = $VERSION; + +END { + + # For VMS. + delete $ENV{HARNESS_ACTIVE}; + delete $ENV{HARNESS_VERSION}; +} + +=head1 DESCRIPTION + +This is a simple test harness which allows tests to be run and results +automatically aggregated and output to STDOUT. + +=head1 SYNOPSIS + + use TAP::Harness; + my $harness = TAP::Harness->new( \%args ); + $harness->runtests(@tests); + +=cut + +my %VALIDATION_FOR; +my @FORMATTER_ARGS; + +sub _error { + my $self = shift; + return $self->{error} unless @_; + $self->{error} = shift; +} + +BEGIN { + + @FORMATTER_ARGS = qw( + directives verbosity timer failures comments errors stdout color + show_count normalize + ); + + %VALIDATION_FOR = ( + lib => sub { + my ( $self, $libs ) = @_; + $libs = [$libs] unless 'ARRAY' eq ref $libs; + + return [ map {"-I$_"} @$libs ]; + }, + switches => sub { shift; shift }, + exec => sub { shift; shift }, + merge => sub { shift; shift }, + aggregator_class => sub { shift; shift }, + formatter_class => sub { shift; shift }, + multiplexer_class => sub { shift; shift }, + parser_class => sub { shift; shift }, + scheduler_class => sub { shift; shift }, + formatter => sub { shift; shift }, + jobs => sub { shift; shift }, + test_args => sub { shift; shift }, + ignore_exit => sub { shift; shift }, + rules => sub { shift; shift }, + sources => sub { shift; shift }, + ); + + for my $method ( sort keys %VALIDATION_FOR ) { + no strict 'refs'; + if ( $method eq 'lib' || $method eq 'switches' ) { + *{$method} = sub { + my $self = shift; + unless (@_) { + $self->{$method} ||= []; + return wantarray + ? @{ $self->{$method} } + : $self->{$method}; + } + $self->_croak("Too many arguments to method '$method'") + if @_ > 1; + my $args = shift; + $args = [$args] unless ref $args; + $self->{$method} = $args; + return $self; + }; + } + else { + *{$method} = sub { + my $self = shift; + return $self->{$method} unless @_; + $self->{$method} = shift; + }; + } + } + + for my $method (@FORMATTER_ARGS) { + no strict 'refs'; + *{$method} = sub { + my $self = shift; + return $self->formatter->$method(@_); + }; + } +} + +############################################################################## + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my %args = ( + verbosity => 1, + lib => [ 'lib', 'blib/lib', 'blib/arch' ], + ) + my $harness = TAP::Harness->new( \%args ); + +The constructor returns a new C<TAP::Harness> object. It accepts an +optional hashref whose allowed keys are: + +=over 4 + +=item * C<verbosity> + +Set the verbosity level: + + 1 verbose Print individual test results to STDOUT. + 0 normal + -1 quiet Suppress some test output (mostly failures + while tests are running). + -2 really quiet Suppress everything but the tests summary. + -3 silent Suppress everything. + +=item * C<timer> + +Append run time for each test to output. Uses L<Time::HiRes> if +available. + +=item * C<failures> + +Show test failures (this is a no-op if C<verbose> is selected). + +=item * C<comments> + +Show test comments (this is a no-op if C<verbose> is selected). + +=item * C<show_count> + +Update the running test count during testing. + +=item * C<normalize> + +Set to a true value to normalize the TAP that is emitted in verbose modes. + +=item * C<lib> + +Accepts a scalar value or array ref of scalar values indicating which +paths to allowed libraries should be included if Perl tests are +executed. Naturally, this only makes sense in the context of tests +written in Perl. + +=item * C<switches> + +Accepts a scalar value or array ref of scalar values indicating which +switches should be included if Perl tests are executed. Naturally, this +only makes sense in the context of tests written in Perl. + +=item * C<test_args> + +A reference to an C<@INC> style array of arguments to be passed to each +test program. + + test_args => ['foo', 'bar'], + +if you want to pass different arguments to each test then you should +pass a hash of arrays, keyed by the alias for each test: + + test_args => { + my_test => ['foo', 'bar'], + other_test => ['baz'], + } + +=item * C<color> + +Attempt to produce color output. + +=item * C<exec> + +Typically, Perl tests are run through this. However, anything which +spits out TAP is fine. You can use this argument to specify the name of +the program (and optional switches) to run your tests with: + + exec => ['/usr/bin/ruby', '-w'] + +You can also pass a subroutine reference in order to determine and +return the proper program to run based on a given test script. The +subroutine reference should expect the TAP::Harness object itself as the +first argument, and the file name as the second argument. It should +return an array reference containing the command to be run and including +the test file name. It can also simply return C<undef>, in which case +TAP::Harness will fall back on executing the test script in Perl: + + exec => sub { + my ( $harness, $test_file ) = @_; + + # Let Perl tests run. + return undef if $test_file =~ /[.]t$/; + return [ qw( /usr/bin/ruby -w ), $test_file ] + if $test_file =~ /[.]rb$/; + } + +If the subroutine returns a scalar with a newline or a filehandle, it +will be interpreted as raw TAP or as a TAP stream, respectively. + +=item * C<merge> + +If C<merge> is true the harness will create parsers that merge STDOUT +and STDERR together for any processes they start. + +=item * C<sources> + +I<NEW to 3.18>. + +If set, C<sources> must be a hashref containing the names of the +L<TAP::Parser::SourceHandler>s to load and/or configure. The values are a +hash of configuration that will be accessible to to the source handlers via +L<TAP::Parser::Source/config_for>. + +For example: + + sources => { + Perl => { exec => '/path/to/custom/perl' }, + File => { extensions => [ '.tap', '.txt' ] }, + MyCustom => { some => 'config' }, + } + +The C<sources> parameter affects how C<source>, C<tap> and C<exec> parameters +are handled. + +For more details, see the C<sources> parameter in L<TAP::Parser/new>, +L<TAP::Parser::Source>, and L<TAP::Parser::IteratorFactory>. + +=item * C<aggregator_class> + +The name of the class to use to aggregate test results. The default is +L<TAP::Parser::Aggregator>. + +=item * C<formatter_class> + +The name of the class to use to format output. The default is +L<TAP::Formatter::Console>, or L<TAP::Formatter::File> if the output +isn't a TTY. + +=item * C<multiplexer_class> + +The name of the class to use to multiplex tests during parallel testing. +The default is L<TAP::Parser::Multiplexer>. + +=item * C<parser_class> + +The name of the class to use to parse TAP. The default is +L<TAP::Parser>. + +=item * C<scheduler_class> + +The name of the class to use to schedule test execution. The default is +L<TAP::Parser::Scheduler>. + +=item * C<formatter> + +If set C<formatter> must be an object that is capable of formatting the +TAP output. See L<TAP::Formatter::Console> for an example. + +=item * C<errors> + +If parse errors are found in the TAP output, a note of this will be +made in the summary report. To see all of the parse errors, set this +argument to true: + + errors => 1 + +=item * C<directives> + +If set to a true value, only test results with directives will be +displayed. This overrides other settings such as C<verbose> or +C<failures>. + +=item * C<ignore_exit> + +If set to a true value instruct C<TAP::Parser> to ignore exit and wait +status from test scripts. + +=item * C<jobs> + +The maximum number of parallel tests to run at any time. Which tests +can be run in parallel is controlled by C<rules>. The default is to +run only one test at a time. + +=item * C<rules> + +A reference to a hash of rules that control which tests may be +executed in parallel. This is an experimental feature and the +interface may change. + + $harness->rules( + { par => [ + { seq => '../ext/DB_File/t/*' }, + { seq => '../ext/IO_Compress_Zlib/t/*' }, + { seq => '../lib/CPANPLUS/*' }, + { seq => '../lib/ExtUtils/t/*' }, + '*' + ] + } + ); + +=item * C<stdout> + +A filehandle for catching standard output. + +=back + +Any keys for which the value is C<undef> will be ignored. + +=cut + +# new supplied by TAP::Base + +{ + my @legal_callback = qw( + parser_args + made_parser + before_runtests + after_runtests + after_test + ); + + my %default_class = ( + aggregator_class => 'TAP::Parser::Aggregator', + formatter_class => 'TAP::Formatter::Console', + multiplexer_class => 'TAP::Parser::Multiplexer', + parser_class => 'TAP::Parser', + scheduler_class => 'TAP::Parser::Scheduler', + ); + + sub _initialize { + my ( $self, $arg_for ) = @_; + $arg_for ||= {}; + + $self->SUPER::_initialize( $arg_for, \@legal_callback ); + my %arg_for = %$arg_for; # force a shallow copy + + for my $name ( sort keys %VALIDATION_FOR ) { + my $property = delete $arg_for{$name}; + if ( defined $property ) { + my $validate = $VALIDATION_FOR{$name}; + + my $value = $self->$validate($property); + if ( $self->_error ) { + $self->_croak; + } + $self->$name($value); + } + } + + $self->jobs(1) unless defined $self->jobs; + + local $default_class{formatter_class} = 'TAP::Formatter::File' + unless -t ( $arg_for{stdout} || \*STDOUT ) && !$ENV{HARNESS_NOTTY}; + + while ( my ( $attr, $class ) = each %default_class ) { + $self->$attr( $self->$attr() || $class ); + } + + unless ( $self->formatter ) { + + # This is a little bodge to preserve legacy behaviour. It's + # pretty horrible that we know which args are destined for + # the formatter. + my %formatter_args = ( jobs => $self->jobs ); + for my $name (@FORMATTER_ARGS) { + if ( defined( my $property = delete $arg_for{$name} ) ) { + $formatter_args{$name} = $property; + } + } + + $self->formatter( + $self->_construct( $self->formatter_class, \%formatter_args ) + ); + } + + if ( my @props = sort keys %arg_for ) { + $self->_croak("Unknown arguments to TAP::Harness::new (@props)"); + } + + return $self; + } +} + +############################################################################## + +=head2 Instance Methods + +=head3 C<runtests> + + $harness->runtests(@tests); + +Accepts an array of C<@tests> to be run. This should generally be the +names of test files, but this is not required. Each element in C<@tests> +will be passed to C<TAP::Parser::new()> as a C<source>. See +L<TAP::Parser> for more information. + +It is possible to provide aliases that will be displayed in place of the +test name by supplying the test as a reference to an array containing +C<< [ $test, $alias ] >>: + + $harness->runtests( [ 't/foo.t', 'Foo Once' ], + [ 't/foo.t', 'Foo Twice' ] ); + +Normally it is an error to attempt to run the same test twice. Aliases +allow you to overcome this limitation by giving each run of the test a +unique name. + +Tests will be run in the order found. + +If the environment variable C<PERL_TEST_HARNESS_DUMP_TAP> is defined it +should name a directory into which a copy of the raw TAP for each test +will be written. TAP is written to files named for each test. +Subdirectories will be created as needed. + +Returns a L<TAP::Parser::Aggregator> containing the test results. + +=cut + +sub runtests { + my ( $self, @tests ) = @_; + + my $aggregate = $self->_construct( $self->aggregator_class ); + + $self->_make_callback( 'before_runtests', $aggregate ); + $aggregate->start; + $self->aggregate_tests( $aggregate, @tests ); + $aggregate->stop; + $self->summary($aggregate); + $self->_make_callback( 'after_runtests', $aggregate ); + + return $aggregate; +} + +=head3 C<summary> + + $harness->summary( $aggregator ); + +Output the summary for a L<TAP::Parser::Aggregator>. + +=cut + +sub summary { + my ( $self, $aggregate ) = @_; + $self->formatter->summary($aggregate); +} + +sub _after_test { + my ( $self, $aggregate, $job, $parser ) = @_; + + $self->_make_callback( 'after_test', $job->as_array_ref, $parser ); + $aggregate->add( $job->description, $parser ); +} + +sub _bailout { + my ( $self, $result ) = @_; + my $explanation = $result->explanation; + die "FAILED--Further testing stopped" + . ( $explanation ? ": $explanation\n" : ".\n" ); +} + +sub _aggregate_parallel { + my ( $self, $aggregate, $scheduler ) = @_; + + my $jobs = $self->jobs; + my $mux = $self->_construct( $self->multiplexer_class ); + + RESULT: { + + # Keep multiplexer topped up + FILL: + while ( $mux->parsers < $jobs ) { + my $job = $scheduler->get_job; + + # If we hit a spinner stop filling and start running. + last FILL if !defined $job || $job->is_spinner; + + my ( $parser, $session ) = $self->make_parser($job); + $mux->add( $parser, [ $session, $job ] ); + } + + if ( my ( $parser, $stash, $result ) = $mux->next ) { + my ( $session, $job ) = @$stash; + if ( defined $result ) { + $session->result($result); + $self->_bailout($result) if $result->is_bailout; + } + else { + + # End of parser. Automatically removed from the mux. + $self->finish_parser( $parser, $session ); + $self->_after_test( $aggregate, $job, $parser ); + $job->finish; + } + redo RESULT; + } + } + + return; +} + +sub _aggregate_single { + my ( $self, $aggregate, $scheduler ) = @_; + + JOB: + while ( my $job = $scheduler->get_job ) { + next JOB if $job->is_spinner; + + my ( $parser, $session ) = $self->make_parser($job); + + while ( defined( my $result = $parser->next ) ) { + $session->result($result); + if ( $result->is_bailout ) { + + # Keep reading until input is exhausted in the hope + # of allowing any pending diagnostics to show up. + 1 while $parser->next; + $self->_bailout($result); + } + } + + $self->finish_parser( $parser, $session ); + $self->_after_test( $aggregate, $job, $parser ); + $job->finish; + } + + return; +} + +=head3 C<aggregate_tests> + + $harness->aggregate_tests( $aggregate, @tests ); + +Run the named tests and display a summary of result. Tests will be run +in the order found. + +Test results will be added to the supplied L<TAP::Parser::Aggregator>. +C<aggregate_tests> may be called multiple times to run several sets of +tests. Multiple C<Test::Harness> instances may be used to pass results +to a single aggregator so that different parts of a complex test suite +may be run using different C<TAP::Harness> settings. This is useful, for +example, in the case where some tests should run in parallel but others +are unsuitable for parallel execution. + + my $formatter = TAP::Formatter::Console->new; + my $ser_harness = TAP::Harness->new( { formatter => $formatter } ); + my $par_harness = TAP::Harness->new( + { formatter => $formatter, + jobs => 9 + } + ); + my $aggregator = TAP::Parser::Aggregator->new; + + $aggregator->start(); + $ser_harness->aggregate_tests( $aggregator, @ser_tests ); + $par_harness->aggregate_tests( $aggregator, @par_tests ); + $aggregator->stop(); + $formatter->summary($aggregator); + +Note that for simpler testing requirements it will often be possible to +replace the above code with a single call to C<runtests>. + +Each element of the C<@tests> array is either: + +=over + +=item * the source name of a test to run + +=item * a reference to a [ source name, display name ] array + +=back + +In the case of a perl test suite, typically I<source names> are simply the file +names of the test scripts to run. + +When you supply a separate display name it becomes possible to run a +test more than once; the display name is effectively the alias by which +the test is known inside the harness. The harness doesn't care if it +runs the same test more than once when each invocation uses a +different name. + +=cut + +sub aggregate_tests { + my ( $self, $aggregate, @tests ) = @_; + + my $jobs = $self->jobs; + my $scheduler = $self->make_scheduler(@tests); + + # #12458 + local $ENV{HARNESS_IS_VERBOSE} = 1 + if $self->formatter->verbosity > 0; + + # Formatter gets only names. + $self->formatter->prepare( map { $_->description } $scheduler->get_all ); + + if ( $self->jobs > 1 ) { + $self->_aggregate_parallel( $aggregate, $scheduler ); + } + else { + $self->_aggregate_single( $aggregate, $scheduler ); + } + + return; +} + +sub _add_descriptions { + my $self = shift; + + # Turn unwrapped scalars into anonymous arrays and copy the name as + # the description for tests that have only a name. + return map { @$_ == 1 ? [ $_->[0], $_->[0] ] : $_ } + map { 'ARRAY' eq ref $_ ? $_ : [$_] } @_; +} + +=head3 C<make_scheduler> + +Called by the harness when it needs to create a +L<TAP::Parser::Scheduler>. Override in a subclass to provide an +alternative scheduler. C<make_scheduler> is passed the list of tests +that was passed to C<aggregate_tests>. + +=cut + +sub make_scheduler { + my ( $self, @tests ) = @_; + return $self->_construct( + $self->scheduler_class, + tests => [ $self->_add_descriptions(@tests) ], + rules => $self->rules + ); +} + +=head3 C<jobs> + +Gets or sets the number of concurrent test runs the harness is +handling. By default, this value is 1 -- for parallel testing, this +should be set higher. + +=cut + +############################################################################## + +sub _get_parser_args { + my ( $self, $job ) = @_; + my $test_prog = $job->filename; + my %args = (); + + $args{sources} = $self->sources if $self->sources; + + my @switches; + @switches = $self->lib if $self->lib; + push @switches => $self->switches if $self->switches; + $args{switches} = \@switches; + $args{spool} = $self->_open_spool($test_prog); + $args{merge} = $self->merge; + $args{ignore_exit} = $self->ignore_exit; + + if ( my $exec = $self->exec ) { + $args{exec} + = ref $exec eq 'CODE' + ? $exec->( $self, $test_prog ) + : [ @$exec, $test_prog ]; + if ( not defined $args{exec} ) { + $args{source} = $test_prog; + } + elsif ( ( ref( $args{exec} ) || "" ) ne "ARRAY" ) { + $args{source} = delete $args{exec}; + } + } + else { + $args{source} = $test_prog; + } + + if ( defined( my $test_args = $self->test_args ) ) { + + if ( ref($test_args) eq 'HASH' ) { + + # different args for each test + if ( exists( $test_args->{ $job->description } ) ) { + $test_args = $test_args->{ $job->description }; + } + else { + $self->_croak( "TAP::Harness Can't find test_args for " + . $job->description ); + } + } + + $args{test_args} = $test_args; + } + + return \%args; +} + +=head3 C<make_parser> + +Make a new parser and display formatter session. Typically used and/or +overridden in subclasses. + + my ( $parser, $session ) = $harness->make_parser; + +=cut + +sub make_parser { + my ( $self, $job ) = @_; + + my $args = $self->_get_parser_args($job); + $self->_make_callback( 'parser_args', $args, $job->as_array_ref ); + my $parser = $self->_construct( $self->parser_class, $args ); + + $self->_make_callback( 'made_parser', $parser, $job->as_array_ref ); + my $session = $self->formatter->open_test( $job->description, $parser ); + + return ( $parser, $session ); +} + +=head3 C<finish_parser> + +Terminate use of a parser. Typically used and/or overridden in +subclasses. The parser isn't destroyed as a result of this. + +=cut + +sub finish_parser { + my ( $self, $parser, $session ) = @_; + + $session->close_test; + $self->_close_spool($parser); + + return $parser; +} + +sub _open_spool { + my $self = shift; + my $test = shift; + + if ( my $spool_dir = $ENV{PERL_TEST_HARNESS_DUMP_TAP} ) { + + my $spool = File::Spec->catfile( $spool_dir, $test ); + + # Make the directory + my ( $vol, $dir, undef ) = File::Spec->splitpath($spool); + my $path = File::Spec->catpath( $vol, $dir, '' ); + eval { mkpath($path) }; + $self->_croak($@) if $@; + + my $spool_handle = IO::Handle->new; + open( $spool_handle, ">$spool" ) + or $self->_croak(" Can't write $spool ( $! ) "); + + return $spool_handle; + } + + return; +} + +sub _close_spool { + my $self = shift; + my ($parser) = @_; + + if ( my $spool_handle = $parser->delete_spool ) { + close($spool_handle) + or $self->_croak(" Error closing TAP spool file( $! ) \n "); + } + + return; +} + +sub _croak { + my ( $self, $message ) = @_; + unless ($message) { + $message = $self->_error; + } + $self->SUPER::_croak($message); + + return; +} + +1; + +__END__ + +############################################################################## + +=head1 CONFIGURING + +C<TAP::Harness> is designed to be easy to configure. + +=head2 Plugins + +C<TAP::Parser> plugins let you change the way TAP is I<input> to and I<output> +from the parser. + +L<TAP::Parser::SourceHandler>s handle TAP I<input>. You can configure them +and load custom handlers using the C<sources> parameter to L</new>. + +L<TAP::Formatter>s handle TAP I<output>. You can load custom formatters by +using the C<formatter_class> parameter to L</new>. To configure a formatter, +you currently need to instantiate it outside of L<TAP::Harness> and pass it in +with the C<formatter> parameter to L</new>. This I<may> be addressed by adding +a I<formatters> parameter to L</new> in the future. + +=head2 C<Module::Build> + +L<Module::Build> version C<0.30> supports C<TAP::Harness>. + +To load C<TAP::Harness> plugins, you'll need to use the C<tap_harness_args> +parameter to C<new>, typically from your C<Build.PL>. For example: + + Module::Build->new( + module_name => 'MyApp', + test_file_exts => [qw(.t .tap .txt)], + use_tap_harness => 1, + tap_harness_args => { + sources => { + MyCustom => {}, + File => { + extensions => ['.tap', '.txt'], + }, + }, + formatter => 'TAP::Formatter::HTML', + }, + build_requires => { + 'Module::Build' => '0.30', + 'TAP::Harness' => '3.18', + }, + )->create_build_script; + +See L</new> + +=head2 C<ExtUtils::MakeMaker> + +L<ExtUtils::MakeMaker> does not support L<TAP::Harness> out-of-the-box. + +=head2 C<prove> + +L<prove> supports C<TAP::Harness> plugins, and has a plugin system of its +own. See L<prove/FORMATTERS>, L<prove/SOURCE HANDLERS> and L<App::Prove> +for more details. + +=head1 WRITING PLUGINS + +If you can't configure C<TAP::Harness> to do what you want, and you can't find +an existing plugin, consider writing one. + +The two primary use cases supported by L<TAP::Harness> for plugins are I<input> +and I<output>: + +=over 2 + +=item Customize how TAP gets into the parser + +To do this, you can either extend an existing L<TAP::Parser::SourceHandler>, +or write your own. It's a pretty simple API, and they can be loaded and +configured using the C<sources> parameter to L</new>. + +=item Customize how TAP results are output from the parser + +To do this, you can either extend an existing L<TAP::Formatter>, or write your +own. Writing formatters are a bit more involved than writing a +I<SourceHandler>, as you'll need to understand the L<TAP::Parser> API. A +good place to start is by understanding how L</aggregate_tests> works. + +Custom formatters can be loaded configured using the C<formatter_class> +parameter to L</new>. + +=back + +=head1 SUBCLASSING + +If you can't configure C<TAP::Harness> to do exactly what you want, and writing +a plugin isn't an option, consider extending it. It is designed to be (mostly) +easy to subclass, though the cases when sub-classing is necessary should be few +and far between. + +=head2 Methods + +The following methods are ones you may wish to override if you want to +subclass C<TAP::Harness>. + +=over 4 + +=item L</new> + +=item L</runtests> + +=item L</summary> + +=back + +=cut + +=head1 REPLACING + +If you like the C<prove> utility and L<TAP::Parser> but you want your +own harness, all you need to do is write one and provide C<new> and +C<runtests> methods. Then you can use the C<prove> utility like so: + + prove --harness My::Test::Harness + +Note that while C<prove> accepts a list of tests (or things to be +tested), C<new> has a fairly rich set of arguments. You'll probably want +to read over this code carefully to see how all of them are being used. + +=head1 SEE ALSO + +L<Test::Harness> + +=cut + +# vim:ts=4:sw=4:et:sta diff --git a/Master/tlpkg/tlperl/lib/TAP/Harness/Beyond.pod b/Master/tlpkg/tlperl/lib/TAP/Harness/Beyond.pod new file mode 100755 index 00000000000..8704f52b280 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Harness/Beyond.pod @@ -0,0 +1,416 @@ +=head1 Beyond make test + +Test::Harness is responsible for running test scripts, analysing +their output and reporting success or failure. When I type +F<make test> (or F<./Build test>) for a module, Test::Harness is usually +used to run the tests (not all modules use Test::Harness but the +majority do). + +To start exploring some of the features of Test::Harness I need to +switch from F<make test> to the F<prove> command (which ships with +Test::Harness). For the following examples I'll also need a recent +version of Test::Harness installed; 3.14 is current as I write. + +For the examples I'm going to assume that we're working with a +'normal' Perl module distribution. Specifically I'll assume that +typing F<make> or F<./Build> causes the built, ready-to-install module +code to be available below ./blib/lib and ./blib/arch and that +there's a directory called 't' that contains our tests. Test::Harness +isn't hardwired to that configuration but it saves me from explaining +which files live where for each example. + +Back to F<prove>; like F<make test> it runs a test suite - but it +provides far more control over which tests are executed, in what +order and how their results are reported. Typically F<make test> +runs all the test scripts below the 't' directory. To do the same +thing with prove I type: + + prove -rb t + +The switches here are -r to recurse into any directories below 't' +and -b which adds ./blib/lib and ./blib/arch to Perl's include path +so that the tests can find the code they will be testing. If I'm +testing a module of which an earlier version is already installed +I need to be careful about the include path to make sure I'm not +running my tests against the installed version rather than the new +one that I'm working on. + +Unlike F<make test>, typing F<prove> doesn't automatically rebuild +my module. If I forget to make before prove I will be testing against +older versions of those files - which inevitably leads to confusion. +I either get into the habit of typing + + make && prove -rb t + +or - if I have no XS code that needs to be built I use the modules +below F<lib> instead + + prove -Ilib -r t + +So far I've shown you nothing that F<make test> doesn't do. Let's +fix that. + +=head2 Saved State + +If I have failing tests in a test suite that consists of more than +a handful of scripts and takes more than a few seconds to run it +rapidly becomes tedious to run the whole test suite repeatedly as +I track down the problems. + +I can tell prove just to run the tests that are failing like this: + + prove -b t/this_fails.t t/so_does_this.t + +That speeds things up but I have to make a note of which tests are +failing and make sure that I run those tests. Instead I can use +prove's --state switch and have it keep track of failing tests for +me. First I do a complete run of the test suite and tell prove to +save the results: + + prove -rb --state=save t + +That stores a machine readable summary of the test run in a file +called '.prove' in the current directory. If I have failures I can +then run just the failing scripts like this: + + prove -b --state=failed + +I can also tell prove to save the results again so that it updates +its idea of which tests failed: + + prove -b --state=failed,save + +As soon as one of my failing tests passes it will be removed from +the list of failed tests. Eventually I fix them all and prove can +find no failing tests to run: + + Files=0, Tests=0, 0 wallclock secs ( 0.00 usr + 0.00 sys = 0.00 CPU) + Result: NOTESTS + +As I work on a particular part of my module it's most likely that +the tests that cover that code will fail. I'd like to run the whole +test suite but have it prioritize these 'hot' tests. I can tell +prove to do this: + + prove -rb --state=hot,save t + +All the tests will run but those that failed most recently will be +run first. If no tests have failed since I started saving state all +tests will run in their normal order. This combines full test +coverage with early notification of failures. + +The --state switch supports a number of options; for example to run +failed tests first followed by all remaining tests ordered by the +timestamps of the test scripts - and save the results - I can use + + prove -rb --state=failed,new,save t + +See the prove documentation (type prove --man) for the full list +of state options. + +When I tell prove to save state it writes a file called '.prove' +('_prove' on Windows) in the current directory. It's a YAML document +so it's quite easy to write tools of your own that work on the saved +test state - but the format isn't officially documented so it might +change without (much) warning in the future. + +=head2 Parallel Testing + +If my tests take too long to run I may be able to speed them up by +running multiple test scripts in parallel. This is particularly +effective if the tests are I/O bound or if I have multiple CPU +cores. I tell prove to run my tests in parallel like this: + + prove -rb -j 9 t + +The -j switch enables parallel testing; the number that follows it +is the maximum number of tests to run in parallel. Sometimes tests +that pass when run sequentially will fail when run in parallel. For +example if two different test scripts use the same temporary file +or attempt to listen on the same socket I'll have problems running +them in parallel. If I see unexpected failures I need to check my +tests to work out which of them are trampling on the same resource +and rename temporary files or add locks as appropriate. + +To get the most performance benefit I want to have the test scripts +that take the longest to run start first - otherwise I'll be waiting +for the one test that takes nearly a minute to complete after all +the others are done. I can use the --state switch to run the tests +in slowest to fastest order: + + prove -rb -j 9 --state=slow,save t + +=head2 Non-Perl Tests + +The Test Anything Protocol (http://testanything.org/) isn't just +for Perl. Just about any language can be used to write tests that +output TAP. There are TAP based testing libraries for C, C++, PHP, +Python and many others. If I can't find a TAP library for my language +of choice it's easy to generate valid TAP. It looks like this: + + 1..3 + ok 1 - init OK + ok 2 - opened file + not ok 3 - appended to file + +The first line is the plan - it specifies the number of tests I'm +going to run so that it's easy to check that the test script didn't +exit before running all the expected tests. The following lines are +the test results - 'ok' for pass, 'not ok' for fail. Each test has +a number and, optionally, a description. And that's it. Any language +that can produce output like that on STDOUT can be used to write +tests. + +Recently I've been rekindling a two-decades-old interest in Forth. +Evidently I have a masochistic streak that even Perl can't satisfy. +I want to write tests in Forth and run them using prove (you can +find my gforth TAP experiments at +https://svn.hexten.net/andy/Forth/Testing/). I can use the --exec +switch to tell prove to run the tests using gforth like this: + + prove -r --exec gforth t + +Alternately, if the language used to write my tests allows a shebang +line I can use that to specify the interpreter. Here's a test written +in PHP: + + #!/usr/bin/php + <?php + print "1..2\n"; + print "ok 1\n"; + print "not ok 2\n"; + ?> + +If I save that as t/phptest.t the shebang line will ensure that it +runs correctly along with all my other tests. + +=head2 Mixing it up + +Subtle interdependencies between test programs can mask problems - +for example an earlier test may neglect to remove a temporary file +that affects the behaviour of a later test. To find this kind of +problem I use the --shuffle and --reverse options to run my tests +in random or reversed order. + +=head2 Rolling My Own + +If I need a feature that prove doesn't provide I can easily write my own. + +Typically you'll want to change how TAP gets I<input> into and I<output> +from the parser. L<App::Prove> supports arbitrary plugins, and L<TAP::Harness> +supports custom I<formatters> and I<source handlers> that you can load using +either L<prove> or L<Module::Build>; there are many examples to base mine on. +For more details see L<App::Prove>, L<TAP::Parser::SourceHandler>, and +L<TAP::Formatter::Base>. + +If writing a plugin is not enough, you can write your own test harness; one of +the motives for the 3.00 rewrite of Test::Harness was to make it easier to +subclass and extend. + +The Test::Harness module is a compatibility wrapper around TAP::Harness. +For new applications I should use TAP::Harness directly. As we'll +see, prove uses TAP::Harness. + +When I run prove it processes its arguments, figures out which test +scripts to run and then passes control to TAP::Harness to run the +tests, parse, analyse and present the results. By subclassing +TAP::Harness I can customise many aspects of the test run. + +I want to log my test results in a database so I can track them +over time. To do this I override the summary method in TAP::Harness. +I start with a simple prototype that dumps the results as a YAML +document: + + package My::TAP::Harness; + + use base qw( TAP::Harness ); use YAML; + + sub summary { + my ( $self, $aggregate ) = @_; print Dump( $aggregate ); + $self->SUPER::summary( $aggregate ); + } + + 1; + +I need to tell prove to use my My::TAP::Harness. If My::TAP::Harness +is on Perl's @INC include path I can + + prove --harness=My::TAP::Harness -rb t + +If I don't have My::TAP::Harness installed on @INC I need to provide +the correct path to perl when I run prove: + + perl -Ilib `which prove` --harness=My::TAP::Harness -rb t + +I can incorporate these options into my own version of prove. It's +pretty simple. Most of the work of prove is handled by App::Prove. +The important code in prove is just: + + use App::Prove; + + my $app = App::Prove->new; + $app->process_args(@ARGV); + exit( $app->run ? 0 : 1 ); + +If I write a subclass of App::Prove I can customise any aspect of +the test runner while inheriting all of prove's behaviour. Here's +myprove: + + #!/usr/bin/env perl use lib qw( lib ); # Add ./lib to @INC + use App::Prove; + + my $app = App::Prove->new; + + # Use custom TAP::Harness subclass $app->harness( 'My::TAP::Harness' + ); + + $app->process_args( @ARGV ); exit( $app->run ? 0 : 1 ); + +Now I can run my tests like this + + ./myprove -rb t + +=head2 Deeper Customisation + +Now that I know how to subclass and replace TAP::Harness I can +replace any other part of the harness. To do that I need to know +which classes are responsible for which functionality. Here's a +brief guided tour; the default class for each component is shown +in parentheses. Normally any replacements I write will be subclasses +of these default classes. + +When I run my tests TAP::Harness creates a scheduler +(TAP::Parser::Scheduler) to work out the running order for the +tests, an aggregator (TAP::Parser::Aggregator) to collect and analyse +the test results and a formatter (TAP::Formatter::Console) to display +those results. + +If I'm running my tests in parallel there may also be a multiplexer +(TAP::Parser::Multiplexer) - the component that allows multiple +tests to run simultaneously. + +Once it has created those helpers TAP::Harness starts running the +tests. For each test it creates a new parser (TAP::Parser) which +is responsible for running the test script and parsing its output. + +To replace any of these components I call one of these harness +methods with the name of the replacement class: + + aggregator_class + formatter_class + multiplexer_class + parser_class + scheduler_class + +For example, to replace the aggregator I would + + $harness->aggregator_class( 'My::Aggregator' ); + +Alternately I can supply the names of my substitute classes to the +TAP::Harness constructor: + + my $harness = TAP::Harness->new( + { aggregator_class => 'My::Aggregator' } + ); + +If I need to reach even deeper into the internals of the harness I +can replace the classes that TAP::Parser uses to execute test scripts +and tokenise their output. Before running a test script TAP::Parser +creates a grammar (TAP::Parser::Grammar) to decode the raw TAP into +tokens, a result factory (TAP::Parser::ResultFactory) to turn the +decoded TAP results into objects and, depending on whether it's +running a test script or reading TAP from a file, scalar or array +a source or an iterator (TAP::Parser::IteratorFactory). + +Each of these objects may be replaced by calling one of these parser +methods: + + source_class + perl_source_class + grammar_class + iterator_factory_class + result_factory_class + +=head2 Callbacks + +As an alternative to subclassing the components I need to change I +can attach callbacks to the default classes. TAP::Harness exposes +these callbacks: + + parser_args Tweak the parameters used to create the parser + made_parser Just made a new parser + before_runtests About to run tests + after_runtests Have run all tests + after_test Have run an individual test script + +TAP::Parser also supports callbacks; bailout, comment, plan, test, +unknown, version and yaml are called for the corresponding TAP +result types, ALL is called for all results, ELSE is called for all +results for which a named callback is not installed and EOF is +called once at the end of each TAP stream. + +To install a callback I pass the name of the callback and a subroutine +reference to TAP::Harness or TAP::Parser's callback method: + + $harness->callback( after_test => sub { + my ( $script, $desc, $parser ) = @_; + } ); + +I can also pass callbacks to the constructor: + + my $harness = TAP::Harness->new({ + callbacks => { + after_test => sub { + my ( $script, $desc, $parser ) = @_; + # Do something interesting here + } + } + }); + +When it comes to altering the behaviour of the test harness there's +more than one way to do it. Which way is best depends on my +requirements. In general if I only want to observe test execution +without changing the harness' behaviour (for example to log test +results to a database) I choose callbacks. If I want to make the +harness behave differently subclassing gives me more control. + +=head2 Parsing TAP + +Perhaps I don't need a complete test harness. If I already have a +TAP test log that I need to parse all I need is TAP::Parser and the +various classes it depends upon. Here's the code I need to run a +test and parse its TAP output + + use TAP::Parser; + + my $parser = TAP::Parser->new( { source => 't/simple.t' } ); + while ( my $result = $parser->next ) { + print $result->as_string, "\n"; + } + +Alternately I can pass an open filehandle as source and have the +parser read from that rather than attempting to run a test script: + + open my $tap, '<', 'tests.tap' + or die "Can't read TAP transcript ($!)\n"; + my $parser = TAP::Parser->new( { source => $tap } ); + while ( my $result = $parser->next ) { + print $result->as_string, "\n"; + } + +This approach is useful if I need to convert my TAP based test +results into some other representation. See TAP::Convert::TET +(http://search.cpan.org/dist/TAP-Convert-TET/) for an example of +this approach. + +=head2 Getting Support + +The Test::Harness developers hang out on the tapx-dev mailing +list[1]. For discussion of general, language independent TAP issues +there's the tap-l[2] list. Finally there's a wiki dedicated to the +Test Anything Protocol[3]. Contributions to the wiki, patches and +suggestions are all welcome. + +[1] L<http://www.hexten.net/mailman/listinfo/tapx-dev> +[2] L<http://testanything.org/mailman/listinfo/tap-l> +[3] L<http://testanything.org/> diff --git a/Master/tlpkg/tlperl/lib/TAP/Object.pm b/Master/tlpkg/tlperl/lib/TAP/Object.pm new file mode 100755 index 00000000000..aa65082d0af --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Object.pm @@ -0,0 +1,158 @@ +package TAP::Object; + +use strict; +use vars qw($VERSION); + +=head1 NAME + +TAP::Object - Base class that provides common functionality to all C<TAP::*> modules + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + package TAP::Whatever; + + use strict; + use vars qw(@ISA); + + use TAP::Object; + + @ISA = qw(TAP::Object); + + # new() implementation by TAP::Object + sub _initialize { + my ( $self, @args) = @_; + # initialize your object + return $self; + } + + # ... later ... + my $obj = TAP::Whatever->new(@args); + +=head1 DESCRIPTION + +C<TAP::Object> provides a default constructor and exception model for all +C<TAP::*> classes. Exceptions are raised using L<Carp>. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +Create a new object. Any arguments passed to C<new> will be passed on to the +L</_initialize> method. Returns a new object. + +=cut + +sub new { + my $class = shift; + my $self = bless {}, $class; + return $self->_initialize(@_); +} + +=head2 Instance Methods + +=head3 C<_initialize> + +Initializes a new object. This method is a stub by default, you should override +it as appropriate. + +I<Note:> L</new> expects you to return C<$self> or raise an exception. See +L</_croak>, and L<Carp>. + +=cut + +sub _initialize { + return $_[0]; +} + +=head3 C<_croak> + +Raise an exception using C<croak> from L<Carp>, eg: + + $self->_croak( 'why me?', 'aaarrgh!' ); + +May also be called as a I<class> method. + + $class->_croak( 'this works too' ); + +=cut + +sub _croak { + my $proto = shift; + require Carp; + Carp::croak(@_); + return; +} + +=head3 C<_confess> + +Raise an exception using C<confess> from L<Carp>, eg: + + $self->_confess( 'why me?', 'aaarrgh!' ); + +May also be called as a I<class> method. + + $class->_confess( 'this works too' ); + +=cut + +sub _confess { + my $proto = shift; + require Carp; + Carp::confess(@_); + return; +} + +=head3 C<_construct> + +Create a new instance of the specified class. + +=cut + +sub _construct { + my ( $self, $class, @args ) = @_; + + $self->_croak("Bad module name $class") + unless $class =~ /^ \w+ (?: :: \w+ ) *$/x; + + unless ( $class->can('new') ) { + local $@; + eval "require $class"; + $self->_croak("Can't load $class") if $@; + } + + return $class->new(@args); +} + +=head3 C<mk_methods> + +Create simple getter/setters. + + __PACKAGE__->mk_methods(@method_names); + +=cut + +sub mk_methods { + my ( $class, @methods ) = @_; + for my $method_name (@methods) { + my $method = "${class}::$method_name"; + no strict 'refs'; + *$method = sub { + my $self = shift; + $self->{$method_name} = shift if @_; + return $self->{$method_name}; + }; + } +} + +1; + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser.pm b/Master/tlpkg/tlperl/lib/TAP/Parser.pm new file mode 100755 index 00000000000..f8baa39f312 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser.pm @@ -0,0 +1,1894 @@ +package TAP::Parser; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Base (); +use TAP::Parser::Grammar (); +use TAP::Parser::Result (); +use TAP::Parser::ResultFactory (); +use TAP::Parser::Source (); +use TAP::Parser::Iterator (); +use TAP::Parser::IteratorFactory (); +use TAP::Parser::SourceHandler::Executable (); +use TAP::Parser::SourceHandler::Perl (); +use TAP::Parser::SourceHandler::File (); +use TAP::Parser::SourceHandler::RawTAP (); +use TAP::Parser::SourceHandler::Handle (); + +use Carp qw( confess ); + +=head1 NAME + +TAP::Parser - Parse L<TAP|Test::Harness::TAP> output + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +my $DEFAULT_TAP_VERSION = 12; +my $MAX_TAP_VERSION = 13; + +$ENV{TAP_VERSION} = $MAX_TAP_VERSION; + +END { + + # For VMS. + delete $ENV{TAP_VERSION}; +} + +BEGIN { # making accessors + @ISA = qw(TAP::Base); + + __PACKAGE__->mk_methods( + qw( + _iterator + _spool + exec + exit + is_good_plan + plan + tests_planned + tests_run + wait + version + in_todo + start_time + end_time + skip_all + grammar_class + result_factory_class + iterator_factory_class + ) + ); + + sub _stream { # deprecated + my $self = shift; + $self->_iterator(@_); + } +} # done making accessors + +=head1 SYNOPSIS + + use TAP::Parser; + + my $parser = TAP::Parser->new( { source => $source } ); + + while ( my $result = $parser->next ) { + print $result->as_string; + } + +=head1 DESCRIPTION + +C<TAP::Parser> is designed to produce a proper parse of TAP output. For +an example of how to run tests through this module, see the simple +harnesses C<examples/>. + +There's a wiki dedicated to the Test Anything Protocol: + +L<http://testanything.org> + +It includes the TAP::Parser Cookbook: + +L<http://testanything.org/wiki/index.php/TAP::Parser_Cookbook> + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $parser = TAP::Parser->new(\%args); + +Returns a new C<TAP::Parser> object. + +The arguments should be a hashref with I<one> of the following keys: + +=over 4 + +=item * C<source> + +I<CHANGED in 3.18> + +This is the preferred method of passing input to the constructor. + +The C<source> is used to create a L<TAP::Parser::Source> that is passed to the +L</iterator_factory_class> which in turn figures out how to handle the source and +creates a <TAP::Parser::Iterator> for it. The iterator is used by the parser to +read in the TAP stream. + +To configure the I<IteratorFactory> use the C<sources> parameter below. + +Note that C<source>, C<tap> and C<exec> are I<mutually exclusive>. + +=item * C<tap> + +I<CHANGED in 3.18> + +The value should be the complete TAP output. + +The I<tap> is used to create a L<TAP::Parser::Source> that is passed to the +L</iterator_factory_class> which in turn figures out how to handle the source and +creates a <TAP::Parser::Iterator> for it. The iterator is used by the parser to +read in the TAP stream. + +To configure the I<IteratorFactory> use the C<sources> parameter below. + +Note that C<source>, C<tap> and C<exec> are I<mutually exclusive>. + +=item * C<exec> + +Must be passed an array reference. + +The I<exec> array ref is used to create a L<TAP::Parser::Source> that is passed +to the L</iterator_factory_class> which in turn figures out how to handle the +source and creates a <TAP::Parser::Iterator> for it. The iterator is used by +the parser to read in the TAP stream. + +By default the L<TAP::Parser::SourceHandler::Executable> class will create a +L<TAP::Parser::Iterator::Process> object to handle the source. This passes the +array reference strings as command arguments to L<IPC::Open3::open3|IPC::Open3>: + + exec => [ '/usr/bin/ruby', 't/my_test.rb' ] + +If any C<test_args> are given they will be appended to the end of the command +argument list. + +To configure the I<IteratorFactory> use the C<sources> parameter below. + +Note that C<source>, C<tap> and C<exec> are I<mutually exclusive>. + +=back + +The following keys are optional. + +=over 4 + +=item * C<sources> + +I<NEW to 3.18>. + +If set, C<sources> must be a hashref containing the names of the +L<TAP::Parser::SourceHandler>s to load and/or configure. The values are a +hash of configuration that will be accessible to to the source handlers via +L<TAP::Parser::Source/config_for>. + +For example: + + sources => { + Perl => { exec => '/path/to/custom/perl' }, + File => { extensions => [ '.tap', '.txt' ] }, + MyCustom => { some => 'config' }, + } + +This will cause C<TAP::Parser> to pass custom configuration to two of the built- +in source handlers - L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::File> - and attempt to load the C<MyCustom> +class. See L<TAP::Parser::IteratorFactory/load_handlers> for more detail. + +The C<sources> parameter affects how C<source>, C<tap> and C<exec> parameters +are handled. + +See L<TAP::Parser::IteratorFactory>, L<TAP::Parser::SourceHandler> and subclasses for +more details. + +=item * C<callback> + +If present, each callback corresponding to a given result type will be called +with the result as the argument if the C<run> method is used: + + my %callbacks = ( + test => \&test_callback, + plan => \&plan_callback, + comment => \&comment_callback, + bailout => \&bailout_callback, + unknown => \&unknown_callback, + ); + + my $aggregator = TAP::Parser::Aggregator->new; + for my $file ( @test_files ) { + my $parser = TAP::Parser->new( + { + source => $file, + callbacks => \%callbacks, + } + ); + $parser->run; + $aggregator->add( $file, $parser ); + } + +=item * C<switches> + +If using a Perl file as a source, optional switches may be passed which will +be used when invoking the perl executable. + + my $parser = TAP::Parser->new( { + source => $test_file, + switches => [ '-Ilib' ], + } ); + +=item * C<test_args> + +Used in conjunction with the C<source> and C<exec> option to supply a reference +to an C<@ARGV> style array of arguments to pass to the test program. + +=item * C<spool> + +If passed a filehandle will write a copy of all parsed TAP to that handle. + +=item * C<merge> + +If false, STDERR is not captured (though it is 'relayed' to keep it +somewhat synchronized with STDOUT.) + +If true, STDERR and STDOUT are the same filehandle. This may cause +breakage if STDERR contains anything resembling TAP format, but does +allow exact synchronization. + +Subtleties of this behavior may be platform-dependent and may change in +the future. + +=item * C<grammar_class> + +This option was introduced to let you easily customize which I<grammar> class +the parser should use. It defaults to L<TAP::Parser::Grammar>. + +See also L</make_grammar>. + +=item * C<result_factory_class> + +This option was introduced to let you easily customize which I<result> +factory class the parser should use. It defaults to +L<TAP::Parser::ResultFactory>. + +See also L</make_result>. + +=item * C<iterator_factory_class> + +I<CHANGED in 3.18> + +This option was introduced to let you easily customize which I<iterator> +factory class the parser should use. It defaults to +L<TAP::Parser::IteratorFactory>. + +=back + +=cut + +# new() implementation supplied by TAP::Base + +# This should make overriding behaviour of the Parser in subclasses easier: +sub _default_grammar_class {'TAP::Parser::Grammar'} +sub _default_result_factory_class {'TAP::Parser::ResultFactory'} +sub _default_iterator_factory_class {'TAP::Parser::IteratorFactory'} + +############################################################################## + +=head2 Instance Methods + +=head3 C<next> + + my $parser = TAP::Parser->new( { source => $file } ); + while ( my $result = $parser->next ) { + print $result->as_string, "\n"; + } + +This method returns the results of the parsing, one result at a time. Note +that it is destructive. You can't rewind and examine previous results. + +If callbacks are used, they will be issued before this call returns. + +Each result returned is a subclass of L<TAP::Parser::Result>. See that +module and related classes for more information on how to use them. + +=cut + +sub next { + my $self = shift; + return ( $self->{_iter} ||= $self->_iter )->(); +} + +############################################################################## + +=head3 C<run> + + $parser->run; + +This method merely runs the parser and parses all of the TAP. + +=cut + +sub run { + my $self = shift; + while ( defined( my $result = $self->next ) ) { + + # do nothing + } +} + +############################################################################## + +=head3 C<make_grammar> + +Make a new L<TAP::Parser::Grammar> object and return it. Passes through any +arguments given. + +The C<grammar_class> can be customized, as described in L</new>. + +=head3 C<make_result> + +Make a new L<TAP::Parser::Result> object using the parser's +L<TAP::Parser::ResultFactory>, and return it. Passes through any arguments +given. + +The C<result_factory_class> can be customized, as described in L</new>. + +=head3 C<make_iterator_factory> + +I<NEW to 3.18>. + +Make a new L<TAP::Parser::IteratorFactory> object and return it. Passes through +any arguments given. + +C<iterator_factory_class> can be customized, as described in L</new>. + +=cut + +# This should make overriding behaviour of the Parser in subclasses easier: +sub make_iterator_factory { shift->iterator_factory_class->new(@_); } +sub make_grammar { shift->grammar_class->new(@_); } +sub make_result { shift->result_factory_class->make_result(@_); } + +{ + + # of the following, anything beginning with an underscore is strictly + # internal and should not be exposed. + my %initialize = ( + version => $DEFAULT_TAP_VERSION, + plan => '', # the test plan (e.g., 1..3) + tap => '', # the TAP + tests_run => 0, # actual current test numbers + results => [], # TAP parser results + skipped => [], # + todo => [], # + passed => [], # + failed => [], # + actual_failed => [], # how many tests really failed + actual_passed => [], # how many tests really passed + todo_passed => [], # tests which unexpectedly succeed + parse_errors => [], # perfect TAP should have none + ); + + # We seem to have this list hanging around all over the place. We could + # probably get it from somewhere else to avoid the repetition. + my @legal_callback = qw( + test + version + plan + comment + bailout + unknown + yaml + ALL + ELSE + EOF + ); + + my @class_overrides = qw( + grammar_class + result_factory_class + iterator_factory_class + ); + + sub _initialize { + my ( $self, $arg_for ) = @_; + + # everything here is basically designed to convert any TAP source to a + # TAP::Parser::Iterator. + + # Shallow copy + my %args = %{ $arg_for || {} }; + + $self->SUPER::_initialize( \%args, \@legal_callback ); + + # get any class overrides out first: + for my $key (@class_overrides) { + my $default_method = "_default_$key"; + my $val = delete $args{$key} || $self->$default_method(); + $self->$key($val); + } + + my $iterator = delete $args{iterator}; + $iterator ||= delete $args{stream}; # deprecated + my $tap = delete $args{tap}; + my $raw_source = delete $args{source}; + my $sources = delete $args{sources}; + my $exec = delete $args{exec}; + my $merge = delete $args{merge}; + my $spool = delete $args{spool}; + my $switches = delete $args{switches}; + my $ignore_exit = delete $args{ignore_exit}; + my $test_args = delete $args{test_args} || []; + + if ( 1 < grep {defined} $iterator, $tap, $raw_source, $exec ) { + $self->_croak( + "You may only choose one of 'exec', 'tap', 'source' or 'iterator'" + ); + } + + if ( my @excess = sort keys %args ) { + $self->_croak("Unknown options: @excess"); + } + + # convert $tap & $exec to $raw_source equiv. + my $type = ''; + my $source = TAP::Parser::Source->new; + if ($tap) { + $type = 'raw TAP'; + $source->raw( \$tap ); + } + elsif ($exec) { + $type = 'exec ' . $exec->[0]; + $source->raw( { exec => [ @$exec, @$test_args ] } ); + } + elsif ($raw_source) { + $type = 'source ' . ref($raw_source) || $raw_source; + $source->raw( ref($raw_source) ? $raw_source : \$raw_source ); + } + elsif ($iterator) { + $type = 'iterator ' . ref($iterator); + } + + if ( $source->raw ) { + my $src_factory = $self->make_iterator_factory($sources); + $source->merge($merge)->switches($switches) + ->test_args($test_args); + $iterator = $src_factory->make_iterator($source); + } + + unless ($iterator) { + $self->_croak( + "PANIC: could not determine iterator for input $type"); + } + + while ( my ( $k, $v ) = each %initialize ) { + $self->{$k} = 'ARRAY' eq ref $v ? [] : $v; + } + + $self->_iterator($iterator); + $self->_spool($spool); + $self->ignore_exit($ignore_exit); + + return $self; + } +} + +=head1 INDIVIDUAL RESULTS + +If you've read this far in the docs, you've seen this: + + while ( my $result = $parser->next ) { + print $result->as_string; + } + +Each result returned is a L<TAP::Parser::Result> subclass, referred to as +I<result types>. + +=head2 Result types + +Basically, you fetch individual results from the TAP. The six types, with +examples of each, are as follows: + +=over 4 + +=item * Version + + TAP version 12 + +=item * Plan + + 1..42 + +=item * Pragma + + pragma +strict + +=item * Test + + ok 3 - We should start with some foobar! + +=item * Comment + + # Hope we don't use up the foobar. + +=item * Bailout + + Bail out! We ran out of foobar! + +=item * Unknown + + ... yo, this ain't TAP! ... + +=back + +Each result fetched is a result object of a different type. There are common +methods to each result object and different types may have methods unique to +their type. Sometimes a type method may be overridden in a subclass, but its +use is guaranteed to be identical. + +=head2 Common type methods + +=head3 C<type> + +Returns the type of result, such as C<comment> or C<test>. + +=head3 C<as_string> + +Prints a string representation of the token. This might not be the exact +output, however. Tests will have test numbers added if not present, TODO and +SKIP directives will be capitalized and, in general, things will be cleaned +up. If you need the original text for the token, see the C<raw> method. + +=head3 C<raw> + +Returns the original line of text which was parsed. + +=head3 C<is_plan> + +Indicates whether or not this is the test plan line. + +=head3 C<is_test> + +Indicates whether or not this is a test line. + +=head3 C<is_comment> + +Indicates whether or not this is a comment. Comments will generally only +appear in the TAP stream if STDERR is merged to STDOUT. See the +C<merge> option. + +=head3 C<is_bailout> + +Indicates whether or not this is bailout line. + +=head3 C<is_yaml> + +Indicates whether or not the current item is a YAML block. + +=head3 C<is_unknown> + +Indicates whether or not the current line could be parsed. + +=head3 C<is_ok> + + if ( $result->is_ok ) { ... } + +Reports whether or not a given result has passed. Anything which is B<not> a +test result returns true. This is merely provided as a convenient shortcut +which allows you to do this: + + my $parser = TAP::Parser->new( { source => $source } ); + while ( my $result = $parser->next ) { + # only print failing results + print $result->as_string unless $result->is_ok; + } + +=head2 C<plan> methods + + if ( $result->is_plan ) { ... } + +If the above evaluates as true, the following methods will be available on the +C<$result> object. + +=head3 C<plan> + + if ( $result->is_plan ) { + print $result->plan; + } + +This is merely a synonym for C<as_string>. + +=head3 C<directive> + + my $directive = $result->directive; + +If a SKIP directive is included with the plan, this method will return it. + + 1..0 # SKIP: why bother? + +=head3 C<explanation> + + my $explanation = $result->explanation; + +If a SKIP directive was included with the plan, this method will return the +explanation, if any. + +=head2 C<pragma> methods + + if ( $result->is_pragma ) { ... } + +If the above evaluates as true, the following methods will be available on the +C<$result> object. + +=head3 C<pragmas> + +Returns a list of pragmas each of which is a + or - followed by the +pragma name. + +=head2 C<commment> methods + + if ( $result->is_comment ) { ... } + +If the above evaluates as true, the following methods will be available on the +C<$result> object. + +=head3 C<comment> + + if ( $result->is_comment ) { + my $comment = $result->comment; + print "I have something to say: $comment"; + } + +=head2 C<bailout> methods + + if ( $result->is_bailout ) { ... } + +If the above evaluates as true, the following methods will be available on the +C<$result> object. + +=head3 C<explanation> + + if ( $result->is_bailout ) { + my $explanation = $result->explanation; + print "We bailed out because ($explanation)"; + } + +If, and only if, a token is a bailout token, you can get an "explanation" via +this method. The explanation is the text after the mystical "Bail out!" words +which appear in the tap output. + +=head2 C<unknown> methods + + if ( $result->is_unknown ) { ... } + +There are no unique methods for unknown results. + +=head2 C<test> methods + + if ( $result->is_test ) { ... } + +If the above evaluates as true, the following methods will be available on the +C<$result> object. + +=head3 C<ok> + + my $ok = $result->ok; + +Returns the literal text of the C<ok> or C<not ok> status. + +=head3 C<number> + + my $test_number = $result->number; + +Returns the number of the test, even if the original TAP output did not supply +that number. + +=head3 C<description> + + my $description = $result->description; + +Returns the description of the test, if any. This is the portion after the +test number but before the directive. + +=head3 C<directive> + + my $directive = $result->directive; + +Returns either C<TODO> or C<SKIP> if either directive was present for a test +line. + +=head3 C<explanation> + + my $explanation = $result->explanation; + +If a test had either a C<TODO> or C<SKIP> directive, this method will return +the accompanying explantion, if present. + + not ok 17 - 'Pigs can fly' # TODO not enough acid + +For the above line, the explanation is I<not enough acid>. + +=head3 C<is_ok> + + if ( $result->is_ok ) { ... } + +Returns a boolean value indicating whether or not the test passed. Remember +that for TODO tests, the test always passes. + +B<Note:> this was formerly C<passed>. The latter method is deprecated and +will issue a warning. + +=head3 C<is_actual_ok> + + if ( $result->is_actual_ok ) { ... } + +Returns a boolean value indicating whether or not the test passed, regardless +of its TODO status. + +B<Note:> this was formerly C<actual_passed>. The latter method is deprecated +and will issue a warning. + +=head3 C<is_unplanned> + + if ( $test->is_unplanned ) { ... } + +If a test number is greater than the number of planned tests, this method will +return true. Unplanned tests will I<always> return false for C<is_ok>, +regardless of whether or not the test C<has_todo> (see +L<TAP::Parser::Result::Test> for more information about this). + +=head3 C<has_skip> + + if ( $result->has_skip ) { ... } + +Returns a boolean value indicating whether or not this test had a SKIP +directive. + +=head3 C<has_todo> + + if ( $result->has_todo ) { ... } + +Returns a boolean value indicating whether or not this test had a TODO +directive. + +Note that TODO tests I<always> pass. If you need to know whether or not +they really passed, check the C<is_actual_ok> method. + +=head3 C<in_todo> + + if ( $parser->in_todo ) { ... } + +True while the most recent result was a TODO. Becomes true before the +TODO result is returned and stays true until just before the next non- +TODO test is returned. + +=head1 TOTAL RESULTS + +After parsing the TAP, there are many methods available to let you dig through +the results and determine what is meaningful to you. + +=head2 Individual Results + +These results refer to individual tests which are run. + +=head3 C<passed> + + my @passed = $parser->passed; # the test numbers which passed + my $passed = $parser->passed; # the number of tests which passed + +This method lets you know which (or how many) tests passed. If a test failed +but had a TODO directive, it will be counted as a passed test. + +=cut + +sub passed { @{ shift->{passed} } } + +=head3 C<failed> + + my @failed = $parser->failed; # the test numbers which failed + my $failed = $parser->failed; # the number of tests which failed + +This method lets you know which (or how many) tests failed. If a test passed +but had a TODO directive, it will B<NOT> be counted as a failed test. + +=cut + +sub failed { @{ shift->{failed} } } + +=head3 C<actual_passed> + + # the test numbers which actually passed + my @actual_passed = $parser->actual_passed; + + # the number of tests which actually passed + my $actual_passed = $parser->actual_passed; + +This method lets you know which (or how many) tests actually passed, +regardless of whether or not a TODO directive was found. + +=cut + +sub actual_passed { @{ shift->{actual_passed} } } +*actual_ok = \&actual_passed; + +=head3 C<actual_ok> + +This method is a synonym for C<actual_passed>. + +=head3 C<actual_failed> + + # the test numbers which actually failed + my @actual_failed = $parser->actual_failed; + + # the number of tests which actually failed + my $actual_failed = $parser->actual_failed; + +This method lets you know which (or how many) tests actually failed, +regardless of whether or not a TODO directive was found. + +=cut + +sub actual_failed { @{ shift->{actual_failed} } } + +############################################################################## + +=head3 C<todo> + + my @todo = $parser->todo; # the test numbers with todo directives + my $todo = $parser->todo; # the number of tests with todo directives + +This method lets you know which (or how many) tests had TODO directives. + +=cut + +sub todo { @{ shift->{todo} } } + +=head3 C<todo_passed> + + # the test numbers which unexpectedly succeeded + my @todo_passed = $parser->todo_passed; + + # the number of tests which unexpectedly succeeded + my $todo_passed = $parser->todo_passed; + +This method lets you know which (or how many) tests actually passed but were +declared as "TODO" tests. + +=cut + +sub todo_passed { @{ shift->{todo_passed} } } + +############################################################################## + +=head3 C<todo_failed> + + # deprecated in favor of 'todo_passed'. This method was horribly misnamed. + +This was a badly misnamed method. It indicates which TODO tests unexpectedly +succeeded. Will now issue a warning and call C<todo_passed>. + +=cut + +sub todo_failed { + warn + '"todo_failed" is deprecated. Please use "todo_passed". See the docs.'; + goto &todo_passed; +} + +=head3 C<skipped> + + my @skipped = $parser->skipped; # the test numbers with SKIP directives + my $skipped = $parser->skipped; # the number of tests with SKIP directives + +This method lets you know which (or how many) tests had SKIP directives. + +=cut + +sub skipped { @{ shift->{skipped} } } + +=head2 Pragmas + +=head3 C<pragma> + +Get or set a pragma. To get the state of a pragma: + + if ( $p->pragma('strict') ) { + # be strict + } + +To set the state of a pragma: + + $p->pragma('strict', 1); # enable strict mode + +=cut + +sub pragma { + my ( $self, $pragma ) = splice @_, 0, 2; + + return $self->{pragma}->{$pragma} unless @_; + + if ( my $state = shift ) { + $self->{pragma}->{$pragma} = 1; + } + else { + delete $self->{pragma}->{$pragma}; + } + + return; +} + +=head3 C<pragmas> + +Get a list of all the currently enabled pragmas: + + my @pragmas_enabled = $p->pragmas; + +=cut + +sub pragmas { sort keys %{ shift->{pragma} || {} } } + +=head2 Summary Results + +These results are "meta" information about the total results of an individual +test program. + +=head3 C<plan> + + my $plan = $parser->plan; + +Returns the test plan, if found. + +=head3 C<good_plan> + +Deprecated. Use C<is_good_plan> instead. + +=cut + +sub good_plan { + warn 'good_plan() is deprecated. Please use "is_good_plan()"'; + goto &is_good_plan; +} + +############################################################################## + +=head3 C<is_good_plan> + + if ( $parser->is_good_plan ) { ... } + +Returns a boolean value indicating whether or not the number of tests planned +matches the number of tests run. + +B<Note:> this was formerly C<good_plan>. The latter method is deprecated and +will issue a warning. + +And since we're on that subject ... + +=head3 C<tests_planned> + + print $parser->tests_planned; + +Returns the number of tests planned, according to the plan. For example, a +plan of '1..17' will mean that 17 tests were planned. + +=head3 C<tests_run> + + print $parser->tests_run; + +Returns the number of tests which actually were run. Hopefully this will +match the number of C<< $parser->tests_planned >>. + +=head3 C<skip_all> + +Returns a true value (actually the reason for skipping) if all tests +were skipped. + +=head3 C<start_time> + +Returns the time when the Parser was created. + +=head3 C<end_time> + +Returns the time when the end of TAP input was seen. + +=head3 C<has_problems> + + if ( $parser->has_problems ) { + ... + } + +This is a 'catch-all' method which returns true if any tests have currently +failed, any TODO tests unexpectedly succeeded, or any parse errors occurred. + +=cut + +sub has_problems { + my $self = shift; + return + $self->failed + || $self->parse_errors + || ( !$self->ignore_exit && ( $self->wait || $self->exit ) ); +} + +=head3 C<version> + + $parser->version; + +Once the parser is done, this will return the version number for the +parsed TAP. Version numbers were introduced with TAP version 13 so if no +version number is found version 12 is assumed. + +=head3 C<exit> + + $parser->exit; + +Once the parser is done, this will return the exit status. If the parser ran +an executable, it returns the exit status of the executable. + +=head3 C<wait> + + $parser->wait; + +Once the parser is done, this will return the wait status. If the parser ran +an executable, it returns the wait status of the executable. Otherwise, this +mererely returns the C<exit> status. + +=head2 C<ignore_exit> + + $parser->ignore_exit(1); + +Tell the parser to ignore the exit status from the test when determining +whether the test passed. Normally tests with non-zero exit status are +considered to have failed even if all individual tests passed. In cases +where it is not possible to control the exit value of the test script +use this option to ignore it. + +=cut + +sub ignore_exit { shift->pragma( 'ignore_exit', @_ ) } + +=head3 C<parse_errors> + + my @errors = $parser->parse_errors; # the parser errors + my $errors = $parser->parse_errors; # the number of parser_errors + +Fortunately, all TAP output is perfect. In the event that it is not, this +method will return parser errors. Note that a junk line which the parser does +not recognize is C<not> an error. This allows this parser to handle future +versions of TAP. The following are all TAP errors reported by the parser: + +=over 4 + +=item * Misplaced plan + +The plan (for example, '1..5'), must only come at the beginning or end of the +TAP output. + +=item * No plan + +Gotta have a plan! + +=item * More than one plan + + 1..3 + ok 1 - input file opened + not ok 2 - first line of the input valid # todo some data + ok 3 read the rest of the file + 1..3 + +Right. Very funny. Don't do that. + +=item * Test numbers out of sequence + + 1..3 + ok 1 - input file opened + not ok 2 - first line of the input valid # todo some data + ok 2 read the rest of the file + +That last test line above should have the number '3' instead of '2'. + +Note that it's perfectly acceptable for some lines to have test numbers and +others to not have them. However, when a test number is found, it must be in +sequence. The following is also an error: + + 1..3 + ok 1 - input file opened + not ok - first line of the input valid # todo some data + ok 2 read the rest of the file + +But this is not: + + 1..3 + ok - input file opened + not ok - first line of the input valid # todo some data + ok 3 read the rest of the file + +=back + +=cut + +sub parse_errors { @{ shift->{parse_errors} } } + +sub _add_error { + my ( $self, $error ) = @_; + push @{ $self->{parse_errors} } => $error; + return $self; +} + +sub _make_state_table { + my $self = shift; + my %states; + my %planned_todo = (); + + # These transitions are defaults for all states + my %state_globals = ( + comment => {}, + bailout => {}, + yaml => {}, + version => { + act => sub { + $self->_add_error( + 'If TAP version is present it must be the first line of output' + ); + }, + }, + unknown => { + act => sub { + my $unk = shift; + if ( $self->pragma('strict') ) { + $self->_add_error( + 'Unknown TAP token: "' . $unk->raw . '"' ); + } + }, + }, + pragma => { + act => sub { + my ($pragma) = @_; + for my $pr ( $pragma->pragmas ) { + if ( $pr =~ /^ ([-+])(\w+) $/x ) { + $self->pragma( $2, $1 eq '+' ); + } + } + }, + }, + ); + + # Provides default elements for transitions + my %state_defaults = ( + plan => { + act => sub { + my ($plan) = @_; + $self->tests_planned( $plan->tests_planned ); + $self->plan( $plan->plan ); + if ( $plan->has_skip ) { + $self->skip_all( $plan->explanation + || '(no reason given)' ); + } + + $planned_todo{$_}++ for @{ $plan->todo_list }; + }, + }, + test => { + act => sub { + my ($test) = @_; + + my ( $number, $tests_run ) + = ( $test->number, ++$self->{tests_run} ); + + # Fake TODO state + if ( defined $number && delete $planned_todo{$number} ) { + $test->set_directive('TODO'); + } + + my $has_todo = $test->has_todo; + + $self->in_todo($has_todo); + if ( defined( my $tests_planned = $self->tests_planned ) ) { + if ( $tests_run > $tests_planned ) { + $test->is_unplanned(1); + } + } + + if ( defined $number ) { + if ( $number != $tests_run ) { + my $count = $tests_run; + $self->_add_error( "Tests out of sequence. Found " + . "($number) but expected ($count)" ); + } + } + else { + $test->_number( $number = $tests_run ); + } + + push @{ $self->{todo} } => $number if $has_todo; + push @{ $self->{todo_passed} } => $number + if $test->todo_passed; + push @{ $self->{skipped} } => $number + if $test->has_skip; + + push @{ $self->{ $test->is_ok ? 'passed' : 'failed' } } => + $number; + push @{ + $self->{ + $test->is_actual_ok + ? 'actual_passed' + : 'actual_failed' + } + } => $number; + }, + }, + yaml => { act => sub { }, }, + ); + + # Each state contains a hash the keys of which match a token type. For + # each token + # type there may be: + # act A coderef to run + # goto The new state to move to. Stay in this state if + # missing + # continue Goto the new state and run the new state for the + # current token + %states = ( + INIT => { + version => { + act => sub { + my ($version) = @_; + my $ver_num = $version->version; + if ( $ver_num <= $DEFAULT_TAP_VERSION ) { + my $ver_min = $DEFAULT_TAP_VERSION + 1; + $self->_add_error( + "Explicit TAP version must be at least " + . "$ver_min. Got version $ver_num" ); + $ver_num = $DEFAULT_TAP_VERSION; + } + if ( $ver_num > $MAX_TAP_VERSION ) { + $self->_add_error( + "TAP specified version $ver_num but " + . "we don't know about versions later " + . "than $MAX_TAP_VERSION" ); + $ver_num = $MAX_TAP_VERSION; + } + $self->version($ver_num); + $self->_grammar->set_version($ver_num); + }, + goto => 'PLAN' + }, + plan => { goto => 'PLANNED' }, + test => { goto => 'UNPLANNED' }, + }, + PLAN => { + plan => { goto => 'PLANNED' }, + test => { goto => 'UNPLANNED' }, + }, + PLANNED => { + test => { goto => 'PLANNED_AFTER_TEST' }, + plan => { + act => sub { + my ($version) = @_; + $self->_add_error( + 'More than one plan found in TAP output'); + }, + }, + }, + PLANNED_AFTER_TEST => { + test => { goto => 'PLANNED_AFTER_TEST' }, + plan => { act => sub { }, continue => 'PLANNED' }, + yaml => { goto => 'PLANNED' }, + }, + GOT_PLAN => { + test => { + act => sub { + my ($plan) = @_; + my $line = $self->plan; + $self->_add_error( + "Plan ($line) must be at the beginning " + . "or end of the TAP output" ); + $self->is_good_plan(0); + }, + continue => 'PLANNED' + }, + plan => { continue => 'PLANNED' }, + }, + UNPLANNED => { + test => { goto => 'UNPLANNED_AFTER_TEST' }, + plan => { goto => 'GOT_PLAN' }, + }, + UNPLANNED_AFTER_TEST => { + test => { act => sub { }, continue => 'UNPLANNED' }, + plan => { act => sub { }, continue => 'UNPLANNED' }, + yaml => { goto => 'PLANNED' }, + }, + ); + + # Apply globals and defaults to state table + for my $name ( keys %states ) { + + # Merge with globals + my $st = { %state_globals, %{ $states{$name} } }; + + # Add defaults + for my $next ( sort keys %{$st} ) { + if ( my $default = $state_defaults{$next} ) { + for my $def ( sort keys %{$default} ) { + $st->{$next}->{$def} ||= $default->{$def}; + } + } + } + + # Stuff back in table + $states{$name} = $st; + } + + return \%states; +} + +=head3 C<get_select_handles> + +Get an a list of file handles which can be passed to C<select> to +determine the readiness of this parser. + +=cut + +sub get_select_handles { shift->_iterator->get_select_handles } + +sub _grammar { + my $self = shift; + return $self->{_grammar} = shift if @_; + + return $self->{_grammar} ||= $self->make_grammar( + { iterator => $self->_iterator, + parser => $self, + version => $self->version + } + ); +} + +sub _iter { + my $self = shift; + my $iterator = $self->_iterator; + my $grammar = $self->_grammar; + my $spool = $self->_spool; + my $state = 'INIT'; + my $state_table = $self->_make_state_table; + + $self->start_time( $self->get_time ); + + # Make next_state closure + my $next_state = sub { + my $token = shift; + my $type = $token->type; + TRANS: { + my $state_spec = $state_table->{$state} + or die "Illegal state: $state"; + + if ( my $next = $state_spec->{$type} ) { + if ( my $act = $next->{act} ) { + $act->($token); + } + if ( my $cont = $next->{continue} ) { + $state = $cont; + redo TRANS; + } + elsif ( my $goto = $next->{goto} ) { + $state = $goto; + } + } + else { + confess("Unhandled token type: $type\n"); + } + } + return $token; + }; + + # Handle end of stream - which means either pop a block or finish + my $end_handler = sub { + $self->exit( $iterator->exit ); + $self->wait( $iterator->wait ); + $self->_finish; + return; + }; + + # Finally make the closure that we return. For performance reasons + # there are two versions of the returned function: one that handles + # callbacks and one that does not. + if ( $self->_has_callbacks ) { + return sub { + my $result = eval { $grammar->tokenize }; + $self->_add_error($@) if $@; + + if ( defined $result ) { + $result = $next_state->($result); + + if ( my $code = $self->_callback_for( $result->type ) ) { + $_->($result) for @{$code}; + } + else { + $self->_make_callback( 'ELSE', $result ); + } + + $self->_make_callback( 'ALL', $result ); + + # Echo TAP to spool file + print {$spool} $result->raw, "\n" if $spool; + } + else { + $result = $end_handler->(); + $self->_make_callback( 'EOF', $self ) + unless defined $result; + } + + return $result; + }; + } # _has_callbacks + else { + return sub { + my $result = eval { $grammar->tokenize }; + $self->_add_error($@) if $@; + + if ( defined $result ) { + $result = $next_state->($result); + + # Echo TAP to spool file + print {$spool} $result->raw, "\n" if $spool; + } + else { + $result = $end_handler->(); + } + + return $result; + }; + } # no callbacks +} + +sub _finish { + my $self = shift; + + $self->end_time( $self->get_time ); + + # Avoid leaks + $self->_iterator(undef); + $self->_grammar(undef); + + # If we just delete the iter we won't get a fault if it's recreated. + # Instead we set it to a sub that returns an infinite + # stream of undef. This segfaults on 5.5.4, presumably because + # we're still executing the closure that gets replaced and it hasn't + # been protected with a refcount. + $self->{_iter} = sub {return} + if $] >= 5.006; + + # sanity checks + if ( !$self->plan ) { + $self->_add_error('No plan found in TAP output'); + } + else { + $self->is_good_plan(1) unless defined $self->is_good_plan; + } + if ( $self->tests_run != ( $self->tests_planned || 0 ) ) { + $self->is_good_plan(0); + if ( defined( my $planned = $self->tests_planned ) ) { + my $ran = $self->tests_run; + $self->_add_error( + "Bad plan. You planned $planned tests but ran $ran."); + } + } + if ( $self->tests_run != ( $self->passed + $self->failed ) ) { + + # this should never happen + my $actual = $self->tests_run; + my $passed = $self->passed; + my $failed = $self->failed; + $self->_croak( "Panic: planned test count ($actual) did not equal " + . "sum of passed ($passed) and failed ($failed) tests!" ); + } + + $self->is_good_plan(0) unless defined $self->is_good_plan; + return $self; +} + +=head3 C<delete_spool> + +Delete and return the spool. + + my $fh = $parser->delete_spool; + +=cut + +sub delete_spool { + my $self = shift; + + return delete $self->{_spool}; +} + +############################################################################## + +=head1 CALLBACKS + +As mentioned earlier, a "callback" key may be added to the +C<TAP::Parser> constructor. If present, each callback corresponding to a +given result type will be called with the result as the argument if the +C<run> method is used. The callback is expected to be a subroutine +reference (or anonymous subroutine) which is invoked with the parser +result as its argument. + + my %callbacks = ( + test => \&test_callback, + plan => \&plan_callback, + comment => \&comment_callback, + bailout => \&bailout_callback, + unknown => \&unknown_callback, + ); + + my $aggregator = TAP::Parser::Aggregator->new; + for my $file ( @test_files ) { + my $parser = TAP::Parser->new( + { + source => $file, + callbacks => \%callbacks, + } + ); + $parser->run; + $aggregator->add( $file, $parser ); + } + +Callbacks may also be added like this: + + $parser->callback( test => \&test_callback ); + $parser->callback( plan => \&plan_callback ); + +The following keys allowed for callbacks. These keys are case-sensitive. + +=over 4 + +=item * C<test> + +Invoked if C<< $result->is_test >> returns true. + +=item * C<version> + +Invoked if C<< $result->is_version >> returns true. + +=item * C<plan> + +Invoked if C<< $result->is_plan >> returns true. + +=item * C<comment> + +Invoked if C<< $result->is_comment >> returns true. + +=item * C<bailout> + +Invoked if C<< $result->is_unknown >> returns true. + +=item * C<yaml> + +Invoked if C<< $result->is_yaml >> returns true. + +=item * C<unknown> + +Invoked if C<< $result->is_unknown >> returns true. + +=item * C<ELSE> + +If a result does not have a callback defined for it, this callback will +be invoked. Thus, if all of the previous result types are specified as +callbacks, this callback will I<never> be invoked. + +=item * C<ALL> + +This callback will always be invoked and this will happen for each +result after one of the above callbacks is invoked. For example, if +L<Term::ANSIColor> is loaded, you could use the following to color your +test output: + + my %callbacks = ( + test => sub { + my $test = shift; + if ( $test->is_ok && not $test->directive ) { + # normal passing test + print color 'green'; + } + elsif ( !$test->is_ok ) { # even if it's TODO + print color 'white on_red'; + } + elsif ( $test->has_skip ) { + print color 'white on_blue'; + + } + elsif ( $test->has_todo ) { + print color 'white'; + } + }, + ELSE => sub { + # plan, comment, and so on (anything which isn't a test line) + print color 'black on_white'; + }, + ALL => sub { + # now print them + print shift->as_string; + print color 'reset'; + print "\n"; + }, + ); + +=item * C<EOF> + +Invoked when there are no more lines to be parsed. Since there is no +accompanying L<TAP::Parser::Result> object the C<TAP::Parser> object is +passed instead. + +=back + +=head1 TAP GRAMMAR + +If you're looking for an EBNF grammar, see L<TAP::Parser::Grammar>. + +=head1 BACKWARDS COMPATABILITY + +The Perl-QA list attempted to ensure backwards compatability with +L<Test::Harness>. However, there are some minor differences. + +=head2 Differences + +=over 4 + +=item * TODO plans + +A little-known feature of L<Test::Harness> is that it supported TODO +lists in the plan: + + 1..2 todo 2 + ok 1 - We have liftoff + not ok 2 - Anti-gravity device activated + +Under L<Test::Harness>, test number 2 would I<pass> because it was +listed as a TODO test on the plan line. However, we are not aware of +anyone actually using this feature and hard-coding test numbers is +discouraged because it's very easy to add a test and break the test +number sequence. This makes test suites very fragile. Instead, the +following should be used: + + 1..2 + ok 1 - We have liftoff + not ok 2 - Anti-gravity device activated # TODO + +=item * 'Missing' tests + +It rarely happens, but sometimes a harness might encounter +'missing tests: + + ok 1 + ok 2 + ok 15 + ok 16 + ok 17 + +L<Test::Harness> would report tests 3-14 as having failed. For the +C<TAP::Parser>, these tests are not considered failed because they've +never run. They're reported as parse failures (tests out of sequence). + +=back + +=head1 SUBCLASSING + +If you find you need to provide custom functionality (as you would have using +L<Test::Harness::Straps>), you're in luck: C<TAP::Parser> and friends are +designed to be easily plugged-into and/or subclassed. + +Before you start, it's important to know a few things: + +=over 2 + +=item 1 + +All C<TAP::*> objects inherit from L<TAP::Object>. + +=item 2 + +Many C<TAP::*> classes have a I<SUBCLASSING> section to guide you. + +=item 3 + +Note that C<TAP::Parser> is designed to be the central 'maker' - ie: it is +responsible for creating most new objects in the C<TAP::Parser::*> namespace. + +This makes it possible for you to have a single point of configuring what +subclasses should be used, which in turn means that in many cases you'll find +you only need to sub-class one of the parser's components. + +The exception to this rule are I<SourceHandlers> & I<Iterators>, but those are +both created with customizeable I<IteratorFactory>. + +=item 4 + +By subclassing, you may end up overriding undocumented methods. That's not +a bad thing per se, but be forewarned that undocumented methods may change +without warning from one release to the next - we cannot guarantee backwards +compatability. If any I<documented> method needs changing, it will be +deprecated first, and changed in a later release. + +=back + +=head2 Parser Components + +=head3 Sources + +A TAP parser consumes input from a single I<raw source> of TAP, which could come +from anywhere (a file, an executable, a database, an io handle, a uri, etc..). +The source gets bundled up in a L<TAP::Parser::Source> object which gathers some +meta data about it. The parser then uses a L<TAP::Parser::IteratorFactory> to +determine which L<TAP::Parser::SourceHandler> to use to turn the raw source +into a stream of TAP by way of L</Iterators>. + +If you simply want C<TAP::Parser> to handle a new source of TAP you probably +don't need to subclass C<TAP::Parser> itself. Rather, you'll need to create a +new L<TAP::Parser::SourceHandler> class, and just plug it into the parser using +the I<sources> param to L</new>. Before you start writing one, read through +L<TAP::Parser::IteratorFactory> to get a feel for how the system works first. + +If you find you really need to use your own iterator factory you can still do +so without sub-classing C<TAP::Parser> by setting L</iterator_factory_class>. + +If you just need to customize the objects on creation, subclass L<TAP::Parser> +and override L</make_iterator_factory>. + +Note that L</make_source> & L</make_perl_source> have been I<DEPRECATED> and +are now removed. + +=head3 Iterators + +A TAP parser uses I<iterators> to loop through the I<stream> of TAP read in +from the I<source> it was given. There are a few types of Iterators available +by default, all sub-classes of L<TAP::Parser::Iterator>. Choosing which +iterator to use is the responsibility of the I<siterator factory>, though it +simply delegates to the I<Source Handler> it uses. + +If you're writing your own L<TAP::Parser::SourceHandler>, you may need to +create your own iterators too. If so you'll need to subclass +L<TAP::Parser::Iterator>. + +Note that L</make_iterator> has been I<DEPRECATED> and is now removed. + +=head3 Results + +A TAP parser creates L<TAP::Parser::Result>s as it iterates through the +input I<stream>. There are quite a few result types available; choosing +which class to use is the responsibility of the I<result factory>. + +To create your own result types you have two options: + +=over 2 + +=item option 1 + +Subclass L<TAP::Parser::Result> and register your new result type/class with +the default L<TAP::Parser::ResultFactory>. + +=item option 2 + +Subclass L<TAP::Parser::ResultFactory> itself and implement your own +L<TAP::Parser::Result> creation logic. Then you'll need to customize the +class used by your parser by setting the C<result_factory_class> parameter. +See L</new> for more details. + +=back + +If you need to customize the objects on creation, subclass L<TAP::Parser> and +override L</make_result>. + +=head3 Grammar + +L<TAP::Parser::Grammar> is the heart of the parser - it tokenizes the TAP +input I<stream> and produces results. If you need to customize its behaviour +you should probably familiarize yourself with the source first. Enough +lecturing. + +Subclass L<TAP::Parser::Grammar> and customize your parser by setting the +C<grammar_class> parameter. See L</new> for more details. + +If you need to customize the objects on creation, subclass L<TAP::Parser> and +override L</make_grammar> + +=head1 ACKNOWLEDGEMENTS + +All of the following have helped. Bug reports, patches, (im)moral +support, or just words of encouragement have all been forthcoming. + +=over 4 + +=item * Michael Schwern + +=item * Andy Lester + +=item * chromatic + +=item * GEOFFR + +=item * Shlomi Fish + +=item * Torsten Schoenfeld + +=item * Jerry Gay + +=item * Aristotle + +=item * Adam Kennedy + +=item * Yves Orton + +=item * Adrian Howard + +=item * Sean & Lil + +=item * Andreas J. Koenig + +=item * Florian Ragwitz + +=item * Corion + +=item * Mark Stosberg + +=item * Matt Kraai + +=item * David Wheeler + +=item * Alex Vandiver + +=item * Cosimo Streppone + +=back + +=head1 AUTHORS + +Curtis "Ovid" Poe <ovid@cpan.org> + +Andy Armstong <andy@hexten.net> + +Eric Wilhelm @ <ewilhelm at cpan dot org> + +Michael Peters <mpeters at plusthree dot com> + +Leif Eriksen <leif dot eriksen at bigpond dot com> + +Steve Purkis <spurkis@cpan.org> + +Nicholas Clark <nick@ccl4.org> + +Lee Johnson <notfadeaway at btinternet dot com> + +=head1 BUGS + +Please report any bugs or feature requests to +C<bug-test-harness@rt.cpan.org>, or through the web interface at +L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Harness>. +We will be notified, and then you'll automatically be notified of +progress on your bug as we make changes. + +Obviously, bugs which include patches are best. If you prefer, you can +patch against bleed by via anonymous checkout of the latest version: + + git clone git://github.com/AndyA/Test-Harness.git + +=head1 COPYRIGHT & LICENSE + +Copyright 2006-2008 Curtis "Ovid" Poe, all rights reserved. + +This program is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. + +=cut + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Aggregator.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Aggregator.pm new file mode 100755 index 00000000000..4df2ac34cc4 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Aggregator.pm @@ -0,0 +1,416 @@ +package TAP::Parser::Aggregator; + +use strict; +use Benchmark; +use vars qw($VERSION @ISA); + +use TAP::Object (); + +@ISA = qw(TAP::Object); + +=head1 NAME + +TAP::Parser::Aggregator - Aggregate TAP::Parser results + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Aggregator; + + my $aggregate = TAP::Parser::Aggregator->new; + $aggregate->add( 't/00-load.t', $load_parser ); + $aggregate->add( 't/10-lex.t', $lex_parser ); + + my $summary = <<'END_SUMMARY'; + Passed: %s + Failed: %s + Unexpectedly succeeded: %s + END_SUMMARY + printf $summary, + scalar $aggregate->passed, + scalar $aggregate->failed, + scalar $aggregate->todo_passed; + +=head1 DESCRIPTION + +C<TAP::Parser::Aggregator> collects parser objects and allows +reporting/querying their aggregate results. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $aggregate = TAP::Parser::Aggregator->new; + +Returns a new C<TAP::Parser::Aggregator> object. + +=cut + +# new() implementation supplied by TAP::Object + +my %SUMMARY_METHOD_FOR; + +BEGIN { # install summary methods + %SUMMARY_METHOD_FOR = map { $_ => $_ } qw( + failed + parse_errors + passed + skipped + todo + todo_passed + total + wait + exit + ); + $SUMMARY_METHOD_FOR{total} = 'tests_run'; + $SUMMARY_METHOD_FOR{planned} = 'tests_planned'; + + for my $method ( keys %SUMMARY_METHOD_FOR ) { + next if 'total' eq $method; + no strict 'refs'; + *$method = sub { + my $self = shift; + return wantarray + ? @{ $self->{"descriptions_for_$method"} } + : $self->{$method}; + }; + } +} # end install summary methods + +sub _initialize { + my ($self) = @_; + $self->{parser_for} = {}; + $self->{parse_order} = []; + for my $summary ( keys %SUMMARY_METHOD_FOR ) { + $self->{$summary} = 0; + next if 'total' eq $summary; + $self->{"descriptions_for_$summary"} = []; + } + return $self; +} + +############################################################################## + +=head2 Instance Methods + +=head3 C<add> + + $aggregate->add( $description => $parser ); + +The C<$description> is usually a test file name (but only by +convention.) It is used as a unique identifier (see e.g. +L<"parsers">.) Reusing a description is a fatal error. + +The C<$parser> is a L<TAP::Parser|TAP::Parser> object. + +=cut + +sub add { + my ( $self, $description, $parser ) = @_; + if ( exists $self->{parser_for}{$description} ) { + $self->_croak( "You already have a parser for ($description)." + . " Perhaps you have run the same test twice." ); + } + push @{ $self->{parse_order} } => $description; + $self->{parser_for}{$description} = $parser; + + while ( my ( $summary, $method ) = each %SUMMARY_METHOD_FOR ) { + + # Slightly nasty. Instead we should maybe have 'cooked' accessors + # for results that may be masked by the parser. + next + if ( $method eq 'exit' || $method eq 'wait' ) + && $parser->ignore_exit; + + if ( my $count = $parser->$method() ) { + $self->{$summary} += $count; + push @{ $self->{"descriptions_for_$summary"} } => $description; + } + } + + return $self; +} + +############################################################################## + +=head3 C<parsers> + + my $count = $aggregate->parsers; + my @parsers = $aggregate->parsers; + my @parsers = $aggregate->parsers(@descriptions); + +In scalar context without arguments, this method returns the number of parsers +aggregated. In list context without arguments, returns the parsers in the +order they were added. + +If C<@descriptions> is given, these correspond to the keys used in each +call to the add() method. Returns an array of the requested parsers (in +the requested order) in list context or an array reference in scalar +context. + +Requesting an unknown identifier is a fatal error. + +=cut + +sub parsers { + my $self = shift; + return $self->_get_parsers(@_) if @_; + my $descriptions = $self->{parse_order}; + my @parsers = @{ $self->{parser_for} }{@$descriptions}; + + # Note: Because of the way context works, we must assign the parsers to + # the @parsers array or else this method does not work as documented. + return @parsers; +} + +sub _get_parsers { + my ( $self, @descriptions ) = @_; + my @parsers; + for my $description (@descriptions) { + $self->_croak("A parser for ($description) could not be found") + unless exists $self->{parser_for}{$description}; + push @parsers => $self->{parser_for}{$description}; + } + return wantarray ? @parsers : \@parsers; +} + +=head3 C<descriptions> + +Get an array of descriptions in the order in which they were added to +the aggregator. + +=cut + +sub descriptions { @{ shift->{parse_order} || [] } } + +=head3 C<start> + +Call C<start> immediately before adding any results to the aggregator. +Among other times it records the start time for the test run. + +=cut + +sub start { + my $self = shift; + $self->{start_time} = Benchmark->new; +} + +=head3 C<stop> + +Call C<stop> immediately after adding all test results to the aggregator. + +=cut + +sub stop { + my $self = shift; + $self->{end_time} = Benchmark->new; +} + +=head3 C<elapsed> + +Elapsed returns a L<Benchmark> object that represents the running time +of the aggregated tests. In order for C<elapsed> to be valid you must +call C<start> before running the tests and C<stop> immediately +afterwards. + +=cut + +sub elapsed { + my $self = shift; + + require Carp; + Carp::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} ); +} + +=head3 C<elapsed_timestr> + +Returns a formatted string representing the runtime returned by +C<elapsed()>. This lets the caller not worry about Benchmark. + +=cut + +sub elapsed_timestr { + my $self = shift; + + my $elapsed = $self->elapsed; + + return timestr($elapsed); +} + +=head3 C<all_passed> + +Return true if all the tests passed and no parse errors were detected. + +=cut + +sub all_passed { + my $self = shift; + return + $self->total + && $self->total == $self->passed + && !$self->has_errors; +} + +=head3 C<get_status> + +Get a single word describing the status of the aggregated tests. +Depending on the outcome of the tests returns 'PASS', 'FAIL' or +'NOTESTS'. This token is understood by L<CPAN::Reporter>. + +=cut + +sub get_status { + my $self = shift; + + my $total = $self->total; + my $passed = $self->passed; + + return + ( $self->has_errors || $total != $passed ) ? 'FAIL' + : $total ? 'PASS' + : 'NOTESTS'; +} + +############################################################################## + +=head2 Summary methods + +Each of the following methods will return the total number of corresponding +tests if called in scalar context. If called in list context, returns the +descriptions of the parsers which contain the corresponding tests (see C<add> +for an explanation of description. + +=over 4 + +=item * failed + +=item * parse_errors + +=item * passed + +=item * planned + +=item * skipped + +=item * todo + +=item * todo_passed + +=item * wait + +=item * exit + +=back + +For example, to find out how many tests unexpectedly succeeded (TODO tests +which passed when they shouldn't): + + my $count = $aggregate->todo_passed; + my @descriptions = $aggregate->todo_passed; + +Note that C<wait> and C<exit> are the totals of the wait and exit +statuses of each of the tests. These values are totalled only to provide +a true value if any of them are non-zero. + +=cut + +############################################################################## + +=head3 C<total> + + my $tests_run = $aggregate->total; + +Returns the total number of tests run. + +=cut + +sub total { shift->{total} } + +############################################################################## + +=head3 C<has_problems> + + if ( $parser->has_problems ) { + ... + } + +Identical to C<has_errors>, but also returns true if any TODO tests +unexpectedly succeeded. This is more akin to "warnings". + +=cut + +sub has_problems { + my $self = shift; + return $self->todo_passed + || $self->has_errors; +} + +############################################################################## + +=head3 C<has_errors> + + if ( $parser->has_errors ) { + ... + } + +Returns true if I<any> of the parsers failed. This includes: + +=over 4 + +=item * Failed tests + +=item * Parse errors + +=item * Bad exit or wait status + +=back + +=cut + +sub has_errors { + my $self = shift; + return + $self->failed + || $self->parse_errors + || $self->exit + || $self->wait; +} + +############################################################################## + +=head3 C<todo_failed> + + # deprecated in favor of 'todo_passed'. This method was horribly misnamed. + +This was a badly misnamed method. It indicates which TODO tests unexpectedly +succeeded. Will now issue a warning and call C<todo_passed>. + +=cut + +sub todo_failed { + warn + '"todo_failed" is deprecated. Please use "todo_passed". See the docs.'; + goto &todo_passed; +} + +=head1 See Also + +L<TAP::Parser> + +L<TAP::Harness> + +=cut + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Grammar.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Grammar.pm new file mode 100755 index 00000000000..81f4d3478ef --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Grammar.pm @@ -0,0 +1,582 @@ +package TAP::Parser::Grammar; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); +use TAP::Parser::ResultFactory (); +use TAP::Parser::YAMLish::Reader (); + +@ISA = qw(TAP::Object); + +=head1 NAME + +TAP::Parser::Grammar - A grammar for the Test Anything Protocol. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Grammar; + my $grammar = $self->make_grammar({ + iterator => $tap_parser_iterator, + parser => $tap_parser, + version => 12, + }); + + my $result = $grammar->tokenize; + +=head1 DESCRIPTION + +C<TAP::Parser::Grammar> tokenizes lines from a L<TAP::Parser::Iterator> and +constructs L<TAP::Parser::Result> subclasses to represent the tokens. + +Do not attempt to use this class directly. It won't make sense. It's mainly +here to ensure that we will be able to have pluggable grammars when TAP is +expanded at some future date (plus, this stuff was really cluttering the +parser). + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $grammar = TAP::Parser::Grammar->new({ + iterator => $iterator, + parser => $parser, + version => $version, + }); + +Returns L<TAP::Parser> grammar object that will parse the TAP stream from the +specified iterator. Both C<iterator> and C<parser> are required arguments. +If C<version> is not set it defaults to C<12> (see L</set_version> for more +details). + +=cut + +# new() implementation supplied by TAP::Object +sub _initialize { + my ( $self, $args ) = @_; + $self->{iterator} = $args->{iterator}; # TODO: accessor + $self->{iterator} ||= $args->{stream}; # deprecated + $self->{parser} = $args->{parser}; # TODO: accessor + $self->set_version( $args->{version} || 12 ); + return $self; +} + +my %language_for; + +{ + + # XXX the 'not' and 'ok' might be on separate lines in VMS ... + my $ok = qr/(?:not )?ok\b/; + my $num = qr/\d+/; + + my %v12 = ( + version => { + syntax => qr/^TAP\s+version\s+(\d+)\s*\z/i, + handler => sub { + my ( $self, $line ) = @_; + my $version = $1; + return $self->_make_version_token( $line, $version, ); + }, + }, + plan => { + syntax => qr/^1\.\.(\d+)\s*(.*)\z/, + handler => sub { + my ( $self, $line ) = @_; + my ( $tests_planned, $tail ) = ( $1, $2 ); + my $explanation = undef; + my $skip = ''; + + if ( $tail =~ /^todo((?:\s+\d+)+)/ ) { + my @todo = split /\s+/, _trim($1); + return $self->_make_plan_token( + $line, $tests_planned, 'TODO', + '', \@todo + ); + } + elsif ( 0 == $tests_planned ) { + $skip = 'SKIP'; + + # If we can't match # SKIP the directive should be undef. + ($explanation) = $tail =~ /^#\s*SKIP\S*\s+(.*)/i; + } + elsif ( $tail !~ /^\s*$/ ) { + return $self->_make_unknown_token($line); + } + + $explanation = '' unless defined $explanation; + + return $self->_make_plan_token( + $line, $tests_planned, $skip, + $explanation, [] + ); + + }, + }, + + # An optimization to handle the most common test lines without + # directives. + simple_test => { + syntax => qr/^($ok) \ ($num) (?:\ ([^#]+))? \z/x, + handler => sub { + my ( $self, $line ) = @_; + my ( $ok, $num, $desc ) = ( $1, $2, $3 ); + + return $self->_make_test_token( + $line, $ok, $num, + $desc + ); + }, + }, + test => { + syntax => qr/^($ok) \s* ($num)? \s* (.*) \z/x, + handler => sub { + my ( $self, $line ) = @_; + my ( $ok, $num, $desc ) = ( $1, $2, $3 ); + my ( $dir, $explanation ) = ( '', '' ); + if ($desc =~ m/^ ( [^\\\#]* (?: \\. [^\\\#]* )* ) + \# \s* (SKIP|TODO) \b \s* (.*) $/ix + ) + { + ( $desc, $dir, $explanation ) = ( $1, $2, $3 ); + } + return $self->_make_test_token( + $line, $ok, $num, $desc, + $dir, $explanation + ); + }, + }, + comment => { + syntax => qr/^#(.*)/, + handler => sub { + my ( $self, $line ) = @_; + my $comment = $1; + return $self->_make_comment_token( $line, $comment ); + }, + }, + bailout => { + syntax => qr/^Bail out!\s*(.*)/, + handler => sub { + my ( $self, $line ) = @_; + my $explanation = $1; + return $self->_make_bailout_token( + $line, + $explanation + ); + }, + }, + ); + + my %v13 = ( + %v12, + plan => { + syntax => qr/^1\.\.(\d+)(?:\s*#\s*SKIP\b(.*))?\z/i, + handler => sub { + my ( $self, $line ) = @_; + my ( $tests_planned, $explanation ) = ( $1, $2 ); + my $skip + = ( 0 == $tests_planned || defined $explanation ) + ? 'SKIP' + : ''; + $explanation = '' unless defined $explanation; + return $self->_make_plan_token( + $line, $tests_planned, $skip, + $explanation, [] + ); + }, + }, + yaml => { + syntax => qr/^ (\s+) (---.*) $/x, + handler => sub { + my ( $self, $line ) = @_; + my ( $pad, $marker ) = ( $1, $2 ); + return $self->_make_yaml_token( $pad, $marker ); + }, + }, + pragma => { + syntax => + qr/^ pragma \s+ ( [-+] \w+ \s* (?: , \s* [-+] \w+ \s* )* ) $/x, + handler => sub { + my ( $self, $line ) = @_; + my $pragmas = $1; + return $self->_make_pragma_token( $line, $pragmas ); + }, + }, + ); + + %language_for = ( + '12' => { + tokens => \%v12, + }, + '13' => { + tokens => \%v13, + setup => sub { + shift->{iterator}->handle_unicode; + }, + }, + ); +} + +############################################################################## + +=head2 Instance Methods + +=head3 C<set_version> + + $grammar->set_version(13); + +Tell the grammar which TAP syntax version to support. The lowest +supported version is 12. Although 'TAP version' isn't valid version 12 +syntax it is accepted so that higher version numbers may be parsed. + +=cut + +sub set_version { + my $self = shift; + my $version = shift; + + if ( my $language = $language_for{$version} ) { + $self->{version} = $version; + $self->{tokens} = $language->{tokens}; + + if ( my $setup = $language->{setup} ) { + $self->$setup(); + } + + $self->_order_tokens; + } + else { + require Carp; + Carp::croak("Unsupported syntax version: $version"); + } +} + +# Optimization to put the most frequent tokens first. +sub _order_tokens { + my $self = shift; + + my %copy = %{ $self->{tokens} }; + my @ordered_tokens = grep {defined} + map { delete $copy{$_} } qw( simple_test test comment plan ); + push @ordered_tokens, values %copy; + + $self->{ordered_tokens} = \@ordered_tokens; +} + +############################################################################## + +=head3 C<tokenize> + + my $token = $grammar->tokenize; + +This method will return a L<TAP::Parser::Result> object representing the +current line of TAP. + +=cut + +sub tokenize { + my $self = shift; + + my $line = $self->{iterator}->next; + unless ( defined $line ) { + delete $self->{parser}; # break circular ref + return; + } + + my $token; + + for my $token_data ( @{ $self->{ordered_tokens} } ) { + if ( $line =~ $token_data->{syntax} ) { + my $handler = $token_data->{handler}; + $token = $self->$handler($line); + last; + } + } + + $token = $self->_make_unknown_token($line) unless $token; + + return $self->{parser}->make_result($token); +} + +############################################################################## + +=head3 C<token_types> + + my @types = $grammar->token_types; + +Returns the different types of tokens which this grammar can parse. + +=cut + +sub token_types { + my $self = shift; + return keys %{ $self->{tokens} }; +} + +############################################################################## + +=head3 C<syntax_for> + + my $syntax = $grammar->syntax_for($token_type); + +Returns a pre-compiled regular expression which will match a chunk of TAP +corresponding to the token type. For example (not that you should really pay +attention to this, C<< $grammar->syntax_for('comment') >> will return +C<< qr/^#(.*)/ >>. + +=cut + +sub syntax_for { + my ( $self, $type ) = @_; + return $self->{tokens}->{$type}->{syntax}; +} + +############################################################################## + +=head3 C<handler_for> + + my $handler = $grammar->handler_for($token_type); + +Returns a code reference which, when passed an appropriate line of TAP, +returns the lexed token corresponding to that line. As a result, the basic +TAP parsing loop looks similar to the following: + + my @tokens; + my $grammar = TAP::Grammar->new; + LINE: while ( defined( my $line = $parser->_next_chunk_of_tap ) ) { + for my $type ( $grammar->token_types ) { + my $syntax = $grammar->syntax_for($type); + if ( $line =~ $syntax ) { + my $handler = $grammar->handler_for($type); + push @tokens => $grammar->$handler($line); + next LINE; + } + } + push @tokens => $grammar->_make_unknown_token($line); + } + +=cut + +sub handler_for { + my ( $self, $type ) = @_; + return $self->{tokens}->{$type}->{handler}; +} + +sub _make_version_token { + my ( $self, $line, $version ) = @_; + return { + type => 'version', + raw => $line, + version => $version, + }; +} + +sub _make_plan_token { + my ( $self, $line, $tests_planned, $directive, $explanation, $todo ) = @_; + + if ( $directive eq 'SKIP' + && 0 != $tests_planned + && $self->{version} < 13 ) + { + warn + "Specified SKIP directive in plan but more than 0 tests ($line)\n"; + } + + return { + type => 'plan', + raw => $line, + tests_planned => $tests_planned, + directive => $directive, + explanation => _trim($explanation), + todo_list => $todo, + }; +} + +sub _make_test_token { + my ( $self, $line, $ok, $num, $desc, $dir, $explanation ) = @_; + return { + ok => $ok, + test_num => $num, + description => _trim($desc), + directive => ( defined $dir ? uc $dir : '' ), + explanation => _trim($explanation), + raw => $line, + type => 'test', + }; +} + +sub _make_unknown_token { + my ( $self, $line ) = @_; + return { + raw => $line, + type => 'unknown', + }; +} + +sub _make_comment_token { + my ( $self, $line, $comment ) = @_; + return { + type => 'comment', + raw => $line, + comment => _trim($comment) + }; +} + +sub _make_bailout_token { + my ( $self, $line, $explanation ) = @_; + return { + type => 'bailout', + raw => $line, + bailout => _trim($explanation) + }; +} + +sub _make_yaml_token { + my ( $self, $pad, $marker ) = @_; + + my $yaml = TAP::Parser::YAMLish::Reader->new; + + my $iterator = $self->{iterator}; + + # Construct a reader that reads from our input stripping leading + # spaces from each line. + my $leader = length($pad); + my $strip = qr{ ^ (\s{$leader}) (.*) $ }x; + my @extra = ($marker); + my $reader = sub { + return shift @extra if @extra; + my $line = $iterator->next; + return $2 if $line =~ $strip; + return; + }; + + my $data = $yaml->read($reader); + + # Reconstitute input. This is convoluted. Maybe we should just + # record it on the way in... + chomp( my $raw = $yaml->get_raw ); + $raw =~ s/^/$pad/mg; + + return { + type => 'yaml', + raw => $raw, + data => $data + }; +} + +sub _make_pragma_token { + my ( $self, $line, $pragmas ) = @_; + return { + type => 'pragma', + raw => $line, + pragmas => [ split /\s*,\s*/, _trim($pragmas) ], + }; +} + +sub _trim { + my $data = shift; + + return '' unless defined $data; + + $data =~ s/^\s+//; + $data =~ s/\s+$//; + return $data; +} + +1; + +=head1 TAP GRAMMAR + +B<NOTE:> This grammar is slightly out of date. There's still some discussion +about it and a new one will be provided when we have things better defined. + +The L<TAP::Parser> does not use a formal grammar because TAP is essentially a +stream-based protocol. In fact, it's quite legal to have an infinite stream. +For the same reason that we don't apply regexes to streams, we're not using a +formal grammar here. Instead, we parse the TAP in lines. + +For purposes for forward compatability, any result which does not match the +following grammar is currently referred to as +L<TAP::Parser::Result::Unknown>. It is I<not> a parse error. + +A formal grammar would look similar to the following: + + (* + For the time being, I'm cheating on the EBNF by allowing + certain terms to be defined by POSIX character classes by + using the following syntax: + + digit ::= [:digit:] + + As far as I am aware, that's not valid EBNF. Sue me. I + didn't know how to write "char" otherwise (Unicode issues). + Suggestions welcome. + *) + + tap ::= version? { comment | unknown } leading_plan lines + | + lines trailing_plan {comment} + + version ::= 'TAP version ' positiveInteger {positiveInteger} "\n" + + leading_plan ::= plan skip_directive? "\n" + + trailing_plan ::= plan "\n" + + plan ::= '1..' nonNegativeInteger + + lines ::= line {line} + + line ::= (comment | test | unknown | bailout ) "\n" + + test ::= status positiveInteger? description? directive? + + status ::= 'not '? 'ok ' + + description ::= (character - (digit | '#')) {character - '#'} + + directive ::= todo_directive | skip_directive + + todo_directive ::= hash_mark 'TODO' ' ' {character} + + skip_directive ::= hash_mark 'SKIP' ' ' {character} + + comment ::= hash_mark {character} + + hash_mark ::= '#' {' '} + + bailout ::= 'Bail out!' {character} + + unknown ::= { (character - "\n") } + + (* POSIX character classes and other terminals *) + + digit ::= [:digit:] + character ::= ([:print:] - "\n") + positiveInteger ::= ( digit - '0' ) {digit} + nonNegativeInteger ::= digit {digit} + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +If you I<really> want to subclass L<TAP::Parser>'s grammar the best thing to +do is read through the code. There's no easy way of summarizing it here. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Iterator>, +L<TAP::Parser::Result>, + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator.pm new file mode 100755 index 00000000000..34c0729727c --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator.pm @@ -0,0 +1,165 @@ +package TAP::Parser::Iterator; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); + +@ISA = qw(TAP::Object); + +=head1 NAME + +TAP::Parser::Iterator - Base class for TAP source iterators + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + # to subclass: + use vars qw(@ISA); + use TAP::Parser::Iterator (); + @ISA = qw(TAP::Parser::Iterator); + sub _initialize { + # see TAP::Object... + } + + sub next_raw { ... } + sub wait { ... } + sub exit { ... } + +=head1 DESCRIPTION + +This is a simple iterator base class that defines L<TAP::Parser>'s iterator +API. Iterators are typically created from L<TAP::Parser::SourceHandler>s. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +Create an iterator. Provided by L<TAP::Object>. + +=head2 Instance Methods + +=head3 C<next> + + while ( my $item = $iter->next ) { ... } + +Iterate through it, of course. + +=head3 C<next_raw> + +B<Note:> this method is abstract and should be overridden. + + while ( my $item = $iter->next_raw ) { ... } + +Iterate raw input without applying any fixes for quirky input syntax. + +=cut + +sub next { + my $self = shift; + my $line = $self->next_raw; + + # vms nit: When encountering 'not ok', vms often has the 'not' on a line + # by itself: + # not + # ok 1 - 'I hate VMS' + if ( defined($line) and $line =~ /^\s*not\s*$/ ) { + $line .= ( $self->next_raw || '' ); + } + + return $line; +} + +sub next_raw { + require Carp; + my $msg = Carp::longmess('abstract method called directly!'); + $_[0]->_croak($msg); +} + +=head3 C<handle_unicode> + +If necessary switch the input stream to handle unicode. This only has +any effect for I/O handle based streams. + +The default implementation does nothing. + +=cut + +sub handle_unicode { } + +=head3 C<get_select_handles> + +Return a list of filehandles that may be used upstream in a select() +call to signal that this Iterator is ready. Iterators that are not +handle-based should return an empty list. + +The default implementation does nothing. + +=cut + +sub get_select_handles { + return; +} + +=head3 C<wait> + +B<Note:> this method is abstract and should be overridden. + + my $wait_status = $iter->wait; + +Return the C<wait> status for this iterator. + +=head3 C<exit> + +B<Note:> this method is abstract and should be overridden. + + my $wait_status = $iter->exit; + +Return the C<exit> 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); +} + +1; + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +You must override the abstract methods as noted above. + +=head2 Example + +L<TAP::Parser::Iterator::Array> is probably the easiest example to follow. +There's not much point repeating it here. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Iterator::Array>, +L<TAP::Parser::Iterator::Stream>, +L<TAP::Parser::Iterator::Process>, + +=cut + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Array.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Array.pm new file mode 100755 index 00000000000..9bb813ee017 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Array.pm @@ -0,0 +1,102 @@ +package TAP::Parser::Iterator::Array; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::Iterator (); + +@ISA = 'TAP::Parser::Iterator'; + +=head1 NAME + +TAP::Parser::Iterator::Array - Iterator for array-based TAP sources + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Iterator::Array; + my @data = ('foo', 'bar', baz'); + my $it = TAP::Parser::Iterator::Array->new(\@data); + my $line = $it->next; + +=head1 DESCRIPTION + +This is a simple iterator wrapper for arrays of scalar content, used by +L<TAP::Parser>. Unless you're writing a plugin or subclassing, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +Create an iterator. Takes one argument: an C<$array_ref> + +=head2 Instance Methods + +=head3 C<next> + +Iterate through it, of course. + +=head3 C<next_raw> + +Iterate raw input without applying any fixes for quirky input syntax. + +=head3 C<wait> + +Get the wait status for this iterator. For an array iterator this will always +be zero. + +=head3 C<exit> + +Get the exit status for this iterator. For an array iterator this will always +be zero. + +=cut + +# new() implementation supplied by TAP::Object + +sub _initialize { + my ( $self, $thing ) = @_; + chomp @$thing; + $self->{idx} = 0; + $self->{array} = $thing; + $self->{exit} = undef; + return $self; +} + +sub wait { shift->exit } + +sub exit { + my $self = shift; + return 0 if $self->{idx} >= @{ $self->{array} }; + return; +} + +sub next_raw { + my $self = shift; + return $self->{array}->[ $self->{idx}++ ]; +} + +1; + +=head1 ATTRIBUTION + +Originally ripped off from L<Test::Harness>. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Iterator>, + +=cut + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Process.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Process.pm new file mode 100755 index 00000000000..c2b92461a89 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Process.pm @@ -0,0 +1,381 @@ +package TAP::Parser::Iterator::Process; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::Iterator (); +use Config; +use IO::Handle; + +@ISA = 'TAP::Parser::Iterator'; + +my $IS_WIN32 = ( $^O =~ /^(MS)?Win32$/ ); + +=head1 NAME + +TAP::Parser::Iterator::Process - Iterator for proccess-based TAP sources + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Iterator::Process; + my %args = ( + command => ['python', 'setup.py', 'test'], + merge => 1, + setup => sub { ... }, + teardown => sub { ... }, + ); + my $it = TAP::Parser::Iterator::Process->new(\%args); + my $line = $it->next; + +=head1 DESCRIPTION + +This is a simple iterator wrapper for executing external processes, used by +L<TAP::Parser>. Unless you're writing a plugin or subclassing, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +Create an iterator. Expects one argument containing a hashref of the form: + + command => \@command_to_execute + merge => $attempt_merge_stderr_and_stdout? + setup => $callback_to_setup_command + teardown => $callback_to_teardown_command + +Tries to uses L<IPC::Open3> & L<IO::Select> to communicate with the spawned +process if they are available. Falls back onto C<open()>. + +=head2 Instance Methods + +=head3 C<next> + +Iterate through the process output, of course. + +=head3 C<next_raw> + +Iterate raw input without applying any fixes for quirky input syntax. + +=head3 C<wait> + +Get the wait status for this iterator's process. + +=head3 C<exit> + +Get the exit status for this iterator's process. + +=cut + +{ + + local $^W; # no warnings + # 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 _use_open3 { + my $self = shift; + return unless $Config{d_fork} || $IS_WIN32; + for my $module (qw( IPC::Open3 IO::Select )) { + eval "use $module"; + return if $@; + } + return 1; +} + +{ + my $got_unicode; + + sub _get_unicode { + return $got_unicode if defined $got_unicode; + eval 'use Encode qw(decode_utf8);'; + $got_unicode = $@ ? 0 : 1; + + } +} + +# 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; + local $^W; # 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 ) { + + # Kludge to avoid warning under 5.5 + eval '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<handle_unicode> + +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<get_select_handles> + +Return a list of filehandles that may be used upstream in a select() +call to signal that this Iterator is ready. Iterators that are not +handle based should return an empty list. + +=cut + +sub get_select_handles { + my $self = shift; + return grep $_, ( $self->{out}, $self->{err} ); +} + +1; + +=head1 ATTRIBUTION + +Originally ripped off from L<Test::Harness>. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Iterator>, + +=cut + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Stream.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Stream.pm new file mode 100755 index 00000000000..32c05508e66 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Iterator/Stream.pm @@ -0,0 +1,108 @@ +package TAP::Parser::Iterator::Stream; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::Iterator (); + +@ISA = 'TAP::Parser::Iterator'; + +=head1 NAME + +TAP::Parser::Iterator::Stream - Iterator for filehandle-based TAP sources + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Iterator::Stream; + open( TEST, 'test.tap' ); + my $it = TAP::Parser::Iterator::Stream->new(\*TEST); + my $line = $it->next; + +=head1 DESCRIPTION + +This is a simple iterator wrapper for reading from filehandles, used by +L<TAP::Parser>. Unless you're writing a plugin or subclassing, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +Create an iterator. Expects one argument containing a filehandle. + +=cut + +# new() implementation supplied by TAP::Object + +sub _initialize { + my ( $self, $thing ) = @_; + $self->{fh} = $thing; + return $self; +} + +=head2 Instance Methods + +=head3 C<next> + +Iterate through it, of course. + +=head3 C<next_raw> + +Iterate raw input without applying any fixes for quirky input syntax. + +=head3 C<wait> + +Get the wait status for this iterator. Always returns zero. + +=head3 C<exit> + +Get the exit status for this iterator. Always returns zero. + +=cut + +sub wait { shift->exit } +sub exit { shift->{fh} ? () : 0 } + +sub next_raw { + my $self = shift; + my $fh = $self->{fh}; + + if ( defined( my $line = <$fh> ) ) { + chomp $line; + return $line; + } + else { + $self->_finish; + return; + } +} + +sub _finish { + my $self = shift; + close delete $self->{fh}; +} + +1; + +=head1 ATTRIBUTION + +Originally ripped off from L<Test::Harness>. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Iterator>, + +=cut + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/IteratorFactory.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/IteratorFactory.pm new file mode 100755 index 00000000000..3a46a50ec55 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/IteratorFactory.pm @@ -0,0 +1,342 @@ +package TAP::Parser::IteratorFactory; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); + +use Carp qw( confess ); +use File::Basename qw( fileparse ); + +@ISA = qw(TAP::Object); + +use constant handlers => []; + +=head1 NAME + +TAP::Parser::IteratorFactory - Figures out which SourceHandler objects to use for a given Source + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::IteratorFactory; + my $factory = TAP::Parser::IteratorFactory->new({ %config }); + my $iterator = $factory->make_iterator( $filename ); + +=head1 DESCRIPTION + +This is a factory class that takes a L<TAP::Parser::Source> and runs it through all the +registered L<TAP::Parser::SourceHandler>s to see which one should handle the source. + +If you're a plugin author, you'll be interested in how to L</register_handler>s, +how L</detect_source> works. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +Creates a new factory class: + + my $sf = TAP::Parser::IteratorFactory->new( $config ); + +C<$config> is optional. If given, sets L</config> and calls L</load_handlers>. + +=cut + +sub _initialize { + my ( $self, $config ) = @_; + $self->config( $config || {} )->load_handlers; + return $self; +} + +=head3 C<register_handler> + +Registers a new L<TAP::Parser::SourceHandler> with this factory. + + __PACKAGE__->register_handler( $handler_class ); + +=head3 C<handlers> + +List of handlers that have been registered. + +=cut + +sub register_handler { + my ( $class, $dclass ) = @_; + + confess("$dclass must implement can_handle & make_iterator methods!") + unless UNIVERSAL::can( $dclass, 'can_handle' ) + && UNIVERSAL::can( $dclass, 'make_iterator' ); + + my $handlers = $class->handlers; + push @{$handlers}, $dclass + unless grep { $_ eq $dclass } @{$handlers}; + + return $class; +} + +############################################################################## + +=head2 Instance Methods + +=head3 C<config> + + my $cfg = $sf->config; + $sf->config({ Perl => { %config } }); + +Chaining getter/setter for the configuration of the available source handlers. +This is a hashref keyed on handler class whose values contain config to be passed +onto the handlers during detection & creation. Class names may be fully qualified +or abbreviated, eg: + + # these are equivalent + $sf->config({ 'TAP::Parser::SourceHandler::Perl' => { %config } }); + $sf->config({ 'Perl' => { %config } }); + +=cut + +sub config { + my $self = shift; + return $self->{config} unless @_; + unless ( 'HASH' eq ref $_[0] ) { + $self->_croak('Argument to &config must be a hash reference'); + } + $self->{config} = shift; + return $self; +} + +sub _last_handler { + my $self = shift; + return $self->{last_handler} unless @_; + $self->{last_handler} = shift; + return $self; +} + +sub _testing { + my $self = shift; + return $self->{testing} unless @_; + $self->{testing} = shift; + return $self; +} + +############################################################################## + +=head3 C<load_handlers> + + $sf->load_handlers; + +Loads the handler classes defined in L</config>. For example, given a config: + + $sf->config({ + MySourceHandler => { some => 'config' }, + }); + +C<load_handlers> will attempt to load the C<MySourceHandler> class by looking in +C<@INC> for it in this order: + + TAP::Parser::SourceHandler::MySourceHandler + MySourceHandler + +C<croak>s on error. + +=cut + +sub load_handlers { + my ($self) = @_; + for my $handler ( keys %{ $self->config } ) { + my $sclass = $self->_load_handler($handler); + + # TODO: store which class we loaded anywhere? + } + return $self; +} + +sub _load_handler { + my ( $self, $handler ) = @_; + + my @errors; + for my $dclass ( "TAP::Parser::SourceHandler::$handler", $handler ) { + return $dclass + if UNIVERSAL::can( $dclass, 'can_handle' ) + && UNIVERSAL::can( $dclass, 'make_iterator' ); + + eval "use $dclass"; + if ( my $e = $@ ) { + push @errors, $e; + next; + } + + return $dclass + if UNIVERSAL::can( $dclass, 'can_handle' ) + && UNIVERSAL::can( $dclass, 'make_iterator' ); + push @errors, + "handler '$dclass' does not implement can_handle & make_iterator"; + } + + $self->_croak( + "Cannot load handler '$handler': " . join( "\n", @errors ) ); +} + +############################################################################## + +=head3 C<make_iterator> + + my $iterator = $src_factory->make_iterator( $source ); + +Given a L<TAP::Parser::Source>, finds the most suitable L<TAP::Parser::SourceHandler> +to use to create a L<TAP::Parser::Iterator> (see L</detect_source>). Dies on error. + +=cut + +sub make_iterator { + my ( $self, $source ) = @_; + + $self->_croak('no raw source defined!') unless defined $source->raw; + + $source->config( $self->config )->assemble_meta; + + # is the raw source already an object? + return $source->raw + if ( $source->meta->{is_object} + && UNIVERSAL::isa( $source->raw, 'TAP::Parser::SourceHandler' ) ); + + # figure out what kind of source it is + my $sd_class = $self->detect_source($source); + $self->_last_handler($sd_class); + + return if $self->_testing; + + # create it + my $iterator = $sd_class->make_iterator($source); + + return $iterator; +} + +=head3 C<detect_source> + +Given a L<TAP::Parser::Source>, detects what kind of source it is and +returns I<one> L<TAP::Parser::SourceHandler> (the most confident one). Dies +on error. + +The detection algorithm works something like this: + + for (@registered_handlers) { + # ask them how confident they are about handling this source + $confidence{$handler} = $handler->can_handle( $source ) + } + # choose the most confident handler + +Ties are handled by choosing the first handler. + +=cut + +sub detect_source { + my ( $self, $source ) = @_; + + confess('no raw source ref defined!') unless defined $source->raw; + + # find a list of handlers that can handle this source: + my %handlers; + for my $dclass ( @{ $self->handlers } ) { + my $confidence = $dclass->can_handle($source); + + # warn "handler: $dclass: $confidence\n"; + $handlers{$dclass} = $confidence if $confidence; + } + + if ( !%handlers ) { + + # use Data::Dump qw( pp ); + # warn pp( $meta ); + + # error: can't detect source + my $raw_source_short = substr( ${ $source->raw }, 0, 50 ); + confess("Cannot detect source of '$raw_source_short'!"); + return; + } + + # if multiple handlers can handle it, choose the most confident one + my @handlers = ( + map {$_} + sort { $handlers{$a} cmp $handlers{$b} } + keys %handlers + ); + + # this is really useful for debugging handlers: + if ( $ENV{TAP_HARNESS_SOURCE_FACTORY_VOTES} ) { + warn( + "votes: ", + join( ', ', map {"$_: $handlers{$_}"} @handlers ), + "\n" + ); + } + + # return 1st + return pop @handlers; +} + +1; + +__END__ + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +=head2 Example + +If we've done things right, you'll probably want to write a new source, +rather than sub-classing this (see L<TAP::Parser::SourceHandler> for that). + +But in case you find the need to... + + package MyIteratorFactory; + + use strict; + use vars '@ISA'; + + use TAP::Parser::IteratorFactory; + + @ISA = qw( TAP::Parser::IteratorFactory ); + + # override source detection algorithm + sub detect_source { + my ($self, $raw_source_ref, $meta) = @_; + # do detective work, using $meta and whatever else... + } + + 1; + +=head1 AUTHORS + +Steve Purkis + +=head1 ATTRIBUTION + +Originally ripped off from L<Test::Harness>. + +Moved out of L<TAP::Parser> & converted to a factory class to support +extensible TAP source detective work by Steve Purkis. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::SourceHandler>, +L<TAP::Parser::SourceHandler::File>, +L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::RawTAP>, +L<TAP::Parser::SourceHandler::Handle>, +L<TAP::Parser::SourceHandler::Executable> + +=cut + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Multiplexer.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Multiplexer.pm new file mode 100755 index 00000000000..306bbb975ad --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Multiplexer.pm @@ -0,0 +1,195 @@ +package TAP::Parser::Multiplexer; + +use strict; +use vars qw($VERSION @ISA); + +use IO::Select; +use TAP::Object (); + +use constant IS_WIN32 => $^O =~ /^(MS)?Win32$/; +use constant IS_VMS => $^O eq 'VMS'; +use constant SELECT_OK => !( IS_VMS || IS_WIN32 ); + +@ISA = 'TAP::Object'; + +=head1 NAME + +TAP::Parser::Multiplexer - Multiplex multiple TAP::Parsers + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Multiplexer; + + my $mux = TAP::Parser::Multiplexer->new; + $mux->add( $parser1, $stash1 ); + $mux->add( $parser2, $stash2 ); + while ( my ( $parser, $stash, $result ) = $mux->next ) { + # do stuff + } + +=head1 DESCRIPTION + +C<TAP::Parser::Multiplexer> gathers input from multiple TAP::Parsers. +Internally it calls select on the input file handles for those parsers +to wait for one or more of them to have input available. + +See L<TAP::Harness> for an example of its use. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $mux = TAP::Parser::Multiplexer->new; + +Returns a new C<TAP::Parser::Multiplexer> object. + +=cut + +# new() implementation supplied by TAP::Object + +sub _initialize { + my $self = shift; + $self->{select} = IO::Select->new; + $self->{avid} = []; # Parsers that can't select + $self->{count} = 0; + return $self; +} + +############################################################################## + +=head2 Instance Methods + +=head3 C<add> + + $mux->add( $parser, $stash ); + +Add a TAP::Parser to the multiplexer. C<$stash> is an optional opaque +reference that will be returned from C<next> along with the parser and +the next result. + +=cut + +sub add { + my ( $self, $parser, $stash ) = @_; + + if ( SELECT_OK && ( my @handles = $parser->get_select_handles ) ) { + my $sel = $self->{select}; + + # We have to turn handles into file numbers here because by + # the time we want to remove them from our IO::Select they + # will already have been closed by the iterator. + my @filenos = map { fileno $_ } @handles; + for my $h (@handles) { + $sel->add( [ $h, $parser, $stash, @filenos ] ); + } + + $self->{count}++; + } + else { + push @{ $self->{avid} }, [ $parser, $stash ]; + } +} + +=head3 C<parsers> + + my $count = $mux->parsers; + +Returns the number of parsers. Parsers are removed from the multiplexer +when their input is exhausted. + +=cut + +sub parsers { + my $self = shift; + return $self->{count} + scalar @{ $self->{avid} }; +} + +sub _iter { + my $self = shift; + + my $sel = $self->{select}; + my $avid = $self->{avid}; + my @ready = (); + + return sub { + + # Drain all the non-selectable parsers first + if (@$avid) { + my ( $parser, $stash ) = @{ $avid->[0] }; + my $result = $parser->next; + shift @$avid unless defined $result; + return ( $parser, $stash, $result ); + } + + unless (@ready) { + return unless $sel->count; + @ready = $sel->can_read; + } + + my ( $h, $parser, $stash, @handles ) = @{ shift @ready }; + my $result = $parser->next; + + unless ( defined $result ) { + $sel->remove(@handles); + $self->{count}--; + + # Force another can_read - we may now have removed a handle + # thought to have been ready. + @ready = (); + } + + return ( $parser, $stash, $result ); + }; +} + +=head3 C<next> + +Return a result from the next available parser. Returns a list +containing the parser from which the result came, the stash that +corresponds with that parser and the result. + + my ( $parser, $stash, $result ) = $mux->next; + +If C<$result> is undefined the corresponding parser has reached the end +of its input (and will automatically be removed from the multiplexer). + +When all parsers are exhausted an empty list will be returned. + + if ( my ( $parser, $stash, $result ) = $mux->next ) { + if ( ! defined $result ) { + # End of this parser + } + else { + # Process result + } + } + else { + # All parsers finished + } + +=cut + +sub next { + my $self = shift; + return ( $self->{_iter} ||= $self->_iter )->(); +} + +=head1 See Also + +L<TAP::Parser> + +L<TAP::Harness> + +=cut + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result.pm new file mode 100755 index 00000000000..2f1e8201df0 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result.pm @@ -0,0 +1,300 @@ +package TAP::Parser::Result; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); + +@ISA = 'TAP::Object'; + +BEGIN { + + # make is_* methods + my @attrs = qw( plan pragma test comment bailout version unknown yaml ); + no strict 'refs'; + for my $token (@attrs) { + my $method = "is_$token"; + *$method = sub { return $token eq shift->type }; + } +} + +############################################################################## + +=head1 NAME + +TAP::Parser::Result - Base class for TAP::Parser output objects + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + # abstract class - not meany to be used directly + # see TAP::Parser::ResultFactory for preferred usage + + # directly: + use TAP::Parser::Result; + my $token = {...}; + my $result = TAP::Parser::Result->new( $token ); + +=head2 DESCRIPTION + +This is a simple base class used by L<TAP::Parser> to store objects that +represent the current bit of test output data from TAP (usually a single +line). Unless you're subclassing, you probably won't need to use this module +directly. + +=head2 METHODS + +=head3 C<new> + + # see TAP::Parser::ResultFactory for preferred usage + + # to use directly: + my $result = TAP::Parser::Result->new($token); + +Returns an instance the appropriate class for the test token passed in. + +=cut + +# new() implementation provided by TAP::Object + +sub _initialize { + my ( $self, $token ) = @_; + if ($token) { + + # assign to a hash slice to make a shallow copy of the token. + # I guess we could assign to the hash as (by default) there are not + # contents, but that seems less helpful if someone wants to subclass us + @{$self}{ keys %$token } = values %$token; + } + return $self; +} + +############################################################################## + +=head2 Boolean methods + +The following methods all return a boolean value and are to be overridden in +the appropriate subclass. + +=over 4 + +=item * C<is_plan> + +Indicates whether or not this is the test plan line. + + 1..3 + +=item * C<is_pragma> + +Indicates whether or not this is a pragma line. + + pragma +strict + +=item * C<is_test> + +Indicates whether or not this is a test line. + + ok 1 Is OK! + +=item * C<is_comment> + +Indicates whether or not this is a comment. + + # this is a comment + +=item * C<is_bailout> + +Indicates whether or not this is bailout line. + + Bail out! We're out of dilithium crystals. + +=item * C<is_version> + +Indicates whether or not this is a TAP version line. + + TAP version 4 + +=item * C<is_unknown> + +Indicates whether or not the current line could be parsed. + + ... this line is junk ... + +=item * C<is_yaml> + +Indicates whether or not this is a YAML chunk. + +=back + +=cut + +############################################################################## + +=head3 C<raw> + + print $result->raw; + +Returns the original line of text which was parsed. + +=cut + +sub raw { shift->{raw} } + +############################################################################## + +=head3 C<type> + + my $type = $result->type; + +Returns the "type" of a token, such as C<comment> or C<test>. + +=cut + +sub type { shift->{type} } + +############################################################################## + +=head3 C<as_string> + + print $result->as_string; + +Prints a string representation of the token. This might not be the exact +output, however. Tests will have test numbers added if not present, TODO and +SKIP directives will be capitalized and, in general, things will be cleaned +up. If you need the original text for the token, see the C<raw> method. + +=cut + +sub as_string { shift->{raw} } + +############################################################################## + +=head3 C<is_ok> + + if ( $result->is_ok ) { ... } + +Reports whether or not a given result has passed. Anything which is B<not> a +test result returns true. This is merely provided as a convenient shortcut. + +=cut + +sub is_ok {1} + +############################################################################## + +=head3 C<passed> + +Deprecated. Please use C<is_ok> instead. + +=cut + +sub passed { + warn 'passed() is deprecated. Please use "is_ok()"'; + shift->is_ok; +} + +############################################################################## + +=head3 C<has_directive> + + if ( $result->has_directive ) { + ... + } + +Indicates whether or not the given result has a TODO or SKIP directive. + +=cut + +sub has_directive { + my $self = shift; + return ( $self->has_todo || $self->has_skip ); +} + +############################################################################## + +=head3 C<has_todo> + + if ( $result->has_todo ) { + ... + } + +Indicates whether or not the given result has a TODO directive. + +=cut + +sub has_todo { 'TODO' eq ( shift->{directive} || '' ) } + +############################################################################## + +=head3 C<has_skip> + + if ( $result->has_skip ) { + ... + } + +Indicates whether or not the given result has a SKIP directive. + +=cut + +sub has_skip { 'SKIP' eq ( shift->{directive} || '' ) } + +=head3 C<set_directive> + +Set the directive associated with this token. Used internally to fake +TODO tests. + +=cut + +sub set_directive { + my ( $self, $dir ) = @_; + $self->{directive} = $dir; +} + +1; + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +Remember: if you want your subclass to be automatically used by the parser, +you'll have to register it with L<TAP::Parser::ResultFactory/register_type>. + +If you're creating a completely new result I<type>, you'll probably need to +subclass L<TAP::Parser::Grammar> too, or else it'll never get used. + +=head2 Example + + package MyResult; + + use strict; + use vars '@ISA'; + + @ISA = 'TAP::Parser::Result'; + + # register with the factory: + TAP::Parser::ResultFactory->register_type( 'my_type' => __PACKAGE__ ); + + sub as_string { 'My results all look the same' } + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::ResultFactory>, +L<TAP::Parser::Result::Bailout>, +L<TAP::Parser::Result::Comment>, +L<TAP::Parser::Result::Plan>, +L<TAP::Parser::Result::Pragma>, +L<TAP::Parser::Result::Test>, +L<TAP::Parser::Result::Unknown>, +L<TAP::Parser::Result::Version>, +L<TAP::Parser::Result::YAML>, + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Bailout.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Bailout.pm new file mode 100755 index 00000000000..383b37b695d --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Bailout.pm @@ -0,0 +1,63 @@ +package TAP::Parser::Result::Bailout; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +=head1 NAME + +TAP::Parser::Result::Bailout - Bailout result token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if a bail out line is encountered. + + 1..5 + ok 1 - woo hooo! + Bail out! Well, so much for "woo hooo!" + +=head1 OVERRIDDEN METHODS + +Mainly listed here to shut up the pitiful screams of the pod coverage tests. +They keep me awake at night. + +=over 4 + +=item * C<as_string> + +=back + +=cut + +############################################################################## + +=head2 Instance Methods + +=head3 C<explanation> + + if ( $result->is_bailout ) { + my $explanation = $result->explanation; + print "We bailed out because ($explanation)"; + } + +If, and only if, a token is a bailout token, you can get an "explanation" via +this method. The explanation is the text after the mystical "Bail out!" words +which appear in the tap output. + +=cut + +sub explanation { shift->{bailout} } +sub as_string { shift->{bailout} } + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Comment.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Comment.pm new file mode 100755 index 00000000000..f3b8ced10ce --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Comment.pm @@ -0,0 +1,61 @@ +package TAP::Parser::Result::Comment; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +=head1 NAME + +TAP::Parser::Result::Comment - Comment result token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if a comment line is encountered. + + 1..1 + ok 1 - woo hooo! + # this is a comment + +=head1 OVERRIDDEN METHODS + +Mainly listed here to shut up the pitiful screams of the pod coverage tests. +They keep me awake at night. + +=over 4 + +=item * C<as_string> + +Note that this method merely returns the comment preceded by a '# '. + +=back + +=cut + +############################################################################## + +=head2 Instance Methods + +=head3 C<comment> + + if ( $result->is_comment ) { + my $comment = $result->comment; + print "I have something to say: $comment"; + } + +=cut + +sub comment { shift->{comment} } +sub as_string { shift->{raw} } + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Plan.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Plan.pm new file mode 100755 index 00000000000..d43275d204f --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Plan.pm @@ -0,0 +1,120 @@ +package TAP::Parser::Result::Plan; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +=head1 NAME + +TAP::Parser::Result::Plan - Plan result token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if a plan line is encountered. + + 1..1 + ok 1 - woo hooo! + +C<1..1> is the plan. Gotta have a plan. + +=head1 OVERRIDDEN METHODS + +Mainly listed here to shut up the pitiful screams of the pod coverage tests. +They keep me awake at night. + +=over 4 + +=item * C<as_string> + +=item * C<raw> + +=back + +=cut + +############################################################################## + +=head2 Instance Methods + +=head3 C<plan> + + if ( $result->is_plan ) { + print $result->plan; + } + +This is merely a synonym for C<as_string>. + +=cut + +sub plan { '1..' . shift->{tests_planned} } + +############################################################################## + +=head3 C<tests_planned> + + my $planned = $result->tests_planned; + +Returns the number of tests planned. For example, a plan of C<1..17> will +cause this method to return '17'. + +=cut + +sub tests_planned { shift->{tests_planned} } + +############################################################################## + +=head3 C<directive> + + my $directive = $plan->directive; + +If a SKIP directive is included with the plan, this method will return it. + + 1..0 # SKIP: why bother? + +=cut + +sub directive { shift->{directive} } + +############################################################################## + +=head3 C<has_skip> + + if ( $result->has_skip ) { ... } + +Returns a boolean value indicating whether or not this test has a SKIP +directive. + +=head3 C<explanation> + + my $explanation = $plan->explanation; + +If a SKIP directive was included with the plan, this method will return the +explanation, if any. + +=cut + +sub explanation { shift->{explanation} } + +=head3 C<todo_list> + + my $todo = $result->todo_list; + for ( @$todo ) { + ... + } + +=cut + +sub todo_list { shift->{todo_list} } + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Pragma.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Pragma.pm new file mode 100755 index 00000000000..40feca71336 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Pragma.pm @@ -0,0 +1,63 @@ +package TAP::Parser::Result::Pragma; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +=head1 NAME + +TAP::Parser::Result::Pragma - TAP pragma token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if a pragma is encountered. + + TAP version 13 + pragma +strict, -foo + +Pragmas are only supported from TAP version 13 onwards. + +=head1 OVERRIDDEN METHODS + +Mainly listed here to shut up the pitiful screams of the pod coverage tests. +They keep me awake at night. + +=over 4 + +=item * C<as_string> + +=item * C<raw> + +=back + +=cut + +############################################################################## + +=head2 Instance Methods + +=head3 C<pragmas> + +if ( $result->is_pragma ) { + @pragmas = $result->pragmas; +} + +=cut + +sub pragmas { + my @pragmas = @{ shift->{pragmas} }; + return wantarray ? @pragmas : \@pragmas; +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Test.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Test.pm new file mode 100755 index 00000000000..953aae3463f --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Test.pm @@ -0,0 +1,274 @@ +package TAP::Parser::Result::Test; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +use vars qw($VERSION); + +=head1 NAME + +TAP::Parser::Result::Test - Test result token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if a test line is encountered. + + 1..1 + ok 1 - woo hooo! + +=head1 OVERRIDDEN METHODS + +This class is the workhorse of the L<TAP::Parser> system. Most TAP lines will +be test lines and if C<< $result->is_test >>, then you have a bunch of methods +at your disposal. + +=head2 Instance Methods + +=cut + +############################################################################## + +=head3 C<ok> + + my $ok = $result->ok; + +Returns the literal text of the C<ok> or C<not ok> status. + +=cut + +sub ok { shift->{ok} } + +############################################################################## + +=head3 C<number> + + my $test_number = $result->number; + +Returns the number of the test, even if the original TAP output did not supply +that number. + +=cut + +sub number { shift->{test_num} } + +sub _number { + my ( $self, $number ) = @_; + $self->{test_num} = $number; +} + +############################################################################## + +=head3 C<description> + + my $description = $result->description; + +Returns the description of the test, if any. This is the portion after the +test number but before the directive. + +=cut + +sub description { shift->{description} } + +############################################################################## + +=head3 C<directive> + + my $directive = $result->directive; + +Returns either C<TODO> or C<SKIP> if either directive was present for a test +line. + +=cut + +sub directive { shift->{directive} } + +############################################################################## + +=head3 C<explanation> + + my $explanation = $result->explanation; + +If a test had either a C<TODO> or C<SKIP> directive, this method will return +the accompanying explantion, if present. + + not ok 17 - 'Pigs can fly' # TODO not enough acid + +For the above line, the explanation is I<not enough acid>. + +=cut + +sub explanation { shift->{explanation} } + +############################################################################## + +=head3 C<is_ok> + + if ( $result->is_ok ) { ... } + +Returns a boolean value indicating whether or not the test passed. Remember +that for TODO tests, the test always passes. + +If the test is unplanned, this method will always return false. See +C<is_unplanned>. + +=cut + +sub is_ok { + my $self = shift; + + return if $self->is_unplanned; + + # TODO directives reverse the sense of a test. + return $self->has_todo ? 1 : $self->ok !~ /not/; +} + +############################################################################## + +=head3 C<is_actual_ok> + + if ( $result->is_actual_ok ) { ... } + +Returns a boolean value indicating whether or not the test passed, regardless +of its TODO status. + +=cut + +sub is_actual_ok { + my $self = shift; + return $self->{ok} !~ /not/; +} + +############################################################################## + +=head3 C<actual_passed> + +Deprecated. Please use C<is_actual_ok> instead. + +=cut + +sub actual_passed { + warn 'actual_passed() is deprecated. Please use "is_actual_ok()"'; + goto &is_actual_ok; +} + +############################################################################## + +=head3 C<todo_passed> + + if ( $test->todo_passed ) { + # test unexpectedly succeeded + } + +If this is a TODO test and an 'ok' line, this method returns true. +Otherwise, it will always return false (regardless of passing status on +non-todo tests). + +This is used to track which tests unexpectedly succeeded. + +=cut + +sub todo_passed { + my $self = shift; + return $self->has_todo && $self->is_actual_ok; +} + +############################################################################## + +=head3 C<todo_failed> + + # deprecated in favor of 'todo_passed'. This method was horribly misnamed. + +This was a badly misnamed method. It indicates which TODO tests unexpectedly +succeeded. Will now issue a warning and call C<todo_passed>. + +=cut + +sub todo_failed { + warn 'todo_failed() is deprecated. Please use "todo_passed()"'; + goto &todo_passed; +} + +############################################################################## + +=head3 C<has_skip> + + if ( $result->has_skip ) { ... } + +Returns a boolean value indicating whether or not this test has a SKIP +directive. + +=head3 C<has_todo> + + if ( $result->has_todo ) { ... } + +Returns a boolean value indicating whether or not this test has a TODO +directive. + +=head3 C<as_string> + + print $result->as_string; + +This method prints the test as a string. It will probably be similar, but +not necessarily identical, to the original test line. Directives are +capitalized, some whitespace may be trimmed and a test number will be added if +it was not present in the original line. If you need the original text of the +test line, use the C<raw> method. + +=cut + +sub as_string { + my $self = shift; + my $string = $self->ok . " " . $self->number; + if ( my $description = $self->description ) { + $string .= " $description"; + } + if ( my $directive = $self->directive ) { + my $explanation = $self->explanation; + $string .= " # $directive $explanation"; + } + return $string; +} + +############################################################################## + +=head3 C<is_unplanned> + + if ( $test->is_unplanned ) { ... } + $test->is_unplanned(1); + +If a test number is greater than the number of planned tests, this method will +return true. Unplanned tests will I<always> return false for C<is_ok>, +regardless of whether or not the test C<has_todo>. + +Note that if tests have a trailing plan, it is not possible to set this +property for unplanned tests as we do not know it's unplanned until the plan +is reached: + + print <<'END'; + ok 1 + ok 2 + 1..1 + END + +=cut + +sub is_unplanned { + my $self = shift; + return ( $self->{unplanned} || '' ) unless @_; + $self->{unplanned} = !!shift; + return $self; +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Unknown.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Unknown.pm new file mode 100755 index 00000000000..f225ca19eae --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Unknown.pm @@ -0,0 +1,51 @@ +package TAP::Parser::Result::Unknown; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +use vars qw($VERSION); + +=head1 NAME + +TAP::Parser::Result::Unknown - Unknown result token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if the parser does not recognize the token line. For example: + + 1..5 + VERSION 7 + ok 1 - woo hooo! + ... woo hooo! is cool! + +In the above "TAP", the second and fourth lines will generate "Unknown" +tokens. + +=head1 OVERRIDDEN METHODS + +Mainly listed here to shut up the pitiful screams of the pod coverage tests. +They keep me awake at night. + +=over 4 + +=item * C<as_string> + +=item * C<raw> + +=back + +=cut + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Version.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Version.pm new file mode 100755 index 00000000000..bfb78e5e8e2 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/Version.pm @@ -0,0 +1,63 @@ +package TAP::Parser::Result::Version; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +=head1 NAME + +TAP::Parser::Result::Version - TAP syntax version token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if a version line is encountered. + + TAP version 13 + ok 1 + not ok 2 + +The first version of TAP to include an explicit version number is 13. + +=head1 OVERRIDDEN METHODS + +Mainly listed here to shut up the pitiful screams of the pod coverage tests. +They keep me awake at night. + +=over 4 + +=item * C<as_string> + +=item * C<raw> + +=back + +=cut + +############################################################################## + +=head2 Instance Methods + +=head3 C<version> + + if ( $result->is_version ) { + print $result->version; + } + +This is merely a synonym for C<as_string>. + +=cut + +sub version { shift->{version} } + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Result/YAML.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/YAML.pm new file mode 100755 index 00000000000..8b3c20ea305 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Result/YAML.pm @@ -0,0 +1,62 @@ +package TAP::Parser::Result::YAML; + +use strict; + +use vars qw($VERSION @ISA); +use TAP::Parser::Result; +@ISA = 'TAP::Parser::Result'; + +=head1 NAME + +TAP::Parser::Result::YAML - YAML result token. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 DESCRIPTION + +This is a subclass of L<TAP::Parser::Result>. A token of this class will be +returned if a YAML block is encountered. + + 1..1 + ok 1 - woo hooo! + +C<1..1> is the plan. Gotta have a plan. + +=head1 OVERRIDDEN METHODS + +Mainly listed here to shut up the pitiful screams of the pod coverage tests. +They keep me awake at night. + +=over 4 + +=item * C<as_string> + +=item * C<raw> + +=back + +=cut + +############################################################################## + +=head2 Instance Methods + +=head3 C<data> + + if ( $result->is_yaml ) { + print $result->data; + } + +Return the parsed YAML data for this result + +=cut + +sub data { shift->{data} } + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/ResultFactory.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/ResultFactory.pm new file mode 100755 index 00000000000..99bbeeeb038 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/ResultFactory.pm @@ -0,0 +1,189 @@ +package TAP::Parser::ResultFactory; + +use strict; +use vars qw($VERSION @ISA %CLASS_FOR); + +use TAP::Object (); +use TAP::Parser::Result::Bailout (); +use TAP::Parser::Result::Comment (); +use TAP::Parser::Result::Plan (); +use TAP::Parser::Result::Pragma (); +use TAP::Parser::Result::Test (); +use TAP::Parser::Result::Unknown (); +use TAP::Parser::Result::Version (); +use TAP::Parser::Result::YAML (); + +@ISA = 'TAP::Object'; + +############################################################################## + +=head1 NAME + +TAP::Parser::ResultFactory - Factory for creating TAP::Parser output objects + +=head1 SYNOPSIS + + use TAP::Parser::ResultFactory; + my $token = {...}; + my $factory = TAP::Parser::ResultFactory->new; + my $result = $factory->make_result( $token ); + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head2 DESCRIPTION + +This is a simple factory class which returns a L<TAP::Parser::Result> subclass +representing the current bit of test data from TAP (usually a single line). +It is used primarily by L<TAP::Parser::Grammar>. Unless you're subclassing, +you probably won't need to use this module directly. + +=head2 METHODS + +=head2 Class Methods + +=head3 C<new> + +Creates a new factory class. +I<Note:> You currently don't need to instantiate a factory in order to use it. + +=head3 C<make_result> + +Returns an instance the appropriate class for the test token passed in. + + my $result = TAP::Parser::ResultFactory->make_result($token); + +Can also be called as an instance method. + +=cut + +sub make_result { + my ( $proto, $token ) = @_; + my $type = $token->{type}; + return $proto->class_for($type)->new($token); +} + +=head3 C<class_for> + +Takes one argument: C<$type>. Returns the class for this $type, or C<croak>s +with an error. + +=head3 C<register_type> + +Takes two arguments: C<$type>, C<$class> + +This lets you override an existing type with your own custom type, or register +a completely new type, eg: + + # create a custom result type: + package MyResult; + use strict; + use vars qw(@ISA); + @ISA = 'TAP::Parser::Result'; + + # register with the factory: + TAP::Parser::ResultFactory->register_type( 'my_type' => __PACKAGE__ ); + + # use it: + my $r = TAP::Parser::ResultFactory->( { type => 'my_type' } ); + +Your custom type should then be picked up automatically by the L<TAP::Parser>. + +=cut + +BEGIN { + %CLASS_FOR = ( + plan => 'TAP::Parser::Result::Plan', + pragma => 'TAP::Parser::Result::Pragma', + test => 'TAP::Parser::Result::Test', + comment => 'TAP::Parser::Result::Comment', + bailout => 'TAP::Parser::Result::Bailout', + version => 'TAP::Parser::Result::Version', + unknown => 'TAP::Parser::Result::Unknown', + yaml => 'TAP::Parser::Result::YAML', + ); +} + +sub class_for { + my ( $class, $type ) = @_; + + # return target class: + return $CLASS_FOR{$type} if exists $CLASS_FOR{$type}; + + # or complain: + require Carp; + Carp::croak("Could not determine class for result type '$type'"); +} + +sub register_type { + my ( $class, $type, $rclass ) = @_; + + # register it blindly, assume they know what they're doing + $CLASS_FOR{$type} = $rclass; + return $class; +} + +1; + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +There are a few things to bear in mind when creating your own +C<ResultFactory>: + +=over 4 + +=item 1 + +The factory itself is never instantiated (this I<may> change in the future). +This means that C<_initialize> is never called. + +=item 2 + +C<TAP::Parser::Result-E<gt>new> is never called, $tokens are reblessed. +This I<will> change in a future version! + +=item 3 + +L<TAP::Parser::Result> subclasses will register themselves with +L<TAP::Parser::ResultFactory> directly: + + package MyFooResult; + TAP::Parser::ResultFactory->register_type( foo => __PACKAGE__ ); + +Of course, it's up to you to decide whether or not to ignore them. + +=back + +=head2 Example + + package MyResultFactory; + + use strict; + use vars '@ISA'; + + use MyResult; + use TAP::Parser::ResultFactory; + + @ISA = qw( TAP::Parser::ResultFactory ); + + # force all results to be 'MyResult' + sub class_for { + return 'MyResult'; + } + + 1; + +=head1 SEE ALSO + +L<TAP::Parser>, +L<TAP::Parser::Result>, +L<TAP::Parser::Grammar> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler.pm new file mode 100755 index 00000000000..031c5993c8e --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler.pm @@ -0,0 +1,312 @@ +package TAP::Parser::Scheduler; + +use strict; +use vars qw($VERSION); +use Carp; +use TAP::Parser::Scheduler::Job; +use TAP::Parser::Scheduler::Spinner; + +=head1 NAME + +TAP::Parser::Scheduler - Schedule tests during parallel testing + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Scheduler; + +=head1 DESCRIPTION + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $sched = TAP::Parser::Scheduler->new; + +Returns a new C<TAP::Parser::Scheduler> object. + +=cut + +sub new { + my $class = shift; + + croak "Need a number of key, value pairs" if @_ % 2; + + my %args = @_; + my $tests = delete $args{tests} || croak "Need a 'tests' argument"; + my $rules = delete $args{rules} || { par => '**' }; + + croak "Unknown arg(s): ", join ', ', sort keys %args + if keys %args; + + # Turn any simple names into a name, description pair. TODO: Maybe + # construct jobs here? + my $self = bless {}, $class; + + $self->_set_rules( $rules, $tests ); + + return $self; +} + +# Build the scheduler data structure. +# +# SCHEDULER-DATA ::= JOB +# || ARRAY OF ARRAY OF SCHEDULER-DATA +# +# The nested arrays are the key to scheduling. The outer array contains +# a list of things that may be executed in parallel. Whenever an +# eligible job is sought any element of the outer array that is ready to +# execute can be selected. The inner arrays represent sequential +# execution. They can only proceed when the first job is ready to run. + +sub _set_rules { + my ( $self, $rules, $tests ) = @_; + my @tests = map { TAP::Parser::Scheduler::Job->new(@$_) } + map { 'ARRAY' eq ref $_ ? $_ : [ $_, $_ ] } @$tests; + my $schedule = $self->_rule_clause( $rules, \@tests ); + + # If any tests are left add them as a sequential block at the end of + # the run. + $schedule = [ [ $schedule, @tests ] ] if @tests; + + $self->{schedule} = $schedule; +} + +sub _rule_clause { + my ( $self, $rule, $tests ) = @_; + croak 'Rule clause must be a hash' + unless 'HASH' eq ref $rule; + + my @type = keys %$rule; + croak 'Rule clause must have exactly one key' + unless @type == 1; + + my %handlers = ( + par => sub { + [ map { [$_] } @_ ]; + }, + seq => sub { [ [@_] ] }, + ); + + my $handler = $handlers{ $type[0] } + || croak 'Unknown scheduler type: ', $type[0]; + my $val = $rule->{ $type[0] }; + + return $handler->( + map { + 'HASH' eq ref $_ + ? $self->_rule_clause( $_, $tests ) + : $self->_expand( $_, $tests ) + } 'ARRAY' eq ref $val ? @$val : $val + ); +} + +sub _glob_to_regexp { + my ( $self, $glob ) = @_; + my $nesting; + my $pattern; + + while (1) { + if ( $glob =~ /\G\*\*/gc ) { + + # ** is any number of characters, including /, within a pathname + $pattern .= '.*?'; + } + elsif ( $glob =~ /\G\*/gc ) { + + # * is zero or more characters within a filename/directory name + $pattern .= '[^/]*'; + } + elsif ( $glob =~ /\G\?/gc ) { + + # ? is exactly one character within a filename/directory name + $pattern .= '[^/]'; + } + elsif ( $glob =~ /\G\{/gc ) { + + # {foo,bar,baz} is any of foo, bar or baz. + $pattern .= '(?:'; + ++$nesting; + } + elsif ( $nesting and $glob =~ /\G,/gc ) { + + # , is only special inside {} + $pattern .= '|'; + } + elsif ( $nesting and $glob =~ /\G\}/gc ) { + + # } that matches { is special. But unbalanced } are not. + $pattern .= ')'; + --$nesting; + } + elsif ( $glob =~ /\G(\\.)/gc ) { + + # A quoted literal + $pattern .= $1; + } + elsif ( $glob =~ /\G([\},])/gc ) { + + # Sometimes meta characters + $pattern .= '\\' . $1; + } + else { + + # Eat everything that is not a meta character. + $glob =~ /\G([^{?*\\\},]*)/gc; + $pattern .= quotemeta $1; + } + return $pattern if pos $glob == length $glob; + } +} + +sub _expand { + my ( $self, $name, $tests ) = @_; + + my $pattern = $self->_glob_to_regexp($name); + $pattern = qr/^ $pattern $/x; + my @match = (); + + for ( my $ti = 0; $ti < @$tests; $ti++ ) { + if ( $tests->[$ti]->filename =~ $pattern ) { + push @match, splice @$tests, $ti, 1; + $ti--; + } + } + + return @match; +} + +=head3 C<get_all> + +Get a list of all remaining tests. + +=cut + +sub get_all { + my $self = shift; + my @all = $self->_gather( $self->{schedule} ); + $self->{count} = @all; + @all; +} + +sub _gather { + my ( $self, $rule ) = @_; + return unless defined $rule; + return $rule unless 'ARRAY' eq ref $rule; + return map { defined() ? $self->_gather($_) : () } map {@$_} @$rule; +} + +=head3 C<get_job> + +Return the next available job or C<undef> if none are available. Returns +a C<TAP::Parser::Scheduler::Spinner> if the scheduler still has pending +jobs but none are available to run right now. + +=cut + +sub get_job { + my $self = shift; + $self->{count} ||= $self->get_all; + my @jobs = $self->_find_next_job( $self->{schedule} ); + if (@jobs) { + --$self->{count}; + return $jobs[0]; + } + + return TAP::Parser::Scheduler::Spinner->new + if $self->{count}; + + return; +} + +sub _not_empty { + my $ar = shift; + return 1 unless 'ARRAY' eq ref $ar; + for (@$ar) { + return 1 if _not_empty($_); + } + return; +} + +sub _is_empty { !_not_empty(@_) } + +sub _find_next_job { + my ( $self, $rule ) = @_; + + my @queue = (); + my $index = 0; + while ( $index < @$rule ) { + my $seq = $rule->[$index]; + + # Prune any exhausted items. + shift @$seq while @$seq && _is_empty( $seq->[0] ); + if (@$seq) { + if ( defined $seq->[0] ) { + if ( 'ARRAY' eq ref $seq->[0] ) { + push @queue, $seq; + } + else { + my $job = splice @$seq, 0, 1, undef; + $job->on_finish( sub { shift @$seq } ); + return $job; + } + } + ++$index; + } + else { + + # Remove the empty sub-array from the array + splice @$rule, $index, 1; + } + } + + for my $seq (@queue) { + if ( my @jobs = $self->_find_next_job( $seq->[0] ) ) { + return @jobs; + } + } + + return; +} + +=head3 C<as_string> + +Return a human readable representation of the scheduling tree. + +=cut + +sub as_string { + my $self = shift; + return $self->_as_string( $self->{schedule} ); +} + +sub _as_string { + my ( $self, $rule, $depth ) = ( shift, shift, shift || 0 ); + my $pad = ' ' x 2; + my $indent = $pad x $depth; + if ( !defined $rule ) { + return "$indent(undef)\n"; + } + elsif ( 'ARRAY' eq ref $rule ) { + return unless @$rule; + my $type = ( 'par', 'seq' )[ $depth % 2 ]; + return join( + '', "$indent$type:\n", + map { $self->_as_string( $_, $depth + 1 ) } @$rule + ); + } + else { + return "$indent'" . $rule->filename . "'\n"; + } +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler/Job.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler/Job.pm new file mode 100755 index 00000000000..db04a077ef9 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler/Job.pm @@ -0,0 +1,107 @@ +package TAP::Parser::Scheduler::Job; + +use strict; +use vars qw($VERSION); +use Carp; + +=head1 NAME + +TAP::Parser::Scheduler::Job - A single testing job. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Scheduler::Job; + +=head1 DESCRIPTION + +Represents a single test 'job'. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $job = TAP::Parser::Scheduler::Job->new( + $name, $desc + ); + +Returns a new C<TAP::Parser::Scheduler::Job> object. + +=cut + +sub new { + my ( $class, $name, $desc, @ctx ) = @_; + return bless { + filename => $name, + description => $desc, + @ctx ? ( context => \@ctx ) : (), + }, $class; +} + +=head3 C<on_finish> + +Register a closure to be called when this job is destroyed. + +=cut + +sub on_finish { + my ( $self, $cb ) = @_; + $self->{on_finish} = $cb; +} + +=head3 C<finish> + +Called when a job is complete to unlock it. + +=cut + +sub finish { + my $self = shift; + if ( my $cb = $self->{on_finish} ) { + $cb->($self); + } +} + +=head3 C<filename> + +=head3 C<description> + +=head3 C<context> + +=cut + +sub filename { shift->{filename} } +sub description { shift->{description} } +sub context { @{ shift->{context} || [] } } + +=head3 C<as_array_ref> + +For backwards compatibility in callbacks. + +=cut + +sub as_array_ref { + my $self = shift; + return [ $self->filename, $self->description, $self->{context} ||= [] ]; +} + +=head3 C<is_spinner> + +Returns false indicating that this is a real job rather than a +'spinner'. Spinners are returned when the scheduler still has pending +jobs but can't (because of locking) return one right now. + +=cut + +sub is_spinner {0} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler/Spinner.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler/Spinner.pm new file mode 100755 index 00000000000..04884ff5aa9 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Scheduler/Spinner.pm @@ -0,0 +1,53 @@ +package TAP::Parser::Scheduler::Spinner; + +use strict; +use vars qw($VERSION); +use Carp; + +=head1 NAME + +TAP::Parser::Scheduler::Spinner - A no-op job. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Scheduler::Spinner; + +=head1 DESCRIPTION + +A no-op job. Returned by C<TAP::Parser::Scheduler> as an instruction to +the harness to spin (keep executing tests) while the scheduler can't +return a real job. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $job = TAP::Parser::Scheduler::Spinner->new; + +Returns a new C<TAP::Parser::Scheduler::Spinner> object. + +=cut + +sub new { bless {}, shift } + +=head3 C<is_spinner> + +Returns true indicating that is a 'spinner' job. Spinners are returned +when the scheduler still has pending jobs but can't (because of locking) +return one right now. + +=cut + +sub is_spinner {1} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Source.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Source.pm new file mode 100755 index 00000000000..1dd77c0351b --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Source.pm @@ -0,0 +1,385 @@ +package TAP::Parser::Source; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); +use File::Basename qw( fileparse ); + +@ISA = qw(TAP::Object); + +=head1 NAME + +TAP::Parser::Source - a TAP source & meta data about it + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Source; + my $source = TAP::Parser::Source->new; + $source->raw( \'reference to raw TAP source' ) + ->config( \%config ) + ->merge( $boolean ) + ->switches( \@switches ) + ->test_args( \@args ) + ->assemble_meta; + + do { ... } if $source->meta->{is_file}; + # see assemble_meta for a full list of data available + +=head1 DESCRIPTION + +A TAP I<source> is something that produces a stream of TAP for the parser to +consume, such as an executable file, a text file, an archive, an IO handle, a +database, etc. C<TAP::Parser::Source>s encapsulate these I<raw> sources, and +provide some useful meta data about them. They are used by +L<TAP::Parser::SourceHandler>s, which do whatever is required to produce & +capture a stream of TAP from the I<raw> source, and package it up in a +L<TAP::Parser::Iterator> for the parser to consume. + +Unless you're writing a new L<TAP::Parser::SourceHandler>, a plugin or +subclassing L<TAP::Parser>, you probably won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $source = TAP::Parser::Source->new; + +Returns a new C<TAP::Parser::Source> object. + +=cut + +# new() implementation supplied by TAP::Object + +sub _initialize { + my ($self) = @_; + $self->meta( {} ); + $self->config( {} ); + return $self; +} + +############################################################################## + +=head2 Instance Methods + +=head3 C<raw> + + my $raw = $source->raw; + $source->raw( $some_value ); + +Chaining getter/setter for the raw TAP source. This is a reference, as it may +contain large amounts of data (eg: raw TAP). + +=head3 C<meta> + + my $meta = $source->meta; + $source->meta({ %some_value }); + +Chaining getter/setter for meta data about the source. This defaults to an +empty hashref. See L</assemble_meta> for more info. + +=head3 C<has_meta> + +True if the source has meta data. + +=head3 C<config> + + my $config = $source->config; + $source->config({ %some_value }); + +Chaining getter/setter for the source's configuration, if any has been provided +by the user. How it's used is up to you. This defaults to an empty hashref. +See L</config_for> for more info. + +=head3 C<merge> + + my $merge = $source->merge; + $source->config( $bool ); + +Chaining getter/setter for the flag that dictates whether STDOUT and STDERR +should be merged (where appropriate). Defaults to undef. + +=head3 C<switches> + + my $switches = $source->switches; + $source->config([ @switches ]); + +Chaining getter/setter for the list of command-line switches that should be +passed to the source (where appropriate). Defaults to undef. + +=head3 C<test_args> + + my $test_args = $source->test_args; + $source->config([ @test_args ]); + +Chaining getter/setter for the list of command-line arguments that should be +passed to the source (where appropriate). Defaults to undef. + +=cut + +sub raw { + my $self = shift; + return $self->{raw} unless @_; + $self->{raw} = shift; + return $self; +} + +sub meta { + my $self = shift; + return $self->{meta} unless @_; + $self->{meta} = shift; + return $self; +} + +sub has_meta { + return scalar %{ shift->meta } ? 1 : 0; +} + +sub config { + my $self = shift; + return $self->{config} unless @_; + $self->{config} = shift; + return $self; +} + +sub merge { + my $self = shift; + return $self->{merge} unless @_; + $self->{merge} = shift; + return $self; +} + +sub switches { + my $self = shift; + return $self->{switches} unless @_; + $self->{switches} = shift; + return $self; +} + +sub test_args { + my $self = shift; + return $self->{test_args} unless @_; + $self->{test_args} = shift; + return $self; +} + +=head3 C<assemble_meta> + + my $meta = $source->assemble_meta; + +Gathers meta data about the L</raw> source, stashes it in L</meta> and returns +it as a hashref. This is done so that the L<TAP::Parser::SourceHandler>s don't +have to repeat common checks. Currently this includes: + + is_scalar => $bool, + is_hash => $bool, + is_array => $bool, + + # for scalars: + length => $n + has_newlines => $bool + + # only done if the scalar looks like a filename + is_file => $bool, + is_dir => $bool, + is_symlink => $bool, + file => { + # only done if the scalar looks like a filename + basename => $string, # including ext + dir => $string, + ext => $string, + lc_ext => $string, + # system checks + exists => $bool, + stat => [ ... ], # perldoc -f stat + empty => $bool, + size => $n, + text => $bool, + binary => $bool, + read => $bool, + write => $bool, + execute => $bool, + setuid => $bool, + setgid => $bool, + sticky => $bool, + is_file => $bool, + is_dir => $bool, + is_symlink => $bool, + # only done if the file's a symlink + lstat => [ ... ], # perldoc -f lstat + # only done if the file's a readable text file + shebang => $first_line, + } + + # for arrays: + size => $n, + +=cut + +sub assemble_meta { + my ($self) = @_; + + return $self->meta if $self->has_meta; + + my $meta = $self->meta; + my $raw = $self->raw; + + # rudimentary is object test - if it's blessed it'll + # inherit from UNIVERSAL + $meta->{is_object} = UNIVERSAL::isa( $raw, 'UNIVERSAL' ) ? 1 : 0; + + if ( $meta->{is_object} ) { + $meta->{class} = ref($raw); + } + else { + my $ref = lc( ref($raw) ); + $meta->{"is_$ref"} = 1; + } + + if ( $meta->{is_scalar} ) { + my $source = $$raw; + $meta->{length} = length($$raw); + $meta->{has_newlines} = $$raw =~ /\n/ ? 1 : 0; + + # only do file checks if it looks like a filename + if ( !$meta->{has_newlines} and $meta->{length} < 1024 ) { + my $file = {}; + $file->{exists} = -e $source ? 1 : 0; + if ( $file->{exists} ) { + $meta->{file} = $file; + + # avoid extra system calls (see `perldoc -f -X`) + $file->{stat} = [ stat(_) ]; + $file->{empty} = -z _ ? 1 : 0; + $file->{size} = -s _; + $file->{text} = -T _ ? 1 : 0; + $file->{binary} = -B _ ? 1 : 0; + $file->{read} = -r _ ? 1 : 0; + $file->{write} = -w _ ? 1 : 0; + $file->{execute} = -x _ ? 1 : 0; + $file->{setuid} = -u _ ? 1 : 0; + $file->{setgid} = -g _ ? 1 : 0; + $file->{sticky} = -k _ ? 1 : 0; + + $meta->{is_file} = $file->{is_file} = -f _ ? 1 : 0; + $meta->{is_dir} = $file->{is_dir} = -d _ ? 1 : 0; + + # symlink check requires another system call + $meta->{is_symlink} = $file->{is_symlink} + = -l $source ? 1 : 0; + if ( $file->{is_symlink} ) { + $file->{lstat} = [ lstat(_) ]; + } + + # put together some common info about the file + ( $file->{basename}, $file->{dir}, $file->{ext} ) + = map { defined $_ ? $_ : '' } + fileparse( $source, qr/\.[^.]*/ ); + $file->{lc_ext} = lc( $file->{ext} ); + $file->{basename} .= $file->{ext} if $file->{ext}; + + if ( $file->{text} and $file->{read} ) { + eval { $file->{shebang} = $self->_read_shebang($$raw); }; + if ( my $e = $@ ) { + warn $e; + } + } + } + } + } + elsif ( $meta->{is_array} ) { + $meta->{size} = $#$raw + 1; + } + elsif ( $meta->{is_hash} ) { + ; # do nothing + } + + return $meta; +} + +=head3 C<shebang> + +Get the shebang line for a script file. + + my $shebang = TAP::Parser::Source->shebang( $some_script ); + +May be called as a class method + +=cut + +{ + + # Global shebang cache. + my %shebang_for; + + sub _read_shebang { + my ( $self, $file ) = @_; + my $shebang; + local *TEST; + if ( open( TEST, $file ) ) { + $shebang = <TEST>; + chomp $shebang; + close(TEST) or die "Can't close $file. $!\n"; + } + else { + die "Can't open $file. $!\n"; + } + return $shebang; + } + + sub shebang { + my ( $class, $file ) = @_; + $shebang_for{$file} = $class->_read_shebang($file) + unless exists $shebang_for{$file}; + return $shebang_for{$file}; + } +} + +=head3 C<config_for> + + my $config = $source->config_for( $class ); + +Returns L</config> for the $class given. Class names may be fully qualified +or abbreviated, eg: + + # these are equivalent + $source->config_for( 'Perl' ); + $source->config_for( 'TAP::Parser::SourceHandler::Perl' ); + +If a fully qualified $class is given, its abbreviated version is checked first. + +=cut + +sub config_for { + my ( $self, $class ) = @_; + my ($abbrv_class) = ( $class =~ /(?:\:\:)?(\w+)$/ ); + my $config = $self->config->{$abbrv_class} || $self->config->{$class}; + return $config; +} + +1; + +__END__ + +=head1 AUTHORS + +Steve Purkis. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::IteratorFactory>, +L<TAP::Parser::SourceHandler> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Source/Perl.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Source/Perl.pm new file mode 100755 index 00000000000..1f4f2e1428c --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Source/Perl.pm @@ -0,0 +1,326 @@ +package TAP::Parser::Source::Perl; + +use strict; +use Config; +use vars qw($VERSION @ISA); + +use constant IS_WIN32 => ( $^O =~ /^(MS)?Win32$/ ); +use constant IS_VMS => ( $^O eq 'VMS' ); + +use TAP::Parser::Source; +use TAP::Parser::Utils qw( split_shell ); + +@ISA = 'TAP::Parser::Source'; + +=head1 NAME + +TAP::Parser::Source::Perl - Stream Perl output + +=head1 VERSION + +Version 3.17 + +=cut + +$VERSION = '3.17'; + +=head1 SYNOPSIS + + use TAP::Parser::Source::Perl; + my $perl = TAP::Parser::Source::Perl->new; + my $stream = $perl->source( [ $filename, @args ] )->get_stream; + +=head1 DESCRIPTION + +Takes a filename and hopefully returns a stream from it. The filename should +be the name of a Perl program. + +Note that this is a subclass of L<TAP::Parser::Source>. See that module for +more methods. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $perl = TAP::Parser::Source::Perl->new; + +Returns a new C<TAP::Parser::Source::Perl> object. + +=head2 Instance Methods + +=head3 C<source> + +Getter/setter the name of the test program and any arguments it requires. + + my ($filename, @args) = @{ $perl->source }; + $perl->source( [ $filename, @args ] ); + +C<croak>s if C<$filename> could not be found. + +=cut + +sub source { + my $self = shift; + $self->_croak("Cannot find ($_[0][0])") + if @_ && !-f $_[0][0]; + return $self->SUPER::source(@_); +} + +=head3 C<switches> + + my $switches = $perl->switches; + my @switches = $perl->switches; + $perl->switches( \@switches ); + +Getter/setter for the additional switches to pass to the perl executable. One +common switch would be to set an include directory: + + $perl->switches( ['-Ilib'] ); + +=cut + +sub switches { + my $self = shift; + unless (@_) { + return wantarray ? @{ $self->{switches} } : $self->{switches}; + } + my $switches = shift; + $self->{switches} = [@$switches]; # force a copy + return $self; +} + +############################################################################## + +=head3 C<get_stream> + + my $stream = $source->get_stream($parser); + +Returns a stream of the output generated by executing C<source>. Must be +passed an object that implements a C<make_iterator> method. Typically +this is a TAP::Parser instance. + +=cut + +sub get_stream { + my ( $self, $factory ) = @_; + + my @switches = $self->_switches; + my $path_sep = $Config{path_sep}; + my $path_pat = qr{$path_sep}; + + # Filter out any -I switches to be handled as libs later. + # + # Nasty kludge. It might be nicer if we got the libs separately + # although at least this way we find any -I switches that were + # supplied other then as explicit libs. + # + # We filter out any names containing colons because they will break + # PERL5LIB + my @libs; + my @filtered_switches; + for (@switches) { + if ( !/$path_pat/ && / ^ ['"]? -I ['"]? (.*?) ['"]? $ /x ) { + push @libs, $1; + } + else { + push @filtered_switches, $_; + } + } + @switches = @filtered_switches; + + my $setup = sub { + if (@libs) { + $ENV{PERL5LIB} + = join( $path_sep, grep {defined} @libs, $ENV{PERL5LIB} ); + } + }; + + # Cargo culted from comments seen elsewhere about VMS / environment + # variables. I don't know if this is actually necessary. + my $previous = $ENV{PERL5LIB}; + my $teardown = sub { + if ( defined $previous ) { + $ENV{PERL5LIB} = $previous; + } + else { + delete $ENV{PERL5LIB}; + } + }; + + # Taint mode ignores environment variables so we must retranslate + # PERL5LIB as -I switches and place PERL5OPT on the command line + # in order that it be seen. + if ( grep { $_ eq "-T" || $_ eq "-t" } @switches ) { + push @switches, $self->_libs2switches(@libs); + push @switches, split_shell( $ENV{PERL5OPT} ); + } + + my @command = $self->_get_command_for_switches(@switches) + or $self->_croak("No command found!"); + + return $factory->make_iterator( + { command => \@command, + merge => $self->merge, + setup => $setup, + teardown => $teardown, + } + ); +} + +sub _get_command_for_switches { + my $self = shift; + my @switches = @_; + my ( $file, @args ) = @{ $self->source }; + my $command = $self->_get_perl; + +# XXX we never need to quote if we treat the parts as atoms (except maybe vms) +#$file = qq["$file"] if ( $file =~ /\s/ ) && ( $file !~ /^".*"$/ ); + my @command = ( $command, @switches, $file, @args ); + return @command; +} + +sub _get_command { + my $self = shift; + return $self->_get_command_for_switches( $self->_switches ); +} + +sub _libs2switches { + my $self = shift; + return map {"-I$_"} grep {$_} @_; +} + +=head3 C<shebang> + +Get the shebang line for a script file. + + my $shebang = TAP::Parser::Source::Perl->shebang( $some_script ); + +May be called as a class method + +=cut + +{ + + # Global shebang cache. + my %shebang_for; + + sub _read_shebang { + my $file = shift; + local *TEST; + my $shebang; + if ( open( TEST, $file ) ) { + $shebang = <TEST>; + close(TEST) or print "Can't close $file. $!\n"; + } + else { + print "Can't open $file. $!\n"; + } + return $shebang; + } + + sub shebang { + my ( $class, $file ) = @_; + unless ( exists $shebang_for{$file} ) { + $shebang_for{$file} = _read_shebang($file); + } + return $shebang_for{$file}; + } +} + +=head3 C<get_taint> + +Decode any taint switches from a Perl shebang line. + + # $taint will be 't' + my $taint = TAP::Parser::Source::Perl->get_taint( '#!/usr/bin/perl -t' ); + + # $untaint will be undefined + my $untaint = TAP::Parser::Source::Perl->get_taint( '#!/usr/bin/perl' ); + +=cut + +sub get_taint { + my ( $class, $shebang ) = @_; + return + unless defined $shebang + && $shebang =~ /^#!.*\bperl.*\s-\w*([Tt]+)/; + return $1; +} + +sub _switches { + my $self = shift; + my ( $file, @args ) = @{ $self->source }; + my @switches = ( + $self->switches, + ); + + my $shebang = $self->shebang($file); + return unless defined $shebang; + + my $taint = $self->get_taint($shebang); + push @switches, "-$taint" if defined $taint; + + # Quote the argument if we're VMS, since VMS will downcase anything + # not quoted. + if (IS_VMS) { + for (@switches) { + $_ = qq["$_"]; + } + } + + return @switches; +} + +sub _get_perl { + my $self = shift; + return $ENV{HARNESS_PERL} if defined $ENV{HARNESS_PERL}; + return Win32::GetShortPathName($^X) if IS_WIN32; + return $^X; +} + +1; + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +=head2 Example + + package MyPerlSource; + + use strict; + use vars '@ISA'; + + use Carp qw( croak ); + use TAP::Parser::Source::Perl; + + @ISA = qw( TAP::Parser::Source::Perl ); + + sub source { + my ($self, $args) = @_; + if ($args) { + $self->{file} = $args->[0]; + return $self->SUPER::source($args); + } + return $self->SUPER::source; + } + + # use the version of perl from the shebang line in the test file + sub _get_perl { + my $self = shift; + if (my $shebang = $self->shebang( $self->{file} )) { + $shebang =~ /^#!(.*\bperl.*?)(?:(?:\s)|(?:$))/; + return $1 if $1; + } + return $self->SUPER::_get_perl(@_); + } + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Source>, + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler.pm new file mode 100755 index 00000000000..c7eb160f001 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler.pm @@ -0,0 +1,194 @@ +package TAP::Parser::SourceHandler; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); +use TAP::Parser::Iterator (); + +@ISA = qw(TAP::Object); + +=head1 NAME + +TAP::Parser::SourceHandler - Base class for different TAP source handlers + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + # abstract class - don't use directly! + # see TAP::Parser::IteratorFactory for general usage + + # must be sub-classed for use + package MySourceHandler; + use base qw( TAP::Parser::SourceHandler ); + sub can_handle { return $confidence_level } + sub make_iterator { return $iterator } + + # see example below for more details + +=head1 DESCRIPTION + +This is an abstract base class for L<TAP::Parser::Source> handlers / handlers. + +A C<TAP::Parser::SourceHandler> does whatever is necessary to produce & capture +a stream of TAP from the I<raw> source, and package it up in a +L<TAP::Parser::Iterator> for the parser to consume. + +C<SourceHandlers> must implement the I<source detection & handling> interface +used by L<TAP::Parser::IteratorFactory>. At 2 methods, the interface is pretty +simple: L</can_handle> and L</make_source>. + +Unless you're writing a new L<TAP::Parser::SourceHandler>, a plugin, or +subclassing L<TAP::Parser>, you probably won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<can_handle> + +I<Abstract method>. + + my $vote = $class->can_handle( $source ); + +C<$source> is a L<TAP::Parser::Source>. + +Returns a number between C<0> & C<1> reflecting how confidently the raw source +can be handled. For example, C<0> means the source cannot handle it, C<0.5> +means it may be able to, and C<1> means it definitely can. See +L<TAP::Parser::IteratorFactory/detect_source> for details on how this is used. + +=cut + +sub can_handle { + my ( $class, $args ) = @_; + $class->_croak( + "Abstract method 'can_handle' not implemented for $class!"); + return; +} + +=head3 C<make_iterator> + +I<Abstract method>. + + my $iterator = $class->make_iterator( $source ); + +C<$source> is a L<TAP::Parser::Source>. + +Returns a new L<TAP::Parser::Iterator> object for use by the L<TAP::Parser>. +C<croak>s on error. + +=cut + +sub make_iterator { + my ( $class, $args ) = @_; + $class->_croak( + "Abstract method 'make_iterator' not implemented for $class!"); + return; +} +1; + +__END__ + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview, and any +of the subclasses that ship with this module as an example. What follows is +a quick overview. + +Start by familiarizing yourself with L<TAP::Parser::Source> and +L<TAP::Parser::IteratorFactory>. L<TAP::Parser::SourceHandler::RawTAP> is +the easiest sub-class to use an an example. + +It's important to point out that if you want your subclass to be automatically +used by L<TAP::Parser> you'll have to and make sure it gets loaded somehow. +If you're using L<prove> you can write an L<App::Prove> plugin. If you're +using L<TAP::Parser> or L<TAP::Harness> directly (eg. through a custom script, +L<ExtUtils::MakeMaker>, or L<Module::Build>) you can use the C<config> option +which will cause L<TAP::Parser::IteratorFactory/load_sources> to load your +subclass). + +Don't forget to register your class with +L<TAP::Parser::IteratorFactory/register_handler>. + +=head2 Example + + package MySourceHandler; + + use strict; + use vars '@ISA'; # compat with older perls + + use MySourceHandler; # see TAP::Parser::SourceHandler + use TAP::Parser::IteratorFactory; + + @ISA = qw( TAP::Parser::SourceHandler ); + + TAP::Parser::IteratorFactory->register_handler( __PACKAGE__ ); + + sub can_handle { + my ( $class, $src ) = @_; + my $meta = $src->meta; + my $config = $src->config_for( $class ); + + if ($config->{accept_all}) { + return 1.0; + } elsif (my $file = $meta->{file}) { + return 0.0 unless $file->{exists}; + return 1.0 if $file->{lc_ext} eq '.tap'; + return 0.9 if $file->{shebang} && $file->{shebang} =~ /^#!.+tap/; + return 0.5 if $file->{text}; + return 0.1 if $file->{binary}; + } elsif ($meta->{scalar}) { + return 0.8 if $$raw_source_ref =~ /\d\.\.\d/; + return 0.6 if $meta->{has_newlines}; + } elsif ($meta->{array}) { + return 0.8 if $meta->{size} < 5; + return 0.6 if $raw_source_ref->[0] =~ /foo/; + return 0.5; + } elsif ($meta->{hash}) { + return 0.6 if $raw_source_ref->{foo}; + return 0.2; + } + + return 0; + } + + sub make_iterator { + my ($class, $source) = @_; + # this is where you manipulate the source and + # capture the stream of TAP in an iterator + # either pick a TAP::Parser::Iterator::* or write your own... + my $iterator = TAP::Parser::Iterator::Array->new([ 'foo', 'bar' ]); + return $iterator; + } + + 1; + +=head1 AUTHORS + +TAPx Developers. + +Source detection stuff added by Steve Purkis + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Source>, +L<TAP::Parser::Iterator>, +L<TAP::Parser::IteratorFactory>, +L<TAP::Parser::SourceHandler::Executable>, +L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::File>, +L<TAP::Parser::SourceHandler::Handle>, +L<TAP::Parser::SourceHandler::RawTAP> + +=cut + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Executable.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Executable.pm new file mode 100755 index 00000000000..5337abac543 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Executable.pm @@ -0,0 +1,185 @@ +package TAP::Parser::SourceHandler::Executable; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::SourceHandler (); +use TAP::Parser::IteratorFactory (); +use TAP::Parser::Iterator::Process (); + +@ISA = qw(TAP::Parser::SourceHandler); + +TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); + +=head1 NAME + +TAP::Parser::SourceHandler::Executable - Stream output from an executable TAP source + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Source; + use TAP::Parser::SourceHandler::Executable; + + my $source = TAP::Parser::Source->new->raw(['/usr/bin/ruby', 'mytest.rb']); + $source->assemble_meta; + + my $class = 'TAP::Parser::SourceHandler::Executable'; + my $vote = $class->can_handle( $source ); + my $iter = $class->make_iterator( $source ); + +=head1 DESCRIPTION + +This is an I<executable> L<TAP::Parser::SourceHandler> - it has 2 jobs: + +1. Figure out if the L<TAP::Parser::Source> it's given is an executable command +(L</can_handle>). + +2. Creates an iterator for executable commands (L</make_iterator>). + +Unless you're writing a plugin or subclassing L<TAP::Parser>, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<can_handle> + + my $vote = $class->can_handle( $source ); + +Only votes if $source looks like an executable file. Casts the following votes: + + 0.9 if it's a hash with an 'exec' key + 0.8 if it's a .sh file + 0.8 if it's a .bat file + 0.75 if it's got an execute bit set + +=cut + +sub can_handle { + my ( $class, $src ) = @_; + my $meta = $src->meta; + + if ( $meta->{is_file} ) { + my $file = $meta->{file}; + + # Note: we go in low so we can be out-voted + return 0.8 if $file->{lc_ext} eq '.sh'; + return 0.8 if $file->{lc_ext} eq '.bat'; + return 0.7 if $file->{execute}; + } + elsif ( $meta->{is_hash} ) { + return 0.9 if $src->raw->{exec}; + } + + return 0; +} + +=head3 C<make_iterator> + + my $iterator = $class->make_iterator( $source ); + +Returns a new L<TAP::Parser::Iterator::Process> for the source. +C<$source-E<gt>raw> must be in one of the following forms: + + { exec => [ @exec ] } + + [ @exec ] + + $file + +C<croak>s on error. + +=cut + +sub make_iterator { + my ( $class, $source ) = @_; + my $meta = $source->meta; + + my @command; + if ( $meta->{is_hash} ) { + @command = @{ $source->raw->{exec} || [] }; + } + elsif ( $meta->{is_scalar} ) { + @command = ${ $source->raw }; + } + elsif ( $meta->{is_array} ) { + @command = @{ $source->raw }; + } + + $class->_croak('No command found in $source->raw!') unless @command; + + $class->_autoflush( \*STDOUT ); + $class->_autoflush( \*STDERR ); + + return $class->iterator_class->new( + { command => \@command, + merge => $source->merge + } + ); +} + +=head3 C<iterator_class> + +The class of iterator to use, override if you're sub-classing. Defaults +to L<TAP::Parser::Iterator::Process>. + +=cut + +use constant iterator_class => 'TAP::Parser::Iterator::Process'; + +# Turns on autoflush for the handle passed +sub _autoflush { + my ( $class, $flushed ) = @_; + my $old_fh = select $flushed; + $| = 1; + select $old_fh; +} + +1; + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +=head2 Example + + package MyRubySourceHandler; + + use strict; + use vars '@ISA'; + + use Carp qw( croak ); + use TAP::Parser::SourceHandler::Executable; + + @ISA = qw( TAP::Parser::SourceHandler::Executable ); + + # expect $handler->(['mytest.rb', 'cmdline', 'args']); + sub make_iterator { + my ($self, $source) = @_; + my @test_args = @{ $source->test_args }; + my $rb_file = $test_args[0]; + croak("error: Ruby file '$rb_file' not found!") unless (-f $rb_file); + return $self->SUPER::raw_source(['/usr/bin/ruby', @test_args]); + } + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::IteratorFactory>, +L<TAP::Parser::SourceHandler>, +L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::File>, +L<TAP::Parser::SourceHandler::Handle>, +L<TAP::Parser::SourceHandler::RawTAP> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/File.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/File.pm new file mode 100755 index 00000000000..d44ace11483 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/File.pm @@ -0,0 +1,136 @@ +package TAP::Parser::SourceHandler::File; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::SourceHandler (); +use TAP::Parser::IteratorFactory (); +use TAP::Parser::Iterator::Stream (); + +@ISA = qw(TAP::Parser::SourceHandler); + +TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); + +=head1 NAME + +TAP::Parser::SourceHandler::File - Stream TAP from a text file. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Source; + use TAP::Parser::SourceHandler::File; + + my $source = TAP::Parser::Source->new->raw( \'file.tap' ); + $source->assemble_meta; + + my $class = 'TAP::Parser::SourceHandler::File'; + my $vote = $class->can_handle( $source ); + my $iter = $class->make_iterator( $source ); + +=head1 DESCRIPTION + +This is a I<raw TAP stored in a file> L<TAP::Parser::SourceHandler> - it has 2 jobs: + +1. Figure out if the I<raw> source it's given is a file containing raw TAP +output. See L<TAP::Parser::IteratorFactory> for more details. + +2. Takes raw TAP from the text file given, and converts into an iterator. + +Unless you're writing a plugin or subclassing L<TAP::Parser>, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<can_handle> + + my $vote = $class->can_handle( $source ); + +Only votes if $source looks like a regular file. Casts the following votes: + + 0.9 if it's a .tap file + 0.9 if it has an extension matching any given in user config. + +=cut + +sub can_handle { + my ( $class, $src ) = @_; + my $meta = $src->meta; + my $config = $src->config_for($class); + + return 0 unless $meta->{is_file}; + my $file = $meta->{file}; + return 0.9 if $file->{lc_ext} eq '.tap'; + + if ( my $exts = $config->{extensions} ) { + return 0.9 if grep { lc($_) eq $file->{lc_ext} } @$exts; + } + + return 0; +} + +=head3 C<make_iterator> + + my $iterator = $class->make_iterator( $source ); + +Returns a new L<TAP::Parser::Iterator::Stream> for the source. C<croak>s +on error. + +=cut + +sub make_iterator { + my ( $class, $source ) = @_; + + $class->_croak('$source->raw must be a scalar ref') + unless $source->meta->{is_scalar}; + + my $file = ${ $source->raw }; + my $fh; + open( $fh, '<', $file ) + or $class->_croak("error opening TAP source file '$file': $!"); + return $class->iterator_class->new($fh); +} + +=head3 C<iterator_class> + +The class of iterator to use, override if you're sub-classing. Defaults +to L<TAP::Parser::Iterator::Stream>. + +=cut + +use constant iterator_class => 'TAP::Parser::Iterator::Stream'; + +1; + +__END__ + +=head1 CONFIGURATION + + { + extensions => [ @case_insensitive_exts_to_match ] + } + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::SourceHandler>, +L<TAP::Parser::SourceHandler::Executable>, +L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::Handle>, +L<TAP::Parser::SourceHandler::RawTAP> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Handle.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Handle.pm new file mode 100755 index 00000000000..30594fa1e75 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Handle.pm @@ -0,0 +1,125 @@ +package TAP::Parser::SourceHandler::Handle; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::SourceHandler (); +use TAP::Parser::IteratorFactory (); +use TAP::Parser::Iterator::Stream (); + +@ISA = qw(TAP::Parser::SourceHandler); + +TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); + +=head1 NAME + +TAP::Parser::SourceHandler::Handle - Stream TAP from an IO::Handle or a GLOB. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Source; + use TAP::Parser::SourceHandler::Executable; + + my $source = TAP::Parser::Source->new->raw( \*TAP_FILE ); + $source->assemble_meta; + + my $class = 'TAP::Parser::SourceHandler::Handle'; + my $vote = $class->can_handle( $source ); + my $iter = $class->make_iterator( $source ); + +=head1 DESCRIPTION + +This is a I<raw TAP stored in an IO Handle> L<TAP::Parser::SourceHandler> class. It +has 2 jobs: + +1. Figure out if the L<TAP::Parser::Source> it's given is an L<IO::Handle> or +GLOB containing raw TAP output (L</can_handle>). + +2. Creates an iterator for IO::Handle's & globs (L</make_iterator>). + +Unless you're writing a plugin or subclassing L<TAP::Parser>, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<can_handle> + + my $vote = $class->can_handle( $source ); + +Casts the following votes: + + 0.9 if $source is an IO::Handle + 0.8 if $source is a glob + +=cut + +sub can_handle { + my ( $class, $src ) = @_; + my $meta = $src->meta; + + return 0.9 + if $meta->{is_object} + && UNIVERSAL::isa( $src->raw, 'IO::Handle' ); + + return 0.8 if $meta->{is_glob}; + + return 0; +} + +=head3 C<make_iterator> + + my $iterator = $class->make_iterator( $source ); + +Returns a new L<TAP::Parser::Iterator::Stream> for the source. + +=cut + +sub make_iterator { + my ( $class, $source ) = @_; + + $class->_croak('$source->raw must be a glob ref or an IO::Handle') + unless $source->meta->{is_glob} + || UNIVERSAL::isa( $source->raw, 'IO::Handle' ); + + return $class->iterator_class->new( $source->raw ); +} + +=head3 C<iterator_class> + +The class of iterator to use, override if you're sub-classing. Defaults +to L<TAP::Parser::Iterator::Stream>. + +=cut + +use constant iterator_class => 'TAP::Parser::Iterator::Stream'; + +1; + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::Iterator>, +L<TAP::Parser::Iterator::Stream>, +L<TAP::Parser::IteratorFactory>, +L<TAP::Parser::SourceHandler>, +L<TAP::Parser::SourceHandler::Executable>, +L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::File>, +L<TAP::Parser::SourceHandler::RawTAP> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Perl.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Perl.pm new file mode 100755 index 00000000000..f6513fd3156 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/Perl.pm @@ -0,0 +1,310 @@ +package TAP::Parser::SourceHandler::Perl; + +use strict; +use Config; +use vars qw($VERSION @ISA); + +use constant IS_WIN32 => ( $^O =~ /^(MS)?Win32$/ ); +use constant IS_VMS => ( $^O eq 'VMS' ); + +use TAP::Parser::SourceHandler::Executable (); +use TAP::Parser::IteratorFactory (); +use TAP::Parser::Iterator::Process (); +use TAP::Parser::Utils qw( split_shell ); + +@ISA = 'TAP::Parser::SourceHandler::Executable'; + +TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); + +=head1 NAME + +TAP::Parser::SourceHandler::Perl - Stream TAP from a Perl executable + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Source; + use TAP::Parser::SourceHandler::Perl; + + my $source = TAP::Parser::Source->new->raw( \'script.pl' ); + $source->assemble_meta; + + my $class = 'TAP::Parser::SourceHandler::Perl'; + my $vote = $class->can_handle( $source ); + my $iter = $class->make_iterator( $source ); + +=head1 DESCRIPTION + +This is a I<Perl> L<TAP::Parser::SourceHandler> - it has 2 jobs: + +1. Figure out if the L<TAP::Parser::Source> it's given is actually a Perl +script (L</can_handle>). + +2. Creates an iterator for Perl sources (L</make_iterator>). + +Unless you're writing a plugin or subclassing L<TAP::Parser>, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<can_handle> + + my $vote = $class->can_handle( $source ); + +Only votes if $source looks like a file. Casts the following votes: + + 0.9 if it has a shebang ala "#!...perl" + 0.8 if it's a .t file + 0.9 if it's a .pl file + 0.75 if it's in a 't' directory + 0.25 by default (backwards compat) + +=cut + +sub can_handle { + my ( $class, $source ) = @_; + my $meta = $source->meta; + + return 0 unless $meta->{is_file}; + my $file = $meta->{file}; + + if ( my $shebang = $file->{shebang} ) { + return 0.9 if $shebang =~ /^#!.*\bperl/; + } + + return 0.8 if $file->{lc_ext} eq '.t'; # vote higher than Executable + return 0.9 if $file->{lc_ext} eq '.pl'; + + return 0.75 if $file->{dir} =~ /^t\b/; # vote higher than Executable + + # backwards compat, always vote: + return 0.25; +} + +=head3 C<make_iterator> + + my $iterator = $class->make_iterator( $source ); + +Constructs & returns a new L<TAP::Parser::Iterator::Process> for the source. +Assumes C<$source-E<gt>raw> contains a reference to the perl script. C<croak>s +if the file could not be found. + +The command to run is built as follows: + + $perl @switches $perl_script @test_args + +The perl command to use is determined by L</get_perl>. The command generated +is guaranteed to preserve: + + PERL5LIB + PERL5OPT + Taint Mode, if set in the script's shebang + +I<Note:> the command generated will I<not> respect any shebang line defined in +your Perl script. This is only a problem if you have compiled a custom version +of Perl or if you want to use a specific version of Perl for one test and a +different version for another, for example: + + #!/path/to/a/custom_perl --some --args + #!/usr/local/perl-5.6/bin/perl -w + +Currently you need to write a plugin to get around this. + +=cut + +sub make_iterator { + my ( $class, $source ) = @_; + my $meta = $source->meta; + my $perl_script = ${ $source->raw }; + + $class->_croak("Cannot find ($perl_script)") unless $meta->{is_file}; + + # TODO: does this really need to be done here? + $class->_autoflush( \*STDOUT ); + $class->_autoflush( \*STDERR ); + + my @switches = $class->_switches($source); + my $path_sep = $Config{path_sep}; + my $path_re = qr{$path_sep}; + + # Filter out any -I switches to be handled as libs later. + # + # Nasty kludge. It might be nicer if we got the libs separately + # although at least this way we find any -I switches that were + # supplied other then as explicit libs. + # + # We filter out any names containing colons because they will break + # PERL5LIB + my @libs; + my @filtered_switches; + for (@switches) { + if ( !/$path_re/ && / ^ ['"]? -I ['"]? (.*?) ['"]? $ /x ) { + push @libs, $1; + } + else { + push @filtered_switches, $_; + } + } + @switches = @filtered_switches; + + my $setup = sub { + if (@libs) { + $ENV{PERL5LIB} + = join( $path_sep, grep {defined} @libs, $ENV{PERL5LIB} ); + } + }; + + # Cargo culted from comments seen elsewhere about VMS / environment + # variables. I don't know if this is actually necessary. + my $previous = $ENV{PERL5LIB}; + my $teardown = sub { + if ( defined $previous ) { + $ENV{PERL5LIB} = $previous; + } + else { + delete $ENV{PERL5LIB}; + } + }; + + # Taint mode ignores environment variables so we must retranslate + # PERL5LIB as -I switches and place PERL5OPT on the command line + # in order that it be seen. + if ( grep { $_ eq "-T" || $_ eq "-t" } @switches ) { + push @switches, $class->_libs2switches(@libs); + push @switches, split_shell( $ENV{PERL5OPT} ); + } + + my @command = $class->_get_command_for_switches( $source, @switches ) + or $class->_croak("No command found!"); + + return TAP::Parser::Iterator::Process->new( + { command => \@command, + merge => $source->merge, + setup => $setup, + teardown => $teardown, + } + ); +} + +sub _get_command_for_switches { + my ( $class, $source, @switches ) = @_; + my $file = ${ $source->raw }; + my @args = @{ $source->test_args || [] }; + my $command = $class->get_perl; + + # XXX don't need to quote if we treat the parts as atoms (except maybe vms) + #$file = qq["$file"] if ( $file =~ /\s/ ) && ( $file !~ /^".*"$/ ); + my @command = ( $command, @switches, $file, @args ); + return @command; +} + +sub _libs2switches { + my $class = shift; + return map {"-I$_"} grep {$_} @_; +} + +=head3 C<get_taint> + +Decode any taint switches from a Perl shebang line. + + # $taint will be 't' + my $taint = TAP::Parser::SourceHandler::Perl->get_taint( '#!/usr/bin/perl -t' ); + + # $untaint will be undefined + my $untaint = TAP::Parser::SourceHandler::Perl->get_taint( '#!/usr/bin/perl' ); + +=cut + +sub get_taint { + my ( $class, $shebang ) = @_; + return + unless defined $shebang + && $shebang =~ /^#!.*\bperl.*\s-\w*([Tt]+)/; + return $1; +} + +sub _switches { + my ( $class, $source ) = @_; + my $file = ${ $source->raw }; + my @args = @{ $source->test_args || [] }; + my @switches = @{ $source->switches || [] }; + my $shebang = $source->meta->{file}->{shebang}; + return unless defined $shebang; + + my $taint = $class->get_taint($shebang); + push @switches, "-$taint" if defined $taint; + + # Quote the argument if we're VMS, since VMS will downcase anything + # not quoted. + if (IS_VMS) { + for (@switches) { + $_ = qq["$_"]; + } + } + + return @switches; +} + +=head3 C<get_perl> + +Gets the version of Perl currently running the test suite. + +=cut + +sub get_perl { + my $class = shift; + return $ENV{HARNESS_PERL} if defined $ENV{HARNESS_PERL}; + return Win32::GetShortPathName($^X) if IS_WIN32; + return $^X; +} + +1; + +__END__ + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +=head2 Example + + package MyPerlSourceHandler; + + use strict; + use vars '@ISA'; + + use TAP::Parser::SourceHandler::Perl; + + @ISA = qw( TAP::Parser::SourceHandler::Perl ); + + # use the version of perl from the shebang line in the test file + sub get_perl { + my $self = shift; + if (my $shebang = $self->shebang( $self->{file} )) { + $shebang =~ /^#!(.*\bperl.*?)(?:(?:\s)|(?:$))/; + return $1 if $1; + } + return $self->SUPER::get_perl(@_); + } + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::IteratorFactory>, +L<TAP::Parser::SourceHandler>, +L<TAP::Parser::SourceHandler::Executable>, +L<TAP::Parser::SourceHandler::File>, +L<TAP::Parser::SourceHandler::Handle>, +L<TAP::Parser::SourceHandler::RawTAP> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/RawTAP.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/RawTAP.pm new file mode 100755 index 00000000000..c7048be98de --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/RawTAP.pm @@ -0,0 +1,131 @@ +package TAP::Parser::SourceHandler::RawTAP; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::SourceHandler (); +use TAP::Parser::IteratorFactory (); +use TAP::Parser::Iterator::Array (); + +@ISA = qw(TAP::Parser::SourceHandler); + +TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); + +=head1 NAME + +TAP::Parser::SourceHandler::RawTAP - Stream output from raw TAP in a scalar/array ref. + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Source; + use TAP::Parser::SourceHandler::RawTAP; + + my $source = TAP::Parser::Source->new->raw( \"1..1\nok 1\n" ); + $source->assemble_meta; + + my $class = 'TAP::Parser::SourceHandler::RawTAP'; + my $vote = $class->can_handle( $source ); + my $iter = $class->make_iterator( $source ); + +=head1 DESCRIPTION + +This is a I<raw TAP output> L<TAP::Parser::SourceHandler> - it has 2 jobs: + +1. Figure out if the L<TAP::Parser::Source> it's given is raw TAP output +(L</can_handle>). + +2. Creates an iterator for raw TAP output (L</make_iterator>). + +Unless you're writing a plugin or subclassing L<TAP::Parser>, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<can_handle> + + my $vote = $class->can_handle( $source ); + +Only votes if $source is an array, or a scalar with newlines. Casts the +following votes: + + 0.9 if it's a scalar with '..' in it + 0.7 if it's a scalar with 'ok' in it + 0.3 if it's just a scalar with newlines + 0.5 if it's an array + +=cut + +sub can_handle { + my ( $class, $src ) = @_; + my $meta = $src->meta; + + return 0 if $meta->{file}; + if ( $meta->{is_scalar} ) { + return 0 unless $meta->{has_newlines}; + return 0.9 if ${ $src->raw } =~ /\d\.\.\d/; + return 0.7 if ${ $src->raw } =~ /ok/; + return 0.3; + } + elsif ( $meta->{is_array} ) { + return 0.5; + } + return 0; +} + +=head3 C<make_iterator> + + my $iterator = $class->make_iterator( $source ); + +Returns a new L<TAP::Parser::Iterator::Array> for the source. +C<$source-E<gt>raw> must be an array ref, or a scalar ref. + +C<croak>s on error. + +=cut + +sub make_iterator { + my ( $class, $src ) = @_; + my $meta = $src->meta; + + my $tap_array; + if ( $meta->{is_scalar} ) { + $tap_array = [ split "\n" => ${ $src->raw } ]; + } + elsif ( $meta->{is_array} ) { + $tap_array = $src->raw; + } + + $class->_croak('No raw TAP found in $source->raw') + unless scalar $tap_array; + + return TAP::Parser::Iterator::Array->new($tap_array); +} + +1; + +=head1 SUBCLASSING + +Please see L<TAP::Parser/SUBCLASSING> for a subclassing overview. + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::IteratorFactory>, +L<TAP::Parser::SourceHandler>, +L<TAP::Parser::SourceHandler::Executable>, +L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::File>, +L<TAP::Parser::SourceHandler::Handle> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/pgTAP.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/pgTAP.pm new file mode 100755 index 00000000000..aeaf5365ff5 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/SourceHandler/pgTAP.pm @@ -0,0 +1,253 @@ +package TAP::Parser::SourceHandler::pgTAP; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Parser::IteratorFactory (); +use TAP::Parser::Iterator::Process (); + +@ISA = qw(TAP::Parser::SourceHandler); +TAP::Parser::IteratorFactory->register_handler(__PACKAGE__); + +=head1 NAME + +TAP::Parser::SourceHandler::pgTAP - Stream TAP from pgTAP test scripts + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + +In F<Build.PL> for your application with pgTAP tests in F<t/*.pg>: + + Module::Build->new( + module_name => 'MyApp', + test_file_exts => [qw(.t .pg)], + use_tap_harness => 1, + tap_harness_args => { + sources => { + Perl => undef, + pgTAP => { + dbname => 'try', + username => 'postgres', + suffix => '.pg', + }, + } + }, + build_requires => { + 'Module::Build' => '0.30', + 'TAP::Parser::SourceHandler::pgTAP' => '3.19', + }, + )->create_build_script; + +If you're using L<C<prove>|prove>: + + prove --source Perl \ + --source pgTAP --pgtap-option dbname=try \ + --pgtap-option username=postgres \ + --pgtap-option suffix=.pg + +Direct use: + + use TAP::Parser::Source; + use TAP::Parser::SourceHandler::pgTAP; + + my $source = TAP::Parser::Source->new->raw(\'mytest.pg'); + $source->config({ pgTAP => { + dbname => 'testing', + username => 'postgres', + suffix => '.pg', + }); + $source->assemble_meta; + + my $class = 'TAP::Parser::SourceHandler::pgTAP'; + my $vote = $class->can_handle( $source ); + my $iter = $class->make_iterator( $source ); + +=head1 DESCRIPTION + +This source handler executes pgTAP tests. It does two things: + +=over + +=item 1. + +Looks at the L<TAP::Parser::Source> passed to it to determine whether or not +the source in question is in fact a pgTAP test (L</can_handle>). + +=item 2. + +Creates an iterator that will call C<psql> to run the pgTAP tests +(L</make_iterator>). + +=back + +Unless you're writing a plugin or subclassing L<TAP::Parser>, you probably +won't need to use this module directly. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<can_handle> + + my $vote = $class->can_handle( $source ); + +Looks at the source to determine whether or not it's a pgTAP test file and +returns a score for how likely it is in fact a pgTAP test file. The scores are +as follows: + + 1 if it has a suffix equal to that in the "suffix" config + 1 if its suffix is ".pg" + 0.8 if its suffix is ".sql" + 0.75 if its suffix is ".s" + +The latter two scores are subject to change, so try to name your pgTAP tests +ending in ".pg" or specify a suffix in the configuration to be sure. + +=cut + +sub can_handle { + my ( $class, $source ) = @_; + my $meta = $source->meta; + + return 0 unless $meta->{is_file}; + + my $suf = $meta->{file}{lc_ext}; + + # If the config specifies a suffix, it's required. + if ( my $config = $source->config_for('pgTAP') ) { + if ( defined $config->{suffix} ) { + return $suf eq $config->{suffix} ? 1 : 0; + } + } + + # Otherwise, return a score for our supported suffixes. + my %score_for = ( + '.pg' => 0.9, + '.sql' => 0.8, + '.s' => 0.75, + ); + return $score_for{$suf} || 0; +} + +=head3 C<make_iterator> + + my $iterator = $class->make_iterator( $source ); + +Returns a new L<TAP::Parser::Iterator::Process> for the source. C<< +$source->raw >> must be either a file name or a scalar reference to the file +name. + +The pgTAP tests are run by executing C<psql>, the PostgreSQL command-line +utility. A number of arguments are passed to it, many of which you can effect +by setting up the source source configuration. The configuration must be a +hash reference, and supports the following keys: + +=over + +=item C<psql> + +The path to the C<psql> command. Defaults to simply "psql", which should work +well enough if it's in your path. + +=item C<dbname> + +The database to which to connect to run the tests. Defaults to the value of +the C<$PGDATABASE> environment variable or, if not set, to the system +username. + +=item C<username> + +The PostgreSQL username to use to connect to PostgreSQL. If not specified, no +username will be used, in which case C<psql> will fall back on either the +C<$PGUSER> environment variable or, if not set, the system username. + +=item C<host> + +Specifies the host name of the machine to which to connect to the PostgreSQL +server. If the value begins with a slash, it is used as the directory for the +Unix-domain socket. Defaults to the value of the C<$PGDATABASE> environment +variable or, if not set, the local host. + +=item C<port> + +Specifies the TCP port or the local Unix-domain socket file extension on which +the server is listening for connections. Defaults to the value of the +C<$PGPORT> environment variable or, if not set, to the port specified at the +time C<psql> was compiled, usually 5432. + +=begin comment + +=item C<search_path> + +The schema search path to use during the execution of the tests. Useful for +overriding the default search path and you have pgTAP installed in a schema +not included in that search path. + +=end comment + +=back + +=cut + +sub make_iterator { + my ( $class, $source ) = @_; + my $config = $source->config_for('pgTAP'); + + my @command = ( $config->{psql} || 'psql' ); + push @command, qw( + --no-psqlrc + --no-align + --quiet + --pset pager= + --pset tuples_only=true + --set ON_ERROR_ROLLBACK=1 + --set ON_ERROR_STOP=1 + ); + + for (qw(username host port dbname)) { + push @command, "--$_" => $config->{$_} if defined $config->{$_}; + } + + my $fn = ref $source->raw ? ${ $source->raw } : $source->raw; + $class->_croak( + 'No such file or directory: ' . ( defined $fn ? $fn : '' ) ) + unless $fn && -e $fn; + + push @command, '--file', $fn; + + # XXX I'd like a way to be able to specify environment variables to set when + # the iterator executes the command... + # local $ENV{PGOPTIONS} = "--search_path=$config->{search_path}" + # if $config->{search_path}; + + return TAP::Parser::Iterator::Process->new( + { command => \@command, + merge => $source->merge + } + ); +} + +=head1 SEE ALSO + +L<TAP::Object>, +L<TAP::Parser>, +L<TAP::Parser::IteratorFactory>, +L<TAP::Parser::SourceHandler>, +L<TAP::Parser::SourceHandler::Executable>, +L<TAP::Parser::SourceHandler::Perl>, +L<TAP::Parser::SourceHandler::File>, +L<TAP::Parser::SourceHandler::Handle>, +L<TAP::Parser::SourceHandler::RawTAP> + +=head1 AUTHOR + +David E. Wheeler <dwheeler@cpan.org> + +=cut diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/Utils.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/Utils.pm new file mode 100755 index 00000000000..21769b1a3f0 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/Utils.pm @@ -0,0 +1,72 @@ +package TAP::Parser::Utils; + +use strict; +use Exporter; +use vars qw($VERSION @ISA @EXPORT_OK); + +@ISA = qw( Exporter ); +@EXPORT_OK = qw( split_shell ); + +=head1 NAME + +TAP::Parser::Utils - Internal TAP::Parser utilities + +=head1 VERSION + +Version 3.20 + +=cut + +$VERSION = '3.20'; + +=head1 SYNOPSIS + + use TAP::Parser::Utils qw( split_shell ) + my @switches = split_shell( $arg ); + +=head1 DESCRIPTION + +B<FOR INTERNAL USE ONLY!> + +=head2 INTERFACE + +=head3 C<split_shell> + +Shell style argument parsing. Handles backslash escaping, single and +double quoted strings but not shell substitutions. + +Pass one or more strings containing shell escaped arguments. The return +value is an array of arguments parsed from the input strings according +to (approximate) shell parsing rules. It's legal to pass C<undef> in +which case an empty array will be returned. That makes it possible to + + my @args = split_shell( $ENV{SOME_ENV_VAR} ); + +without worrying about whether the environment variable exists. + +This is used to split HARNESS_PERL_ARGS into individual switches. + +=cut + +sub split_shell { + my @parts = (); + + for my $switch ( grep defined && length, @_ ) { + push @parts, $1 while $switch =~ / + ( + (?: [^\\"'\s]+ + | \\. + | " (?: \\. | [^"] )* " + | ' (?: \\. | [^'] )* ' + )+ + ) /xg; + } + + for (@parts) { + s/ \\(.) | ['"] /defined $1 ? $1 : ''/exg; + } + + return @parts; +} + +1; diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/YAMLish/Reader.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/YAMLish/Reader.pm new file mode 100755 index 00000000000..04ea4d10bd6 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/YAMLish/Reader.pm @@ -0,0 +1,333 @@ +package TAP::Parser::YAMLish::Reader; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); + +@ISA = 'TAP::Object'; +$VERSION = '3.20'; + +# TODO: +# Handle blessed object syntax + +# Printable characters for escapes +my %UNESCAPES = ( + z => "\x00", a => "\x07", t => "\x09", + n => "\x0a", v => "\x0b", f => "\x0c", + r => "\x0d", e => "\x1b", '\\' => '\\', +); + +my $QQ_STRING = qr{ " (?:\\. | [^"])* " }x; +my $HASH_LINE = qr{ ^ ($QQ_STRING|\S+) \s* : \s* (?: (.+?) \s* )? $ }x; +my $IS_HASH_KEY = qr{ ^ [\w\'\"] }x; +my $IS_END_YAML = qr{ ^ \.\.\. \s* $ }x; +my $IS_QQ_STRING = qr{ ^ $QQ_STRING $ }x; + +# new() implementation supplied by TAP::Object + +sub read { + my $self = shift; + my $obj = shift; + + die "Must have a code reference to read input from" + unless ref $obj eq 'CODE'; + + $self->{reader} = $obj; + $self->{capture} = []; + + # Prime the reader + $self->_next; + return unless $self->{next}; + + my $doc = $self->_read; + + # The terminator is mandatory otherwise we'd consume a line from the + # iterator that doesn't belong to us. If we want to remove this + # restriction we'll have to implement look-ahead in the iterators. + # Which might not be a bad idea. + my $dots = $self->_peek; + die "Missing '...' at end of YAMLish" + unless defined $dots + and $dots =~ $IS_END_YAML; + + delete $self->{reader}; + delete $self->{next}; + + return $doc; +} + +sub get_raw { join( "\n", grep defined, @{ shift->{capture} || [] } ) . "\n" } + +sub _peek { + my $self = shift; + return $self->{next} unless wantarray; + my $line = $self->{next}; + $line =~ /^ (\s*) (.*) $ /x; + return ( $2, length $1 ); +} + +sub _next { + my $self = shift; + die "_next called with no reader" + unless $self->{reader}; + my $line = $self->{reader}->(); + $self->{next} = $line; + push @{ $self->{capture} }, $line; +} + +sub _read { + my $self = shift; + + my $line = $self->_peek; + + # Do we have a document header? + if ( $line =~ /^ --- (?: \s* (.+?) \s* )? $/x ) { + $self->_next; + + return $self->_read_scalar($1) if defined $1; # Inline? + + my ( $next, $indent ) = $self->_peek; + + if ( $next =~ /^ - /x ) { + return $self->_read_array($indent); + } + elsif ( $next =~ $IS_HASH_KEY ) { + return $self->_read_hash( $next, $indent ); + } + elsif ( $next =~ $IS_END_YAML ) { + die "Premature end of YAMLish"; + } + else { + die "Unsupported YAMLish syntax: '$next'"; + } + } + else { + die "YAMLish document header not found"; + } +} + +# Parse a double quoted string +sub _read_qq { + my $self = shift; + my $str = shift; + + unless ( $str =~ s/^ " (.*?) " $/$1/x ) { + die "Internal: not a quoted string"; + } + + $str =~ s/\\"/"/gx; + $str =~ s/ \\ ( [tartan\\favez] | x([0-9a-fA-F]{2}) ) + / (length($1) > 1) ? pack("H2", $2) : $UNESCAPES{$1} /gex; + return $str; +} + +# Parse a scalar string to the actual scalar +sub _read_scalar { + my $self = shift; + my $string = shift; + + return undef if $string eq '~'; + return {} if $string eq '{}'; + return [] if $string eq '[]'; + + if ( $string eq '>' || $string eq '|' ) { + + my ( $line, $indent ) = $self->_peek; + die "Multi-line scalar content missing" unless defined $line; + + my @multiline = ($line); + + while (1) { + $self->_next; + my ( $next, $ind ) = $self->_peek; + last if $ind < $indent; + + my $pad = $string eq '|' ? ( ' ' x ( $ind - $indent ) ) : ''; + push @multiline, $pad . $next; + } + + return join( ( $string eq '>' ? ' ' : "\n" ), @multiline ) . "\n"; + } + + if ( $string =~ /^ ' (.*) ' $/x ) { + ( my $rv = $1 ) =~ s/''/'/g; + return $rv; + } + + if ( $string =~ $IS_QQ_STRING ) { + return $self->_read_qq($string); + } + + if ( $string =~ /^['"]/ ) { + + # A quote with folding... we don't support that + die __PACKAGE__ . " does not support multi-line quoted scalars"; + } + + # Regular unquoted string + return $string; +} + +sub _read_nested { + my $self = shift; + + my ( $line, $indent ) = $self->_peek; + + if ( $line =~ /^ -/x ) { + return $self->_read_array($indent); + } + elsif ( $line =~ $IS_HASH_KEY ) { + return $self->_read_hash( $line, $indent ); + } + else { + die "Unsupported YAMLish syntax: '$line'"; + } +} + +# Parse an array +sub _read_array { + my ( $self, $limit ) = @_; + + my $ar = []; + + while (1) { + my ( $line, $indent ) = $self->_peek; + last + if $indent < $limit + || !defined $line + || $line =~ $IS_END_YAML; + + if ( $indent > $limit ) { + die "Array line over-indented"; + } + + if ( $line =~ /^ (- \s+) \S+ \s* : (?: \s+ | $ ) /x ) { + $indent += length $1; + $line =~ s/-\s+//; + push @$ar, $self->_read_hash( $line, $indent ); + } + elsif ( $line =~ /^ - \s* (.+?) \s* $/x ) { + die "Unexpected start of YAMLish" if $line =~ /^---/; + $self->_next; + push @$ar, $self->_read_scalar($1); + } + elsif ( $line =~ /^ - \s* $/x ) { + $self->_next; + push @$ar, $self->_read_nested; + } + elsif ( $line =~ $IS_HASH_KEY ) { + $self->_next; + push @$ar, $self->_read_hash( $line, $indent, ); + } + else { + die "Unsupported YAMLish syntax: '$line'"; + } + } + + return $ar; +} + +sub _read_hash { + my ( $self, $line, $limit ) = @_; + + my $indent; + my $hash = {}; + + while (1) { + die "Badly formed hash line: '$line'" + unless $line =~ $HASH_LINE; + + my ( $key, $value ) = ( $self->_read_scalar($1), $2 ); + $self->_next; + + if ( defined $value ) { + $hash->{$key} = $self->_read_scalar($value); + } + else { + $hash->{$key} = $self->_read_nested; + } + + ( $line, $indent ) = $self->_peek; + last + if $indent < $limit + || !defined $line + || $line =~ $IS_END_YAML; + } + + return $hash; +} + +1; + +__END__ + +=pod + +=head1 NAME + +TAP::Parser::YAMLish::Reader - Read YAMLish data from iterator + +=head1 VERSION + +Version 3.20 + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +Note that parts of this code were derived from L<YAML::Tiny> with the +permission of Adam Kennedy. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + +The constructor C<new> creates and returns an empty +C<TAP::Parser::YAMLish::Reader> object. + + my $reader = TAP::Parser::YAMLish::Reader->new; + +=head2 Instance Methods + +=head3 C<read> + + my $got = $reader->read($iterator); + +Read YAMLish from a L<TAP::Parser::Iterator> and return the data structure it +represents. + +=head3 C<get_raw> + + my $source = $reader->get_source; + +Return the raw YAMLish source from the most recent C<read>. + +=head1 AUTHOR + +Andy Armstrong, <andy@hexten.net> + +Adam Kennedy wrote L<YAML::Tiny> which provided the template and many of +the YAML matching regular expressions for this module. + +=head1 SEE ALSO + +L<YAML::Tiny>, L<YAML>, L<YAML::Syck>, L<Config::Tiny>, L<CSS::Tiny>, +L<http://use.perl.org/~Alias/journal/29427> + +=head1 COPYRIGHT + +Copyright 2007-2008 Andy Armstrong. + +Portions copyright 2006-2008 Adam Kennedy. + +This program is free software; you can redistribute +it and/or modify it under the same terms as Perl itself. + +The full text of the license can be found in the +LICENSE file included with this module. + +=cut + diff --git a/Master/tlpkg/tlperl/lib/TAP/Parser/YAMLish/Writer.pm b/Master/tlpkg/tlperl/lib/TAP/Parser/YAMLish/Writer.pm new file mode 100755 index 00000000000..9dfbf74da09 --- /dev/null +++ b/Master/tlpkg/tlperl/lib/TAP/Parser/YAMLish/Writer.pm @@ -0,0 +1,255 @@ +package TAP::Parser::YAMLish::Writer; + +use strict; +use vars qw($VERSION @ISA); + +use TAP::Object (); + +@ISA = 'TAP::Object'; +$VERSION = '3.20'; + +my $ESCAPE_CHAR = qr{ [ \x00-\x1f \" ] }x; +my $ESCAPE_KEY = qr{ (?: ^\W ) | $ESCAPE_CHAR }x; + +my @UNPRINTABLE = qw( + z x01 x02 x03 x04 x05 x06 a + x08 t n v f r x0e x0f + x10 x11 x12 x13 x14 x15 x16 x17 + x18 x19 x1a e x1c x1d x1e x1f +); + +# new() implementation supplied by TAP::Object + +sub write { + my $self = shift; + + die "Need something to write" + unless @_; + + my $obj = shift; + my $out = shift || \*STDOUT; + + die "Need a reference to something I can write to" + unless ref $out; + + $self->{writer} = $self->_make_writer($out); + + $self->_write_obj( '---', $obj ); + $self->_put('...'); + + delete $self->{writer}; +} + +sub _make_writer { + my $self = shift; + my $out = shift; + + my $ref = ref $out; + + if ( 'CODE' eq $ref ) { + return $out; + } + elsif ( 'ARRAY' eq $ref ) { + return sub { push @$out, shift }; + } + elsif ( 'SCALAR' eq $ref ) { + return sub { $$out .= shift() . "\n" }; + } + elsif ( 'GLOB' eq $ref || 'IO::Handle' eq $ref ) { + return sub { print $out shift(), "\n" }; + } + + die "Can't write to $out"; +} + +sub _put { + my $self = shift; + $self->{writer}->( join '', @_ ); +} + +sub _enc_scalar { + my $self = shift; + my $val = shift; + my $rule = shift; + + return '~' unless defined $val; + + if ( $val =~ /$rule/ ) { + $val =~ s/\\/\\\\/g; + $val =~ s/"/\\"/g; + $val =~ s/ ( [\x00-\x1f] ) / '\\' . $UNPRINTABLE[ ord($1) ] /gex; + return qq{"$val"}; + } + + if ( length($val) == 0 or $val =~ /\s/ ) { + $val =~ s/'/''/; + return "'$val'"; + } + + return $val; +} + +sub _write_obj { + my $self = shift; + my $prefix = shift; + my $obj = shift; + my $indent = shift || 0; + + if ( my $ref = ref $obj ) { + my $pad = ' ' x $indent; + if ( 'HASH' eq $ref ) { + if ( keys %$obj ) { + $self->_put($prefix); + for my $key ( sort keys %$obj ) { + my $value = $obj->{$key}; + $self->_write_obj( + $pad . $self->_enc_scalar( $key, $ESCAPE_KEY ) . ':', + $value, $indent + 1 + ); + } + } + else { + $self->_put( $prefix, ' {}' ); + } + } + elsif ( 'ARRAY' eq $ref ) { + if (@$obj) { + $self->_put($prefix); + for my $value (@$obj) { + $self->_write_obj( + $pad . '-', $value, + $indent + 1 + ); + } + } + else { + $self->_put( $prefix, ' []' ); + } + } + else { + die "Don't know how to encode $ref"; + } + } + else { + $self->_put( $prefix, ' ', $self->_enc_scalar( $obj, $ESCAPE_CHAR ) ); + } +} + +1; + +__END__ + +=pod + +=head1 NAME + +TAP::Parser::YAMLish::Writer - Write YAMLish data + +=head1 VERSION + +Version 3.20 + +=head1 SYNOPSIS + + use TAP::Parser::YAMLish::Writer; + + my $data = { + one => 1, + two => 2, + three => [ 1, 2, 3 ], + }; + + my $yw = TAP::Parser::YAMLish::Writer->new; + + # Write to an array... + $yw->write( $data, \@some_array ); + + # ...an open file handle... + $yw->write( $data, $some_file_handle ); + + # ...a string ... + $yw->write( $data, \$some_string ); + + # ...or a closure + $yw->write( $data, sub { + my $line = shift; + print "$line\n"; + } ); + +=head1 DESCRIPTION + +Encodes a scalar, hash reference or array reference as YAMLish. + +=head1 METHODS + +=head2 Class Methods + +=head3 C<new> + + my $writer = TAP::Parser::YAMLish::Writer->new; + +The constructor C<new> creates and returns an empty +C<TAP::Parser::YAMLish::Writer> object. + +=head2 Instance Methods + +=head3 C<write> + + $writer->write($obj, $output ); + +Encode a scalar, hash reference or array reference as YAML. + + my $writer = sub { + my $line = shift; + print SOMEFILE "$line\n"; + }; + + my $data = { + one => 1, + two => 2, + three => [ 1, 2, 3 ], + }; + + my $yw = TAP::Parser::YAMLish::Writer->new; + $yw->write( $data, $writer ); + + +The C< $output > argument may be: + +=over + +=item * a reference to a scalar to append YAML to + +=item * the handle of an open file + +=item * a reference to an array into which YAML will be pushed + +=item * a code reference + +=back + +If you supply a code reference the subroutine will be called once for +each line of output with the line as its only argument. Passed lines +will have no trailing newline. + +=head1 AUTHOR + +Andy Armstrong, <andy@hexten.net> + +=head1 SEE ALSO + +L<YAML::Tiny>, L<YAML>, L<YAML::Syck>, L<Config::Tiny>, L<CSS::Tiny>, +L<http://use.perl.org/~Alias/journal/29427> + +=head1 COPYRIGHT + +Copyright 2007-2008 Andy Armstrong. + +This program is free software; you can redistribute +it and/or modify it under the same terms as Perl itself. + +The full text of the license can be found in the +LICENSE file included with this module. + +=cut + |