package App::Prove::State; use strict; use File::Find; use File::Spec; use Carp; use TAP::Parser::YAMLish::Reader (); use TAP::Parser::YAMLish::Writer (); use TAP::Base; use vars qw($VERSION @ISA); @ISA = qw( TAP::Base ); 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.05 =cut $VERSION = '3.05'; =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=fail,save -rbv =cut =head1 METHODS =head2 Class Methods =head3 C =cut sub new { my $class = shift; my %args = %{ shift || {} }; my $self = bless { _ => { tests => {}, generation => 1 }, select => [], seq => 1, store => delete $args{store}, }, $class; my $store = $self->{store}; $self->load($store) if defined $store && -f $store; return $self; } sub DESTROY { my $self = shift; if ( $self->{should_save} && defined( my $store = $self->{store} ) ) { $self->save($store); } } =head2 Instance Methods =head3 C Apply a list of switch options to the state. =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->{_}->{generation} - 1; my $now = $self->get_time; my @switches = map { split /,/ } @opts; my %handler = ( last => sub { $self->_select( where => sub { $_->{gen} >= $last_gen }, order => sub { $_->{seq} } ); }, failed => sub { $self->_select( where => sub { $_->{last_result} != 0 }, order => sub { -$_->{last_result} } ); }, passed => sub { $self->_select( where => sub { $_->{last_result} == 0 } ); }, all => sub { $self->_select(); }, todo => sub { $self->_select( where => sub { $_->{last_todo} != 0 }, order => sub { -$_->{last_todo}; } ); }, hot => sub { $self->_select( where => sub { defined $_->{last_fail_time} }, order => sub { $now - $_->{last_fail_time} } ); }, slow => sub { $self->_select( order => sub { -$_->{elapsed} } ); }, fast => sub { $self->_select( order => sub { $_->{elapsed} } ); }, new => sub { $self->_select( order => sub { ( $_->{total_failures} || 0 ) + ( $_->{total_passes} || 0 ); } ); }, old => sub { $self->_select( order => sub { -( ( $_->{total_failures} || 0 ) + ( $_->{total_passes} || 0 ) ); } ); }, 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); } } 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} } ) { croak q{No tests named and 't' directory not found} unless -d 't'; @argv = 't'; } 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 keys %{ $self->{_}->{tests} }; return map { $self->_query_clause($_) } @sel; } return; } sub _query_clause { my ( $self, $clause ) = @_; my @got; my $tests = $self->{_}->{tests}; my $where = $clause->{where} || sub {1}; # Select for my $test ( sort keys %$tests ) { local $_ = $tests->{$test}; push @got, $test 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 $_ = $tests->{$_}; $order->() } ] } @got; } return @got; } sub _get_raw_tests { my $self = shift; my $recurse = shift; my @argv = @_; my @tests; # Do globbing on Win32. @argv = map { glob "$_" } @argv if NEED_GLOB; for my $arg (@argv) { if ( '-' eq $arg ) { push @argv => ; chomp(@argv); next; } push @tests, sort -d $arg ? $recurse ? $self->_expand_dir_recursive($arg) : glob( File::Spec->catfile( $arg, '*.t' ) ) : $arg; } return @tests; } sub _expand_dir_recursive { my ( $self, $dir ) = @_; my @tests; find( { follow => 1, #21938 wanted => sub { -f && /\.t$/ && push @tests => $File::Find::name; } }, $dir ); return @tests; } =head3 C Store the results of a test. =cut sub observe_test { my ( $self, $test, $parser ) = @_; $self->_record_test( $test, scalar( $parser->failed ) + ( $parser->has_problems ? 1 : 0 ), scalar( $parser->todo ), $parser->start_time, $parser->end_time ); } # Store: # last fail time # last pass time # last run time # most recent result # most recent todos # total failures # total passes # state generation sub _record_test { my ( $self, $test, $fail, $todo, $start_time, $end_time ) = @_; my $rec = $self->{_}->{tests}->{ $test->[0] } ||= {}; $rec->{seq} = $self->{seq}++; $rec->{gen} = $self->{_}->{generation}; $rec->{last_run_time} = $end_time; $rec->{last_result} = $fail; $rec->{last_todo} = $todo; $rec->{elapsed} = $end_time - $start_time; if ($fail) { $rec->{total_failures}++; $rec->{last_fail_time} = $end_time; } else { $rec->{total_passes}++; $rec->{last_pass_time} = $end_time; } } =head3 C Write the state to a file. =cut sub save { my ( $self, $name ) = @_; my $writer = TAP::Parser::YAMLish::Writer->new; local *FH; open FH, ">$name" or croak "Can't write $name ($!)"; $writer->write( $self->{_} || {}, \*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 ($!)"; $self->{_} = $reader->read( sub { my $line = ; defined $line && chomp $line; return $line; } ); # $writer->write( $self->{tests} || {}, \*FH ); close FH; $self->_regen_seq; $self->{_}->{generation}++; } sub _regen_seq { my $self = shift; for my $rec ( values %{ $self->{_}->{tests} || {} } ) { $self->{seq} = $rec->{seq} + 1 if defined $rec->{seq} && $rec->{seq} >= $self->{seq}; } }