From 3672f46ee3a66ee1dbfb12e2d77b3c40ca269f5f Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Mar 2026 09:37:53 +0100 Subject: [PATCH 1/4] Add jprove wrapper and import prove/App::Prove/IO::Select - Add prove script to src/main/perl/bin/prove - Add App::Prove and App::Prove::State modules - Add IO::Select module (required by TAP::Parser::Multiplexer) - Create jprove wrapper script (similar to jcpan) - Update config.yaml to import Test-Harness tests to perl5_t/ Test-Harness test summary (from perl5_t/Test-Harness/): - Many tests pass: base.t, aggregator.t, callbacks.t, console.t, errors.t, file.t, grammar.t, harness-bailout.t, harness-subclass.t, iterators.t, nested.t, object.t, results.t, bailout.t, env_opts.t - Some tests have partial failures due to missing features - regression.t is slow (2000+ subtests) but works Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/import-perl5/config.yaml | 21 + jprove | 7 + .../org/perlonjava/core/Configuration.java | 2 +- src/main/perl/bin/prove | 407 +++++++++ src/main/perl/lib/App/Prove.pm | 817 ++++++++++++++++++ src/main/perl/lib/App/Prove/State.pm | 548 ++++++++++++ src/main/perl/lib/App/Prove/State/Result.pm | 233 +++++ .../perl/lib/App/Prove/State/Result/Test.pm | 152 ++++ src/main/perl/lib/IO/Select.pm | 417 +++++++++ 9 files changed, 2603 insertions(+), 1 deletion(-) create mode 100755 jprove create mode 100644 src/main/perl/bin/prove create mode 100644 src/main/perl/lib/App/Prove.pm create mode 100644 src/main/perl/lib/App/Prove/State.pm create mode 100644 src/main/perl/lib/App/Prove/State/Result.pm create mode 100644 src/main/perl/lib/App/Prove/State/Result/Test.pm create mode 100644 src/main/perl/lib/IO/Select.pm diff --git a/dev/import-perl5/config.yaml b/dev/import-perl5/config.yaml index 483bb8de7..01ed50b94 100644 --- a/dev/import-perl5/config.yaml +++ b/dev/import-perl5/config.yaml @@ -462,6 +462,10 @@ imports: target: src/main/perl/lib/IO/Socket type: directory + # IO::Select - OO interface to select() (required by TAP::Parser::Multiplexer) + - source: perl5/dist/IO/lib/IO/Select.pm + target: src/main/perl/lib/IO/Select.pm + # Phase 2: IO::Zlib - Compressed I/O (for Archive::Tar) - source: perl5/cpan/IO-Zlib/Zlib.pm target: src/main/perl/lib/IO/Zlib.pm @@ -646,6 +650,23 @@ imports: target: perl5_t/constant type: directory + # prove script - Test harness command-line tool (required by jprove wrapper) + - source: perl5/cpan/Test-Harness/bin/prove + target: src/main/perl/bin/prove + + # App::Prove - prove application framework + - source: perl5/cpan/Test-Harness/lib/App/Prove.pm + target: src/main/perl/lib/App/Prove.pm + + - source: perl5/cpan/Test-Harness/lib/App/Prove + target: src/main/perl/lib/App/Prove + type: directory + + # Test-Harness tests + - source: perl5/cpan/Test-Harness/t + target: perl5_t/Test-Harness + type: directory + # Add more imports below as needed # Example with minimal fields: # - source: perl5/lib/SomeModule.pm diff --git a/jprove b/jprove new file mode 100755 index 000000000..98914e6d6 --- /dev/null +++ b/jprove @@ -0,0 +1,7 @@ +#!/bin/bash +# +# jprove - Test Harness for PerlOnJava (Unix wrapper) +# Runs the standard prove script with jperl +# +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +exec "$SCRIPT_DIR/jperl" "$SCRIPT_DIR/src/main/perl/bin/prove" "$@" diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index e669f8608..648d45671 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "9470032eb"; + public static final String gitCommitId = "1d09334fe"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/perl/bin/prove b/src/main/perl/bin/prove new file mode 100644 index 000000000..836038296 --- /dev/null +++ b/src/main/perl/bin/prove @@ -0,0 +1,407 @@ +#!/usr/bin/perl -w + +BEGIN { pop @INC if $INC[-1] eq '.' } +use strict; +use warnings; +use App::Prove; + +my $app = App::Prove->new; +$app->process_args(@ARGV); +exit( $app->run ? 0 : 1 ); + +__END__ + +=head1 NAME + +prove - Run tests through a TAP harness. + +=head1 USAGE + + prove [options] [files or directories] + +=head1 OPTIONS + +Boolean options: + + -v, --verbose Print all test lines. Also sets TEST_VERBOSE + -l, --lib Add 'lib' to the path for your tests (-Ilib). + -b, --blib Add 'blib/lib' and 'blib/arch' to the path for + your tests + -s, --shuffle Run the tests in random order. + -c, --color Colored test output (default). + --nocolor Do not color test output. + --count Show the X/Y test count when not verbose + (default) + --nocount Disable the X/Y test count. + -D --dry Dry run. Show test that would have run. + -f, --failures Show failed tests. + -o, --comments Show comments. + --ignore-exit Ignore exit status from test scripts. + -m, --merge Merge test scripts' STDERR with their STDOUT. + -r, --recurse Recursively descend into directories. + --reverse Run the tests in reverse order. + -q, --quiet Suppress some test output while running tests. + -Q, --QUIET Only print summary results. + -p, --parse Show full list of TAP parse errors, if any. + --directives Only show results with TODO or SKIP directives. + --timer Print elapsed time after each test. + --trap Trap Ctrl-C and print summary on interrupt. + --normalize Normalize TAP output in verbose output + -T Enable tainting checks. + -t Enable tainting warnings. + -W Enable fatal warnings. + -w Enable warnings. + -h, --help Display this help + -?, Display this help + -V, --version Display the version + -H, --man Longer manpage for prove + --norc Don't process default .proverc + +Options that take arguments: + + -I Library paths to include. + -P Load plugin (searches App::Prove::Plugin::*.) + -M Load a module. + -e, --exec Interpreter to run the tests ('' for compiled + tests.) + --ext Set the extension for tests (default '.t') + --harness Define test harness to use. See TAP::Harness. + --formatter Result formatter to use. See FORMATTERS. + --source Load and/or configure a SourceHandler. See + SOURCE HANDLERS. + -a, --archive out.tgz Store the resulting TAP in an archive file. + -j, --jobs N Run N test jobs in parallel (try 9.) + --state=opts Control prove's persistent state. + --statefile=file Use `file` instead of `.prove` for state + --rc=rcfile Process options from rcfile + --rules Rules for parallel vs sequential processing. + +=head1 NOTES + +=head2 .proverc + +If F<~/.proverc> or F<./.proverc> exist they will be read and any +options they contain processed before the command line options. Options +in F<.proverc> are specified in the same way as command line options: + + # .proverc + --state=hot,fast,save + -j9 + +Additional option files may be specified with the C<--rc> option. +Default option file processing is disabled by the C<--norc> option. + +Under Windows and VMS the option file is named F<_proverc> rather than +F<.proverc> and is sought only in the current directory. + +=head2 Reading from C + +If you have a list of tests (or URLs, or anything else you want to test) in a +file, you can add them to your tests by using a '-': + + prove - < my_list_of_things_to_test.txt + +See the C in the C directory of this distribution. + +=head2 Default Test Directory + +If no files or directories are supplied, C looks for all files +matching the pattern C. + +=head2 Colored Test Output + +Colored test output using L is the default, but +if output is not to a terminal, color is disabled. You can override this by +adding the C<--color> switch. + +Color support requires L and, on windows platforms, also +L. If the necessary module(s) are not installed +colored output will not be available. + +=head2 Exit Code + +If the tests fail C will exit with non-zero status. + +=head2 Arguments to Tests + +It is possible to supply arguments to tests. To do so separate them from +prove's own arguments with the arisdottle, '::'. For example + + prove -v t/mytest.t :: --url http://example.com + +would run F with the options '--url http://example.com'. +When running multiple tests they will each receive the same arguments. + +=head2 C<--exec> + +Normally you can just pass a list of Perl tests and the harness will know how +to execute them. However, if your tests are not written in Perl or if you +want all tests invoked exactly the same way, use the C<-e>, or C<--exec> +switch: + + prove --exec '/usr/bin/ruby -w' t/ + prove --exec '/usr/bin/perl -Tw -mstrict -Ilib' t/ + prove --exec '/path/to/my/customer/exec' + +=head2 C<--merge> + +If you need to make sure your diagnostics are displayed in the correct +order relative to test results you can use the C<--merge> option to +merge the test scripts' STDERR into their STDOUT. + +This guarantees that STDOUT (where the test results appear) and STDERR +(where the diagnostics appear) will stay in sync. The harness will +display any diagnostics your tests emit on STDERR. + +Caveat: this is a bit of a kludge. In particular note that if anything +that appears on STDERR looks like a test result the test harness will +get confused. Use this option only if you understand the consequences +and can live with the risk. + +=head2 C<--trap> + +The C<--trap> option will attempt to trap SIGINT (Ctrl-C) during a test +run and display the test summary even if the run is interrupted + +=head2 C<--state> + +You can ask C to remember the state of previous test runs and +select and/or order the tests to be run based on that saved state. + +The C<--state> switch requires an argument which must be a comma +separated list of one or more of the following options. + +=over + +=item C + +Run the same tests as the last time the state was saved. This makes it +possible, for example, to recreate the ordering of a shuffled test. + + # Run all tests in random order + $ prove -b --state=save --shuffle + + # Run them again in the same order + $ prove -b --state=last + +=item C + +Run only the tests that failed on the last run. + + # Run all tests + $ prove -b --state=save + + # Run failures + $ prove -b --state=failed + +If you also specify the C option newly passing tests will be +excluded from subsequent runs. + + # Repeat until no more failures + $ prove -b --state=failed,save + +=item C + +Run only the passed tests from last time. Useful to make sure that no +new problems have been introduced. + +=item C + +Run all tests in normal order. Multiple options may be specified, so to +run all tests with the failures from last time first: + + $ prove -b --state=failed,all,save + +=item C + +Run the tests that most recently failed first. The last failure time of +each test is stored. The C option causes tests to be run in most-recent- +failure order. + + $ prove -b --state=hot,save + +Tests that have never failed will not be selected. To run all tests with +the most recently failed first use + + $ prove -b --state=hot,all,save + +This combination of options may also be specified thus + + $ prove -b --state=adrian + +=item C + +Run any tests with todos. + +=item C + +Run the tests in slowest to fastest order. This is useful in conjunction +with the C<-j> parallel testing switch to ensure that your slowest tests +start running first. + + $ prove -b --state=slow -j9 + +=item C + +Run test tests in fastest to slowest order. + +=item C + +Run the tests in newest to oldest order based on the modification times +of the test scripts. + +=item C + +Run the tests in oldest to newest order. + +=item C + +Run those test scripts that have been modified since the last test run. + +=item C + +Save the state on exit. The state is stored in a file called F<.prove> +(F<_prove> on Windows and VMS) in the current directory. + +=back + +The C<--state> switch may be used more than once. + + $ prove -b --state=hot --state=all,save + +=head2 --rules + +The C<--rules> option is used to control which tests are run sequentially and +which are run in parallel, if the C<--jobs> option is specified. The option may +be specified multiple times, and the order matters. + +The most practical use is likely to specify that some tests are not +"parallel-ready". Since mentioning a file with --rules doesn't cause it to +be selected to run as a test, you can "set and forget" some rules preferences in +your .proverc file. Then you'll be able to take maximum advantage of the +performance benefits of parallel testing, while some exceptions are still run +in parallel. + +=head3 --rules examples + + # All tests are allowed to run in parallel, except those starting with "p" + --rules='seq=t/p*.t' --rules='par=**' + + # All tests must run in sequence except those starting with "p", which should be run parallel + --rules='par=t/p*.t' + +=head3 --rules resolution + +=over 4 + +=item * By default, all tests are eligible to be run in parallel. Specifying any of your own rules removes this one. + +=item * "First match wins". The first rule that matches a test will be the one that applies. + +=item * Any test which does not match a rule will be run in sequence at the end of the run. + +=item * The existence of a rule does not imply selecting a test. You must still specify the tests to run. + +=item * Specifying a rule to allow tests to run in parallel does not make them run in parallel. You still need specify the number of parallel C in your Harness object. + +=back + +=head3 --rules Glob-style pattern matching + +We implement our own glob-style pattern matching for --rules. Here are the +supported patterns: + + ** is any number of characters, including /, within a pathname + * is zero or more characters within a filename/directory name + ? is exactly one character within a filename/directory name + {foo,bar,baz} is any of foo, bar or baz. + \ is an escape character + +=head3 More advanced specifications for parallel vs sequence run rules + +If you need more advanced management of what runs in parallel vs in sequence, see +the associated 'rules' documentation in L and L. +If what's possible directly through C is not sufficient, you can write your own +harness to access these features directly. + +=head2 @INC + +prove introduces a separation between "options passed to the perl which +runs prove" and "options passed to the perl which runs tests"; this +distinction is by design. Thus the perl which is running a test starts +with the default C<@INC>. Additional library directories can be added +via the C environment variable, via -Ifoo in C or +via the C<-Ilib> option to F. + +=head2 Taint Mode + +Normally when a Perl program is run in taint mode the contents of the +C environment variable do not appear in C<@INC>. + +Because C is often used during testing to add build +directories to C<@INC> prove passes the names of any directories found +in C as -I switches. The net effect of this is that +C is honoured even when prove is run in taint mode. + + +=head1 FORMATTERS + +You can load a custom L: + + prove --formatter MyFormatter + +=head1 SOURCE HANDLERS + +You can load custom Ls, to change the way the +parser interprets particular I of TAP. + + prove --source MyHandler --source YetAnother t + +If you want to provide config to the source you can use: + + prove --source MyCustom \ + --source Perl --perl-option 'foo=bar baz' --perl-option avg=0.278 \ + --source File --file-option extensions=.txt --file-option extensions=.tmp t + --source pgTAP --pgtap-option pset=format=html --pgtap-option pset=border=2 + +Each C<--$source-option> option must specify a key/value pair separated by an +C<=>. If an option can take multiple values, just specify it multiple times, +as with the C examples above. If the option should be a hash +reference, specify the value as a second pair separated by a C<=>, as in the +C examples above (escape C<=> with a backslash). + +All C<--sources> are combined into a hash, and passed to L's +C parameter. + +See L for more details on how configuration is +passed to I. + +=head1 PLUGINS + +Plugins can be loaded using the C<< -PI >> syntax, eg: + + prove -PMyPlugin + +This will search for a module named C, or failing +that, C. If the plugin can't be found, C will complain & exit. + +You can pass arguments to your plugin by appending C<=arg1,arg2,etc> to the +plugin name: + + prove -PMyPlugin=fou,du,fafa + +Please check individual plugin documentation for more details. + +=head2 Available Plugins + +For an up-to-date list of plugins available, please check CPAN: + +L + +=head2 Writing Plugins + +Please see L. + +=cut + +# vim:ts=4:sw=4:et:sta diff --git a/src/main/perl/lib/App/Prove.pm b/src/main/perl/lib/App/Prove.pm new file mode 100644 index 000000000..076fa0e7d --- /dev/null +++ b/src/main/perl/lib/App/Prove.pm @@ -0,0 +1,817 @@ +package App::Prove; + +use strict; +use warnings; + +use TAP::Harness::Env; +use Text::ParseWords qw(shellwords); +use File::Spec; +use Getopt::Long; +use App::Prove::State; +use Carp; + +use base 'TAP::Object'; + +=head1 NAME + +App::Prove - Implements the C command. + +=head1 VERSION + +Version 3.52 + +=cut + +our $VERSION = '3.52'; + +=head1 DESCRIPTION + +L provides a command, C, which runs a TAP based +test suite and prints a report. The C command is a minimal +wrapper around an instance of this module. + +=head1 SYNOPSIS + + use App::Prove; + + my $app = App::Prove->new; + $app->process_args(@ARGV); + $app->run; + +=cut + +use constant IS_WIN32 => ( $^O =~ /^(MS)?Win32$/ ); +use constant IS_VMS => $^O eq 'VMS'; +use constant IS_UNIXY => !( IS_VMS || IS_WIN32 ); + +use constant STATE_FILE => IS_UNIXY ? '.prove' : '_prove'; +use constant RC_FILE => IS_UNIXY ? '.proverc' : '_proverc'; + +use constant PLUGINS => 'App::Prove::Plugin'; + +my @ATTR; + +BEGIN { + @ATTR = qw( + archive argv blib show_count color directives exec failures comments + formatter harness includes modules plugins jobs lib merge parse quiet + really_quiet recurse backwards shuffle taint_fail taint_warn timer + verbose warnings_fail warnings_warn show_help show_man show_version + state_class test_args state dry extensions ignore_exit rules state_manager + normalize sources tapversion trap + statefile + ); + __PACKAGE__->mk_methods(@ATTR); +} + +=head1 METHODS + +=head2 Class Methods + +=head3 C + +Create a new C. Optionally a hash ref of attribute +initializers may be passed. + +=cut + +# new() implementation supplied by TAP::Object + +sub _initialize { + my $self = shift; + my $args = shift || {}; + + my @is_array = qw( + argv rc_opts includes modules state plugins rules sources + ); + + # setup defaults: + for my $key (@is_array) { + $self->{$key} = []; + } + + for my $attr (@ATTR) { + if ( exists $args->{$attr} ) { + + # TODO: Some validation here + $self->{$attr} = $args->{$attr}; + } + } + + $self->state_class('App::Prove::State'); + return $self; +} + +=head3 C + +Getter/setter for the name of the class used for maintaining state. This +class should either subclass from C or provide an identical +interface. + +=head3 C + +Getter/setter for the instance of the C. + +=cut + +=head3 C + + $prove->add_rc_file('myproj/.proverc'); + +Called before C to prepend the contents of an rc file to +the options. + +=cut + +sub add_rc_file { + my ( $self, $rc_file ) = @_; + + local *RC; + open RC, "<$rc_file" or croak "Can't read $rc_file ($!)"; + while ( defined( my $line = ) ) { + push @{ $self->{rc_opts} }, + grep { defined and not /^#/ } + $line =~ m{ ' ([^']*) ' | " ([^"]*) " | (\#.*) | (\S+) }xg; + } + close RC; +} + +=head3 C + + $prove->process_args(@args); + +Processes the command-line arguments. Attributes will be set +appropriately. Any filenames may be found in the C attribute. + +Dies on invalid arguments. + +=cut + +sub process_args { + my $self = shift; + + my @rc = RC_FILE; + unshift @rc, glob '~/' . RC_FILE if IS_UNIXY; + + # Preprocess meta-args. + my @args; + while ( defined( my $arg = shift ) ) { + if ( $arg eq '--norc' ) { + @rc = (); + } + elsif ( $arg eq '--rc' ) { + defined( my $rc = shift ) + or croak "Missing argument to --rc"; + push @rc, $rc; + } + elsif ( $arg =~ m{^--rc=(.+)$} ) { + push @rc, $1; + } + else { + push @args, $arg; + } + } + + # Everything after the arisdottle '::' gets passed as args to + # test programs. + if ( defined( my $stop_at = _first_pos( '::', @args ) ) ) { + my @test_args = splice @args, $stop_at; + shift @test_args; + $self->{test_args} = \@test_args; + } + + # Grab options from RC files + $self->add_rc_file($_) for grep -f, @rc; + unshift @args, @{ $self->{rc_opts} }; + + if ( my @bad = map {"-$_"} grep {/^-(man|help)$/} @args ) { + die "Long options should be written with two dashes: ", + join( ', ', @bad ), "\n"; + } + + # And finally... + + { + local @ARGV = @args; + Getopt::Long::Configure(qw(no_ignore_case bundling pass_through)); + + # Don't add coderefs to GetOptions + GetOptions( + 'v|verbose' => \$self->{verbose}, + 'f|failures' => \$self->{failures}, + 'o|comments' => \$self->{comments}, + 'l|lib' => \$self->{lib}, + 'b|blib' => \$self->{blib}, + 's|shuffle' => \$self->{shuffle}, + 'color!' => \$self->{color}, + 'colour!' => \$self->{color}, + 'count!' => \$self->{show_count}, + 'c' => \$self->{color}, + 'D|dry' => \$self->{dry}, + 'ext=s@' => sub { + my ( $opt, $val ) = @_; + + # Workaround for Getopt::Long 2.25 handling of + # multivalue options + push @{ $self->{extensions} ||= [] }, $val; + }, + 'harness=s' => \$self->{harness}, + 'ignore-exit' => \$self->{ignore_exit}, + 'source=s@' => $self->{sources}, + 'formatter=s' => \$self->{formatter}, + 'r|recurse' => \$self->{recurse}, + 'reverse' => \$self->{backwards}, + 'p|parse' => \$self->{parse}, + 'q|quiet' => \$self->{quiet}, + 'Q|QUIET' => \$self->{really_quiet}, + 'e|exec=s' => \$self->{exec}, + 'm|merge' => \$self->{merge}, + 'I=s@' => $self->{includes}, + 'M=s@' => $self->{modules}, + 'P=s@' => $self->{plugins}, + 'state=s@' => $self->{state}, + 'statefile=s' => \$self->{statefile}, + 'directives' => \$self->{directives}, + 'h|help|?' => \$self->{show_help}, + 'H|man' => \$self->{show_man}, + 'V|version' => \$self->{show_version}, + 'a|archive=s' => \$self->{archive}, + 'j|jobs=i' => \$self->{jobs}, + 'timer' => \$self->{timer}, + 'T' => \$self->{taint_fail}, + 't' => \$self->{taint_warn}, + 'W' => \$self->{warnings_fail}, + 'w' => \$self->{warnings_warn}, + 'normalize' => \$self->{normalize}, + 'rules=s@' => $self->{rules}, + 'tapversion=s' => \$self->{tapversion}, + 'trap' => \$self->{trap}, + ) or croak('Unable to continue'); + + # Stash the remainder of argv for later + $self->{argv} = [@ARGV]; + } + + return; +} + +sub _first_pos { + my $want = shift; + for ( 0 .. $#_ ) { + return $_ if $_[$_] eq $want; + } + return; +} + +sub _help { + my ( $self, $verbosity ) = @_; + + eval('use Pod::Usage 1.12 ()'); + if ( my $err = $@ ) { + die 'Please install Pod::Usage for the --help option ' + . '(or try `perldoc prove`.)' + . "\n ($@)"; + } + + Pod::Usage::pod2usage( { -verbose => $verbosity } ); + + return; +} + +sub _color_default { + my $self = shift; + + return -t STDOUT && !$ENV{HARNESS_NOTTY}; +} + +sub _get_args { + my $self = shift; + + my %args; + + $args{trap} = 1 if $self->trap; + + if ( defined $self->color ? $self->color : $self->_color_default ) { + $args{color} = 1; + } + if ( !defined $self->show_count ) { + $args{show_count} = 1; + } + else { + $args{show_count} = $self->show_count; + } + + if ( $self->archive ) { + $self->require_harness( archive => 'TAP::Harness::Archive' ); + $args{archive} = $self->archive; + } + + if ( my $jobs = $self->jobs ) { + $args{jobs} = $jobs; + } + + if ( my $harness_opt = $self->harness ) { + $self->require_harness( harness => $harness_opt ); + } + + if ( my $formatter = $self->formatter ) { + $args{formatter_class} = $formatter; + } + + for my $handler ( @{ $self->sources } ) { + my ( $name, $config ) = $self->_parse_source($handler); + $args{sources}->{$name} = $config; + } + + if ( $self->ignore_exit ) { + $args{ignore_exit} = 1; + } + + if ( $self->taint_fail && $self->taint_warn ) { + die '-t and -T are mutually exclusive'; + } + + if ( $self->warnings_fail && $self->warnings_warn ) { + die '-w and -W are mutually exclusive'; + } + + for my $a (qw( lib switches )) { + my $method = "_get_$a"; + my $val = $self->$method(); + $args{$a} = $val if defined $val; + } + + # Handle verbose, quiet, really_quiet flags + my %verb_map = ( verbose => 1, quiet => -1, really_quiet => -2, ); + + my @verb_adj = map { $self->$_() ? $verb_map{$_} : () } + keys %verb_map; + + die "Only one of verbose, quiet or really_quiet should be specified\n" + if @verb_adj > 1; + + $args{verbosity} = shift @verb_adj if @verb_adj; + + for my $a (qw( merge failures comments timer directives normalize )) { + $args{$a} = 1 if $self->$a(); + } + + $args{errors} = 1 if $self->parse; + + # defined but zero-length exec runs test files as binaries + $args{exec} = [ split( /\s+/, $self->exec ) ] + if ( defined( $self->exec ) ); + + $args{version} = $self->tapversion if defined( $self->tapversion ); + + if ( defined( my $test_args = $self->test_args ) ) { + $args{test_args} = $test_args; + } + + if ( @{ $self->rules } ) { + my @rules; + for ( @{ $self->rules } ) { + if (/^par=(.*)/) { + push @rules, $1; + } + elsif (/^seq=(.*)/) { + push @rules, { seq => $1 }; + } + } + $args{rules} = { par => [@rules] }; + } + $args{harness_class} = $self->{harness_class} if $self->{harness_class}; + + return \%args; +} + +sub _find_module { + my ( $self, $class, @search ) = @_; + + croak "Bad module name $class" + unless $class =~ /^ \w+ (?: :: \w+ ) *$/x; + + for my $pfx (@search) { + my $name = join( '::', $pfx, $class ); + eval "require $name"; + return $name unless $@; + } + + eval "require $class"; + return $class unless $@; + return; +} + +sub _load_extension { + my ( $self, $name, @search ) = @_; + + my @args = (); + if ( $name =~ /^(.*?)=(.*)/ ) { + $name = $1; + @args = split( /,/, $2 ); + } + + if ( my $class = $self->_find_module( $name, @search ) ) { + if ( $class->can('load') ) { + $class->load( { app_prove => $self, args => [@args] } ); + } + } + else { + croak "Can't load module $name"; + } +} + +sub _load_extensions { + my ( $self, $ext, @search ) = @_; + $self->_load_extension( $_, @search ) for @$ext; +} + +sub _parse_source { + my ( $self, $handler ) = @_; + + # Load any options. + ( my $opt_name = lc $handler ) =~ s/::/-/g; + local @ARGV = @{ $self->{argv} }; + my %config; + Getopt::Long::GetOptions( + "$opt_name-option=s%" => sub { + my ( $name, $k, $v ) = @_; + if ( $v =~ /(? $v; + } + else { + $config{$k} = $v; + } + } + } + ); + $self->{argv} = \@ARGV; + return ( $handler, \%config ); +} + +=head3 C + +Perform whatever actions the command line args specified. The C +command line tool consists of the following code: + + use App::Prove; + + my $app = App::Prove->new; + $app->process_args(@ARGV); + exit( $app->run ? 0 : 1 ); # if you need the exit code + +=cut + +sub run { + my $self = shift; + + unless ( $self->state_manager ) { + $self->state_manager( + $self->state_class->new( { store => $self->statefile || STATE_FILE } ) ); + } + + if ( $self->show_help ) { + $self->_help(1); + } + elsif ( $self->show_man ) { + $self->_help(2); + } + elsif ( $self->show_version ) { + $self->print_version; + } + elsif ( $self->dry ) { + print "$_\n" for $self->_get_tests; + } + else { + + $self->_load_extensions( $self->modules ); + $self->_load_extensions( $self->plugins, PLUGINS ); + + local $ENV{TEST_VERBOSE} = 1 if $self->verbose; + + return $self->_runtests( $self->_get_args, $self->_get_tests ); + } + + return 1; +} + +sub _get_tests { + my $self = shift; + + my $state = $self->state_manager; + my $ext = $self->extensions; + $state->extensions($ext) if defined $ext; + if ( defined( my $state_switch = $self->state ) ) { + $state->apply_switch(@$state_switch); + } + + my @tests = $state->get_tests( $self->recurse, @{ $self->argv } ); + + $self->_shuffle(@tests) if $self->shuffle; + @tests = reverse @tests if $self->backwards; + + return @tests; +} + +sub _runtests { + my ( $self, $args, @tests ) = @_; + my $harness = TAP::Harness::Env->create($args); + + my $state = $self->state_manager; + + $harness->callback( + after_test => sub { + $state->observe_test(@_); + } + ); + + $harness->callback( + after_runtests => sub { + $state->commit(@_); + } + ); + + my $aggregator = $harness->runtests(@tests); + + return !$aggregator->has_errors; +} + +sub _get_switches { + my $self = shift; + my @switches; + + # notes that -T or -t must be at the front of the switches! + if ( $self->taint_fail ) { + push @switches, '-T'; + } + elsif ( $self->taint_warn ) { + push @switches, '-t'; + } + if ( $self->warnings_fail ) { + push @switches, '-W'; + } + elsif ( $self->warnings_warn ) { + push @switches, '-w'; + } + + return @switches ? \@switches : (); +} + +sub _get_lib { + my $self = shift; + my @libs; + if ( $self->lib ) { + push @libs, 'lib'; + } + if ( $self->blib ) { + push @libs, 'blib/lib', 'blib/arch'; + } + if ( @{ $self->includes } ) { + push @libs, @{ $self->includes }; + } + + #24926 + @libs = map { File::Spec->rel2abs($_) } @libs; + + # Huh? + return @libs ? \@libs : (); +} + +sub _shuffle { + my $self = shift; + + # Fisher-Yates shuffle + my $i = @_; + while ($i) { + my $j = rand $i--; + @_[ $i, $j ] = @_[ $j, $i ]; + } + return; +} + +=head3 C + +Load a harness replacement class. + + $prove->require_harness($for => $class_name); + +=cut + +sub require_harness { + my ( $self, $for, $class ) = @_; + + my ($class_name) = $class =~ /^(\w+(?:::\w+)*)/; + + # Emulate Perl's -MModule=arg1,arg2 behaviour + $class =~ s!^(\w+(?:::\w+)*)=(.*)$!$1 split(/,/,q{$2})!; + + eval("use $class;"); + die "$class_name is required to use the --$for feature: $@" if $@; + + $self->{harness_class} = $class_name; + + return; +} + +=head3 C + +Display the version numbers of the loaded L and the +current Perl. + +=cut + +sub print_version { + my $self = shift; + require TAP::Harness; + printf( + "TAP::Harness v%s and Perl v%vd\n", + $TAP::Harness::VERSION, $^V + ); + + return; +} + +1; + +# vim:ts=4:sw=4:et:sta + +__END__ + +=head2 Attributes + +After command line parsing the following attributes reflect the values +of the corresponding command line switches. They may be altered before +calling C. + +=over + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=item C + +=back + +=head1 PLUGINS + +C provides support for 3rd-party plugins. These are currently +loaded at run-time, I arguments have been parsed (so you can not +change the way arguments are processed, sorry), typically with the +C<< -PI >> switch, eg: + + prove -PMyPlugin + +This will search for a module named C, or failing +that, C. If the plugin can't be found, C will complain & exit. + +You can pass an argument to your plugin by appending an C<=> after the plugin +name, eg C<-PMyPlugin=foo>. You can pass multiple arguments using commas: + + prove -PMyPlugin=foo,bar,baz + +These are passed in to your plugin's C class method (if it has one), +along with a reference to the C object that is invoking your plugin: + + sub load { + my ($class, $p) = @_; + + my @args = @{ $p->{args} }; + # @args will contain ( 'foo', 'bar', 'baz' ) + $p->{app_prove}->do_something; + ... + } + +=head2 Sample Plugin + +Here's a sample plugin, for your reference: + + package App::Prove::Plugin::Foo; + + # Sample plugin, try running with: + # prove -PFoo=bar -r -j3 + # prove -PFoo -Q + # prove -PFoo=bar,My::Formatter + + use strict; + use warnings; + + sub load { + my ($class, $p) = @_; + my @args = @{ $p->{args} }; + my $app = $p->{app_prove}; + + print "loading plugin: $class, args: ", join(', ', @args ), "\n"; + + # turn on verbosity + $app->verbose( 1 ); + + # set the formatter? + $app->formatter( $args[1] ) if @args > 1; + + # print some of App::Prove's state: + for my $attr (qw( jobs quiet really_quiet recurse verbose )) { + my $val = $app->$attr; + $val = 'undef' unless defined( $val ); + print "$attr: $val\n"; + } + + return 1; + } + + 1; + +=head1 SEE ALSO + +L, L + +=cut diff --git a/src/main/perl/lib/App/Prove/State.pm b/src/main/perl/lib/App/Prove/State.pm new file mode 100644 index 000000000..d97bad30d --- /dev/null +++ b/src/main/perl/lib/App/Prove/State.pm @@ -0,0 +1,548 @@ +package App::Prove::State; + +use strict; +use warnings; + +use File::Find; +use File::Spec; +use Carp; + +use App::Prove::State::Result; +use TAP::Parser::YAMLish::Reader (); +use TAP::Parser::YAMLish::Writer (); +use base 'TAP::Base'; + +BEGIN { + __PACKAGE__->mk_methods('result_class'); +} + +use constant IS_WIN32 => ( $^O =~ /^(MS)?Win32$/ ); +use constant NEED_GLOB => IS_WIN32; + +=head1 NAME + +App::Prove::State - State storage for the C command. + +=head1 VERSION + +Version 3.52 + +=cut + +our $VERSION = '3.52'; + +=head1 DESCRIPTION + +The C command supports a C<--state> option that instructs it to +store persistent state across runs. This module implements that state +and the operations that may be performed on it. + +=head1 SYNOPSIS + + # Re-run failed tests + $ prove --state=failed,save -rbv + +=cut + +=head1 METHODS + +=head2 Class Methods + +=head3 C + +Accepts a hashref with the following key/value pairs: + +=over 4 + +=item * C + +The filename of the data store holding the data that App::Prove::State reads. + +=item * C (optional) + +The test name extensions. Defaults to C<.t>. + +=item * C (optional) + +The name of the C. Defaults to C. + +=back + +=cut + +# override TAP::Base::new: +sub new { + my $class = shift; + my %args = %{ shift || {} }; + + my $self = bless { + select => [], + seq => 1, + store => delete $args{store}, + extensions => ( delete $args{extensions} || ['.t'] ), + result_class => + ( delete $args{result_class} || 'App::Prove::State::Result' ), + }, $class; + + $self->{_} = $self->result_class->new( + { tests => {}, + generation => 1, + } + ); + my $store = $self->{store}; + $self->load($store) + if defined $store && -f $store; + + return $self; +} + +=head2 C + +Getter/setter for the name of the class used for tracking test results. This +class should either subclass from C or provide an +identical interface. + +=cut + +=head2 C + +Get or set the list of extensions that files must have in order to be +considered tests. Defaults to ['.t']. + +=cut + +sub extensions { + my $self = shift; + $self->{extensions} = shift if @_; + return $self->{extensions}; +} + +=head2 C + +Get the results of the last test run. Returns a C instance. + +=cut + +sub results { + my $self = shift; + $self->{_} || $self->result_class->new; +} + +=head2 C + +Save the test results. Should be called after all tests have run. + +=cut + +sub commit { + my $self = shift; + if ( $self->{should_save} ) { + $self->save; + } +} + +=head2 Instance Methods + +=head3 C + + $self->apply_switch('failed,save'); + +Apply a list of switch options to the state, updating the internal +object state as a result. Nothing is returned. + +Diagnostics: + - "Illegal state option: %s" + +=over + +=item C + +Run in the same order as last time + +=item C + +Run only the failed tests from last time + +=item C + +Run only the passed tests from last time + +=item C + +Run all tests in normal order + +=item C + +Run the tests that most recently failed first + +=item C + +Run the tests ordered by number of todos. + +=item C + +Run the tests in slowest to fastest order. + +=item C + +Run test tests in fastest to slowest order. + +=item C + +Run the tests in newest to oldest order. + +=item C + +Run the tests in oldest to newest order. + +=item C + +Save the state on exit. + +=back + +=cut + +sub apply_switch { + my $self = shift; + my @opts = @_; + + my $last_gen = $self->results->generation - 1; + my $last_run_time = $self->results->last_run_time; + my $now = $self->get_time; + + my @switches = map { split /,/ } @opts; + + my %handler = ( + last => sub { + $self->_select( + limit => shift, + where => sub { $_->generation >= $last_gen }, + order => sub { $_->sequence } + ); + }, + failed => sub { + $self->_select( + limit => shift, + where => sub { $_->result != 0 }, + order => sub { -$_->result } + ); + }, + passed => sub { + $self->_select( + limit => shift, + where => sub { $_->result == 0 } + ); + }, + all => sub { + $self->_select( limit => shift ); + }, + todo => sub { + $self->_select( + limit => shift, + where => sub { $_->num_todo != 0 }, + order => sub { -$_->num_todo; } + ); + }, + hot => sub { + $self->_select( + limit => shift, + where => sub { defined $_->last_fail_time }, + order => sub { $now - $_->last_fail_time } + ); + }, + slow => sub { + $self->_select( + limit => shift, + order => sub { -$_->elapsed } + ); + }, + fast => sub { + $self->_select( + limit => shift, + order => sub { $_->elapsed } + ); + }, + new => sub { + $self->_select( + limit => shift, + order => sub { -$_->mtime } + ); + }, + old => sub { + $self->_select( + limit => shift, + order => sub { $_->mtime } + ); + }, + fresh => sub { + $self->_select( + limit => shift, + where => sub { $_->mtime >= $last_run_time } + ); + }, + save => sub { + $self->{should_save}++; + }, + adrian => sub { + unshift @switches, qw( hot all save ); + }, + ); + + while ( defined( my $ele = shift @switches ) ) { + my ( $opt, $arg ) + = ( $ele =~ /^([^:]+):(.*)/ ) + ? ( $1, $2 ) + : ( $ele, undef ); + my $code = $handler{$opt} + || croak "Illegal state option: $opt"; + $code->($arg); + } + return; +} + +sub _select { + my ( $self, %spec ) = @_; + push @{ $self->{select} }, \%spec; +} + +=head3 C + +Given a list of args get the names of tests that should run + +=cut + +sub get_tests { + my $self = shift; + my $recurse = shift; + my @argv = @_; + my %seen; + + my @selected = $self->_query; + + unless ( @argv || @{ $self->{select} } ) { + @argv = $recurse ? '.' : 't'; + croak qq{No tests named and '@argv' directory not found} + unless -d $argv[0]; + } + + push @selected, $self->_get_raw_tests( $recurse, @argv ) if @argv; + return grep { !$seen{$_}++ } @selected; +} + +sub _query { + my $self = shift; + if ( my @sel = @{ $self->{select} } ) { + warn "No saved state, selection will be empty\n" + unless $self->results->num_tests; + return map { $self->_query_clause($_) } @sel; + } + return; +} + +sub _query_clause { + my ( $self, $clause ) = @_; + my @got; + my $results = $self->results; + my $where = $clause->{where} || sub {1}; + + # Select + for my $name ( $results->test_names ) { + next unless -f $name; + local $_ = $results->test($name); + push @got, $name if $where->(); + } + + # Sort + if ( my $order = $clause->{order} ) { + @got = map { $_->[0] } + sort { + ( defined $b->[1] <=> defined $a->[1] ) + || ( ( $a->[1] || 0 ) <=> ( $b->[1] || 0 ) ) + } map { + [ $_, + do { local $_ = $results->test($_); $order->() } + ] + } @got; + } + + if ( my $limit = $clause->{limit} ) { + @got = splice @got, 0, $limit if @got > $limit; + } + + return @got; +} + +sub _get_raw_tests { + my $self = shift; + my $recurse = shift; + my @argv = @_; + my @tests; + + # Do globbing on Win32. + if (NEED_GLOB) { + eval "use File::Glob::Windows"; # [49732] + @argv = map { glob "$_" } @argv; + } + my $extensions = $self->{extensions}; + + for my $arg (@argv) { + if ( '-' eq $arg ) { + push @argv => ; + chomp(@argv); + next; + } + + push @tests, + sort -d $arg + ? $recurse + ? $self->_expand_dir_recursive( $arg, $extensions ) + : map { glob( File::Spec->catfile( $arg, "*$_" ) ) } + @{$extensions} + : $arg; + } + return @tests; +} + +sub _expand_dir_recursive { + my ( $self, $dir, $extensions ) = @_; + + my @tests; + my $ext_string = join( '|', map {quotemeta} @{$extensions} ); + + find( + { follow => 1, #21938 + follow_skip => 2, + wanted => sub { + -f + && /(?:$ext_string)$/ + && push @tests => $File::Find::name; + } + }, + $dir + ); + return @tests; +} + +=head3 C + +Store the results of a test. + +=cut + +# Store: +# last fail time +# last pass time +# last run time +# most recent result +# most recent todos +# total failures +# total passes +# state generation +# parser + +sub observe_test { + + my ( $self, $test_info, $parser ) = @_; + my $name = $test_info->[0]; + my $fail = scalar( $parser->failed ) + ( $parser->has_problems ? 1 : 0 ); + my $todo = scalar( $parser->todo ); + my $start_time = $parser->start_time; + my $end_time = $parser->end_time, + + my $test = $self->results->test($name); + + $test->sequence( $self->{seq}++ ); + $test->generation( $self->results->generation ); + + $test->run_time($end_time); + $test->result($fail); + $test->num_todo($todo); + $test->elapsed( $end_time - $start_time ); + + $test->parser($parser); + + if ($fail) { + $test->total_failures( $test->total_failures + 1 ); + $test->last_fail_time($end_time); + } + else { + $test->total_passes( $test->total_passes + 1 ); + $test->last_pass_time($end_time); + } +} + +=head3 C + +Write the state to a file. + +=cut + +sub save { + my ($self) = @_; + + my $store = $self->{store} or return; + $self->results->last_run_time( $self->get_time ); + + my $writer = TAP::Parser::YAMLish::Writer->new; + local *FH; + open FH, ">$store" or croak "Can't write $store ($!)"; + $writer->write( $self->results->raw, \*FH ); + close FH; +} + +=head3 C + +Load the state from a file + +=cut + +sub load { + my ( $self, $name ) = @_; + my $reader = TAP::Parser::YAMLish::Reader->new; + local *FH; + open FH, "<$name" or croak "Can't read $name ($!)"; + + # XXX this is temporary + $self->{_} = $self->result_class->new( + $reader->read( + sub { + my $line = ; + defined $line && chomp $line; + return $line; + } + ) + ); + + # $writer->write( $self->{tests} || {}, \*FH ); + close FH; + $self->_regen_seq; + $self->_prune_and_stamp; + $self->results->generation( $self->results->generation + 1 ); +} + +sub _prune_and_stamp { + my $self = shift; + + my $results = $self->results; + my @tests = $self->results->tests; + for my $test (@tests) { + my $name = $test->name; + if ( my @stat = stat $name ) { + $test->mtime( $stat[9] ); + } + else { + $results->remove($name); + } + } +} + +sub _regen_seq { + my $self = shift; + for my $test ( $self->results->tests ) { + $self->{seq} = $test->sequence + 1 + if defined $test->sequence && $test->sequence >= $self->{seq}; + } +} + +1; diff --git a/src/main/perl/lib/App/Prove/State/Result.pm b/src/main/perl/lib/App/Prove/State/Result.pm new file mode 100644 index 000000000..4bf73d8ed --- /dev/null +++ b/src/main/perl/lib/App/Prove/State/Result.pm @@ -0,0 +1,233 @@ +package App::Prove::State::Result; + +use strict; +use warnings; +use Carp 'croak'; + +use App::Prove::State::Result::Test; + +use constant STATE_VERSION => 1; + +=head1 NAME + +App::Prove::State::Result - Individual test suite results. + +=head1 VERSION + +Version 3.52 + +=cut + +our $VERSION = '3.52'; + +=head1 DESCRIPTION + +The C command supports a C<--state> option that instructs it to +store persistent state across runs. This module encapsulates the results for a +single test suite run. + +=head1 SYNOPSIS + + # Re-run failed tests + $ prove --state=failed,save -rbv + +=cut + +=head1 METHODS + +=head2 Class Methods + +=head3 C + + my $result = App::Prove::State::Result->new({ + generation => $generation, + tests => \%tests, + }); + +Returns a new C instance. + +=cut + +sub new { + my ( $class, $arg_for ) = @_; + $arg_for ||= {}; + my %instance_data = %$arg_for; # shallow copy + $instance_data{version} = $class->state_version; + my $tests = delete $instance_data{tests} || {}; + my $self = bless \%instance_data => $class; + $self->_initialize($tests); + return $self; +} + +sub _initialize { + my ( $self, $tests ) = @_; + my %tests; + while ( my ( $name, $test ) = each %$tests ) { + $tests{$name} = $self->test_class->new( + { %$test, + name => $name + } + ); + } + $self->tests( \%tests ); + return $self; +} + +=head2 C + +Returns the current version of state storage. + +=cut + +sub state_version {STATE_VERSION} + +=head2 C + +Returns the name of the class used for tracking individual tests. This class +should either subclass from C or provide an +identical interface. + +=cut + +sub test_class { + return 'App::Prove::State::Result::Test'; +} + +my %methods = ( + generation => { method => 'generation', default => 0 }, + last_run_time => { method => 'last_run_time', default => undef }, +); + +while ( my ( $key, $description ) = each %methods ) { + my $default = $description->{default}; + no strict 'refs'; + *{ $description->{method} } = sub { + my $self = shift; + if (@_) { + $self->{$key} = shift; + return $self; + } + return $self->{$key} || $default; + }; +} + +=head3 C + +Getter/setter for the "generation" of the test suite run. The first +generation is 1 (one) and subsequent generations are 2, 3, etc. + +=head3 C + +Getter/setter for the time of the test suite run. + +=head3 C + +Returns the tests for a given generation. This is a hashref or a hash, +depending on context called. The keys to the hash are the individual +test names and the value is a hashref with various interesting values. +Each k/v pair might resemble something like this: + + 't/foo.t' => { + elapsed => '0.0428488254547119', + gen => '7', + last_pass_time => '1219328376.07815', + last_result => '0', + last_run_time => '1219328376.07815', + last_todo => '0', + mtime => '1191708862', + seq => '192', + total_passes => '6', + } + +=cut + +sub tests { + my $self = shift; + if (@_) { + $self->{tests} = shift; + return $self; + } + my %tests = %{ $self->{tests} }; + my @tests = sort { $a->sequence <=> $b->sequence } values %tests; + return wantarray ? @tests : \@tests; +} + +=head3 C + + my $test = $result->test('t/customer/create.t'); + +Returns an individual C instance for the +given test name (usually the filename). Will return a new +C instance if the name is not found. + +=cut + +sub test { + my ( $self, $name ) = @_; + croak("test() requires a test name") unless defined $name; + + my $tests = $self->{tests} ||= {}; + if ( my $test = $tests->{$name} ) { + return $test; + } + else { + my $test = $self->test_class->new( { name => $name } ); + $self->{tests}->{$name} = $test; + return $test; + } +} + +=head3 C + +Returns an list of test names, sorted by run order. + +=cut + +sub test_names { + my $self = shift; + return map { $_->name } $self->tests; +} + +=head3 C + + $result->remove($test_name); # remove the test + my $test = $result->test($test_name); # fatal error + +Removes a given test from results. This is a no-op if the test name is not +found. + +=cut + +sub remove { + my ( $self, $name ) = @_; + delete $self->{tests}->{$name}; + return $self; +} + +=head3 C + +Returns the number of tests for a given test suite result. + +=cut + +sub num_tests { keys %{ shift->{tests} } } + +=head3 C + +Returns a hashref of raw results, suitable for serialization by YAML. + +=cut + +sub raw { + my $self = shift; + my %raw = %$self; + + my %tests; + for my $test ( $self->tests ) { + $tests{ $test->name } = $test->raw; + } + $raw{tests} = \%tests; + return \%raw; +} + +1; diff --git a/src/main/perl/lib/App/Prove/State/Result/Test.pm b/src/main/perl/lib/App/Prove/State/Result/Test.pm new file mode 100644 index 000000000..17609e66c --- /dev/null +++ b/src/main/perl/lib/App/Prove/State/Result/Test.pm @@ -0,0 +1,152 @@ +package App::Prove::State::Result::Test; + +use strict; +use warnings; + +=head1 NAME + +App::Prove::State::Result::Test - Individual test results. + +=head1 VERSION + +Version 3.52 + +=cut + +our $VERSION = '3.52'; + +=head1 DESCRIPTION + +The C command supports a C<--state> option that instructs it to +store persistent state across runs. This module encapsulates the results for a +single test. + +=head1 SYNOPSIS + + # Re-run failed tests + $ prove --state=failed,save -rbv + +=cut + +my %methods = ( + name => { method => 'name' }, + elapsed => { method => 'elapsed', default => 0 }, + gen => { method => 'generation', default => 1 }, + last_pass_time => { method => 'last_pass_time', default => undef }, + last_fail_time => { method => 'last_fail_time', default => undef }, + last_result => { method => 'result', default => 0 }, + last_run_time => { method => 'run_time', default => undef }, + last_todo => { method => 'num_todo', default => 0 }, + mtime => { method => 'mtime', default => undef }, + seq => { method => 'sequence', default => 1 }, + total_passes => { method => 'total_passes', default => 0 }, + total_failures => { method => 'total_failures', default => 0 }, + parser => { method => 'parser' }, +); + +while ( my ( $key, $description ) = each %methods ) { + my $default = $description->{default}; + no strict 'refs'; + *{ $description->{method} } = sub { + my $self = shift; + if (@_) { + $self->{$key} = shift; + return $self; + } + return $self->{$key} || $default; + }; +} + +=head1 METHODS + +=head2 Class Methods + +=head3 C + +=cut + +sub new { + my ( $class, $arg_for ) = @_; + $arg_for ||= {}; + bless $arg_for => $class; +} + +=head2 Instance Methods + +=head3 C + +The name of the test. Usually a filename. + +=head3 C + +The total elapsed times the test took to run, in seconds from the epoch.. + +=head3 C + +The number for the "generation" of the test run. The first generation is 1 +(one) and subsequent generations are 2, 3, etc. + +=head3 C + +The last time the test program passed, in seconds from the epoch. + +Returns C if the program has never passed. + +=head3 C + +The last time the test suite failed, in seconds from the epoch. + +Returns C if the program has never failed. + +=head3 C + +Returns the mtime of the test, in seconds from the epoch. + +=head3 C + +Returns a hashref of raw test data, suitable for serialization by YAML. + +=head3 C + +Currently, whether or not the test suite passed with no 'problems' (such as +TODO passed). + +=head3 C + +The total time it took for the test to run, in seconds. If C is +available, it will have finer granularity. + +=head3 C + +The number of tests with TODO directives. + +=head3 C + +The order in which this test was run for the given test suite result. + +=head3 C + +The number of times the test has passed. + +=head3 C + +The number of times the test has failed. + +=head3 C + +The underlying parser object. This is useful if you need the full +information for the test program. + +=cut + +sub raw { + my $self = shift; + my %raw = %$self; + + # this is backwards-compatibility hack and is not guaranteed. + delete $raw{name}; + delete $raw{parser}; + return \%raw; +} + +1; diff --git a/src/main/perl/lib/IO/Select.pm b/src/main/perl/lib/IO/Select.pm new file mode 100644 index 000000000..97ffd0f49 --- /dev/null +++ b/src/main/perl/lib/IO/Select.pm @@ -0,0 +1,417 @@ +# IO::Select.pm +# +# Copyright (c) 1997-8 Graham Barr . All rights reserved. +# This program is free software; you can redistribute it and/or +# modify it under the same terms as Perl itself. + +package IO::Select; + +use strict; +use warnings::register; +require Exporter; + +our $VERSION = "1.56"; + +our @ISA = qw(Exporter); # This is only so we can do version checking + +sub VEC_BITS () {0} +sub FD_COUNT () {1} +sub FIRST_FD () {2} + +sub new +{ + my $self = shift; + my $type = ref($self) || $self; + + my $vec = bless [undef,0], $type; + + $vec->add(@_) + if @_; + + $vec; +} + +sub add +{ + shift->_update('add', @_); +} + + +sub remove +{ + shift->_update('remove', @_); +} + + +sub exists +{ + my $vec = shift; + my $fno = $vec->_fileno(shift); + return undef unless defined $fno; + $vec->[$fno + FIRST_FD]; +} + + +sub _fileno +{ + my($self, $f) = @_; + return unless defined $f; + $f = $f->[0] if ref($f) eq 'ARRAY'; + if($f =~ /^[0-9]+$/) { # plain file number + return $f; + } + elsif(defined(my $fd = fileno($f))) { + return $fd; + } + else { + # Neither a plain file number nor an opened filehandle; but maybe it was + # previously registered and has since been closed. ->remove still wants to + # know what fileno it had + foreach my $i ( FIRST_FD .. $#$self ) { + return $i - FIRST_FD if defined $self->[$i] && $self->[$i] == $f; + } + return undef; + } +} + +sub _update +{ + my $vec = shift; + my $add = shift eq 'add'; + + my $bits = $vec->[VEC_BITS]; + $bits = '' unless defined $bits; + + my $count = 0; + my $f; + foreach $f (@_) + { + my $fn = $vec->_fileno($f); + if ($add) { + next unless defined $fn; + my $i = $fn + FIRST_FD; + if (defined $vec->[$i]) { + $vec->[$i] = $f; # if array rest might be different, so we update + next; + } + $vec->[FD_COUNT]++; + vec($bits, $fn, 1) = 1; + $vec->[$i] = $f; + } else { # remove + if ( ! defined $fn ) { # remove if fileno undef'd + $fn = 0; + for my $fe (@{$vec}[FIRST_FD .. $#$vec]) { + if (defined($fe) && $fe == $f) { + $vec->[FD_COUNT]--; + $fe = undef; + vec($bits, $fn, 1) = 0; + last; + } + ++$fn; + } + } + else { + my $i = $fn + FIRST_FD; + next unless defined $vec->[$i]; + $vec->[FD_COUNT]--; + vec($bits, $fn, 1) = 0; + $vec->[$i] = undef; + } + } + $count++; + } + $vec->[VEC_BITS] = $vec->[FD_COUNT] ? $bits : undef; + $count; +} + +sub can_read +{ + my $vec = shift; + my $timeout = shift; + my $r = $vec->[VEC_BITS]; + + defined($r) && (select($r,undef,undef,$timeout) > 0) + ? handles($vec, $r) + : (); +} + +sub can_write +{ + my $vec = shift; + my $timeout = shift; + my $w = $vec->[VEC_BITS]; + + defined($w) && (select(undef,$w,undef,$timeout) > 0) + ? handles($vec, $w) + : (); +} + +sub has_exception +{ + my $vec = shift; + my $timeout = shift; + my $e = $vec->[VEC_BITS]; + + defined($e) && (select(undef,undef,$e,$timeout) > 0) + ? handles($vec, $e) + : (); +} + +sub has_error +{ + warnings::warn("Call to deprecated method 'has_error', use 'has_exception'") + if warnings::enabled(); + goto &has_exception; +} + +sub count +{ + my $vec = shift; + $vec->[FD_COUNT]; +} + +sub bits +{ + my $vec = shift; + $vec->[VEC_BITS]; +} + +sub as_string # for debugging +{ + my $vec = shift; + my $str = ref($vec) . ": "; + my $bits = $vec->bits; + my $count = $vec->count; + $str .= defined($bits) ? unpack("b*", $bits) : "undef"; + $str .= " $count"; + my @handles = @$vec; + splice(@handles, 0, FIRST_FD); + for (@handles) { + $str .= " " . (defined($_) ? "$_" : "-"); + } + $str; +} + +sub _max +{ + my($a,$b,$c) = @_; + $a > $b + ? $a > $c + ? $a + : $c + : $b > $c + ? $b + : $c; +} + +sub select +{ + shift + if defined $_[0] && !ref($_[0]); + + my($r,$w,$e,$t) = @_; + my @result = (); + + my $rb = defined $r ? $r->[VEC_BITS] : undef; + my $wb = defined $w ? $w->[VEC_BITS] : undef; + my $eb = defined $e ? $e->[VEC_BITS] : undef; + + if(select($rb,$wb,$eb,$t) > 0) + { + my @r = (); + my @w = (); + my @e = (); + my $i = _max(defined $r ? scalar(@$r)-1 : 0, + defined $w ? scalar(@$w)-1 : 0, + defined $e ? scalar(@$e)-1 : 0); + + for( ; $i >= FIRST_FD ; $i--) + { + my $j = $i - FIRST_FD; + push(@r, $r->[$i]) + if defined $rb && defined $r->[$i] && vec($rb, $j, 1); + push(@w, $w->[$i]) + if defined $wb && defined $w->[$i] && vec($wb, $j, 1); + push(@e, $e->[$i]) + if defined $eb && defined $e->[$i] && vec($eb, $j, 1); + } + + @result = (\@r, \@w, \@e); + } + @result; +} + + +sub handles +{ + my $vec = shift; + my $bits = shift; + my @h = (); + my $i; + my $max = scalar(@$vec) - 1; + + for ($i = FIRST_FD; $i <= $max; $i++) + { + next unless defined $vec->[$i]; + push(@h, $vec->[$i]) + if !defined($bits) || vec($bits, $i - FIRST_FD, 1); + } + + @h; +} + +1; +__END__ + +=head1 NAME + +IO::Select - OO interface to the C +function call. It allows the user to see what IO handles, see L, +are ready for reading, writing or have an exception pending. + +=head1 CONSTRUCTOR + +=over 4 + +=item new ( [ HANDLES ] ) + +The constructor creates a new object and optionally initialises it with a set +of handles. + +=back + +=head1 METHODS + +=over 4 + +=item add ( HANDLES ) + +Add the list of handles to the C object. It is these values that +will be returned when an event occurs. C keeps these values in a +cache which is indexed by the C of the handle, so if more than one +handle with the same C is specified then only the last one is cached. + +Each handle can be an C object, an integer or an array +reference where the first element is an C or an integer. + +=item remove ( HANDLES ) + +Remove all the given handles from the object. This method also works +by the C of the handles. So the exact handles that were added +need not be passed, just handles that have an equivalent C + +=item exists ( HANDLE ) + +Returns a true value (actually the handle itself) if it is present. +Returns undef otherwise. + +=item handles + +Return an array of all registered handles. + +=item can_read ( [ TIMEOUT ] ) + +Return an array of handles that are ready for reading. C is the +maximum amount of time to wait before returning an empty list (with C<$!> +unchanged), in seconds, possibly fractional. If C is not given +and any handles are registered then the call will block indefinitely. +Upon error, an empty list is returned, with C<$!> set to indicate the +error. To distinguish between timeout and error, set C<$!> to zero +before calling this method, and check it after an empty list is returned. + +=item can_write ( [ TIMEOUT ] ) + +Same as C except check for handles that can be written to. + +=item has_exception ( [ TIMEOUT ] ) + +Same as C except check for handles that have an exception +condition, for example pending out-of-band data. + +=item count () + +Returns the number of handles that the object will check for when +one of the C methods is called or the object is passed to +the C is a static method, that is you call it with the package name +like C. C, C and C are either C or +C objects. C is optional and has the same effect as +for the core select call. + +If at least one handle is ready for the specified kind of operation, +the result will be an array of 3 elements, each a reference to an array +which will hold the handles that are ready for reading, writing and +have exceptions respectively. Upon timeout, an empty list is returned, +with C<$!> unchanged. Upon error, an empty list is returned, with C<$!> +set to indicate the error. To distinguish between timeout and error, +set C<$!> to zero before calling this method, and check it after an +empty list is returned. + +=back + +=head1 EXAMPLE + +Here is a short example which shows how C could be used +to write a server which communicates with several sockets while also +listening for more connections on a listen socket + + use IO::Select; + use IO::Socket; + + my $lsn = IO::Socket::INET->new(Listen => 1, LocalPort => 8080); + my $sel = IO::Select->new( $lsn ); + + while(my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if($fh == $lsn) { + # Create a new socket + my $new = $lsn->accept; + $sel->add($new); + } + else { + # Process socket + + # Maybe we have finished with the socket + $sel->remove($fh); + $fh->close; + } + } + } + +=head1 AUTHOR + +Graham Barr. Currently maintained by the Perl Porters. Please report all +bugs at L. + +=head1 COPYRIGHT + +Copyright (c) 1997-8 Graham Barr . All rights reserved. +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +=cut + From f52c60e7f9ca304842ab024e0edeb6a66593f22b Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Mar 2026 09:40:20 +0100 Subject: [PATCH 2/4] Add jprove documentation to testing guide Document the jprove wrapper in docs/reference/testing.md alongside the existing perl_test_runner.pl documentation. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- docs/reference/testing.md | 46 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/reference/testing.md b/docs/reference/testing.md index 02be897c1..01205a45d 100644 --- a/docs/reference/testing.md +++ b/docs/reference/testing.md @@ -63,7 +63,51 @@ perl dev/tools/perl_test_runner.pl --jobs 4 --timeout 20 src/test/resources/unit --jperl PATH Path to jperl executable (default: ./jperl) ``` -### 2. JUnit/Gradle Testing (For CI/CD) +### 2. Using jprove (Standard Perl prove) + +PerlOnJava includes `jprove`, a wrapper that runs the standard Perl `prove` test harness with jperl: + +```bash +# Run tests in a directory +./jprove src/test/resources/unit + +# Run with verbose output +./jprove -v t/*.t + +# Run specific test files +./jprove t/basic.t t/advanced.t + +# Run recursively +./jprove -r t/ + +# Run with parallel jobs +./jprove -j4 t/ +``` + +**Common Options:** +```bash +-v, --verbose Print all test lines +-l, --lib Add 'lib' to @INC +-r, --recurse Recursively descend into directories +-j, --jobs N Run N test jobs in parallel +-q, --quiet Suppress some test output +--timer Print elapsed time after each test +--color Colored test output (default) +--nocolor Disable colored output +``` + +**Example Output:** +``` +./jprove src/test/resources/unit/array.t +src/test/resources/unit/array.t .. ok +All tests successful. +Files=1, Tests=15, 1 wallclock secs +Result: PASS +``` + +`jprove` is useful when you want standard Perl `prove` behavior and options, while `perl_test_runner.pl` provides additional features like JSON reporting and feature impact analysis. + +### 3. JUnit/Gradle Testing (For CI/CD) Uses JUnit 5 with tags for test filtering: From a09b0ded4064615f1ac880910066cbd4d9ea91fa Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Mar 2026 09:41:13 +0100 Subject: [PATCH 3/4] Add jprove to changelog Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- docs/about/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 96d7f1509..e5286a93d 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,7 +6,7 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. ## v5.42.3: Unreleased - Next minor version - Security: added `SECURITY.md` and CycloneDX SBOM generation (`make sbom`) -- Tools: added `jcpan` and `jperldoc` +- Tools: added `jcpan`, `jperldoc`, and `jprove` - Perl debugger with `-d` command line option - Add `defer` feature - Non-local control flow: `last`/`next`/`redo`/`goto LABEL` From ddd646f635129cbf7f4f75cbedeffe29a16d2133 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Mar 2026 09:47:16 +0100 Subject: [PATCH 4/4] Mark constant.pm as protected in sync config PerlOnJava has a custom constant.pm implementation that should not be overwritten by sync.pl. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- dev/import-perl5/config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/import-perl5/config.yaml b/dev/import-perl5/config.yaml index 01ed50b94..123184d3d 100644 --- a/dev/import-perl5/config.yaml +++ b/dev/import-perl5/config.yaml @@ -641,9 +641,10 @@ imports: - source: perl5/lib/Class/Struct.pm target: src/main/perl/lib/Class/Struct.pm - # From core distribution + # From core distribution - PerlOnJava has custom implementation - source: perl5/dist/constant/lib/constant.pm target: src/main/perl/lib/constant.pm + protected: true # Tests for distribution - source: perl5/dist/constant/t