1package App::Prove::State; 2 3use strict; 4use vars qw($VERSION @ISA); 5 6use File::Find; 7use File::Spec; 8use Carp; 9 10use App::Prove::State::Result; 11use TAP::Parser::YAMLish::Reader (); 12use TAP::Parser::YAMLish::Writer (); 13use TAP::Base; 14 15BEGIN { 16 @ISA = qw( TAP::Base ); 17 __PACKAGE__->mk_methods('result_class'); 18} 19 20use constant IS_WIN32 => ( $^O =~ /^(MS)?Win32$/ ); 21use constant NEED_GLOB => IS_WIN32; 22 23=head1 NAME 24 25App::Prove::State - State storage for the C<prove> command. 26 27=head1 VERSION 28 29Version 3.23 30 31=cut 32 33$VERSION = '3.23'; 34 35=head1 DESCRIPTION 36 37The C<prove> command supports a C<--state> option that instructs it to 38store persistent state across runs. This module implements that state 39and the operations that may be performed on it. 40 41=head1 SYNOPSIS 42 43 # Re-run failed tests 44 $ prove --state=fail,save -rbv 45 46=cut 47 48=head1 METHODS 49 50=head2 Class Methods 51 52=head3 C<new> 53 54Accepts a hashref with the following key/value pairs: 55 56=over 4 57 58=item * C<store> 59 60The filename of the data store holding the data that App::Prove::State reads. 61 62=item * C<extensions> (optional) 63 64The test name extensions. Defaults to C<.t>. 65 66=item * C<result_class> (optional) 67 68The name of the C<result_class>. Defaults to C<App::Prove::State::Result>. 69 70=back 71 72=cut 73 74# override TAP::Base::new: 75sub new { 76 my $class = shift; 77 my %args = %{ shift || {} }; 78 79 my $self = bless { 80 select => [], 81 seq => 1, 82 store => delete $args{store}, 83 extensions => ( delete $args{extensions} || ['.t'] ), 84 result_class => ( delete $args{result_class} || 'App::Prove::State::Result' ), 85 }, $class; 86 87 $self->{_} = $self->result_class->new( 88 { tests => {}, 89 generation => 1, 90 } 91 ); 92 my $store = $self->{store}; 93 $self->load($store) 94 if defined $store && -f $store; 95 96 return $self; 97} 98 99=head2 C<result_class> 100 101Getter/setter for the name of the class used for tracking test results. This 102class should either subclass from C<App::Prove::State::Result> or provide an 103identical interface. 104 105=cut 106 107=head2 C<extensions> 108 109Get or set the list of extensions that files must have in order to be 110considered tests. Defaults to ['.t']. 111 112=cut 113 114sub extensions { 115 my $self = shift; 116 $self->{extensions} = shift if @_; 117 return $self->{extensions}; 118} 119 120=head2 C<results> 121 122Get the results of the last test run. Returns a C<result_class()> instance. 123 124=cut 125 126sub results { 127 my $self = shift; 128 $self->{_} || $self->result_class->new; 129} 130 131=head2 C<commit> 132 133Save the test results. Should be called after all tests have run. 134 135=cut 136 137sub commit { 138 my $self = shift; 139 if ( $self->{should_save} ) { 140 $self->save; 141 } 142} 143 144=head2 Instance Methods 145 146=head3 C<apply_switch> 147 148 $self->apply_switch('failed,save'); 149 150Apply a list of switch options to the state, updating the internal 151object state as a result. Nothing is returned. 152 153Diagnostics: 154 - "Illegal state option: %s" 155 156=over 157 158=item C<last> 159 160Run in the same order as last time 161 162=item C<failed> 163 164Run only the failed tests from last time 165 166=item C<passed> 167 168Run only the passed tests from last time 169 170=item C<all> 171 172Run all tests in normal order 173 174=item C<hot> 175 176Run the tests that most recently failed first 177 178=item C<todo> 179 180Run the tests ordered by number of todos. 181 182=item C<slow> 183 184Run the tests in slowest to fastest order. 185 186=item C<fast> 187 188Run test tests in fastest to slowest order. 189 190=item C<new> 191 192Run the tests in newest to oldest order. 193 194=item C<old> 195 196Run the tests in oldest to newest order. 197 198=item C<save> 199 200Save the state on exit. 201 202=back 203 204=cut 205 206sub apply_switch { 207 my $self = shift; 208 my @opts = @_; 209 210 my $last_gen = $self->results->generation - 1; 211 my $last_run_time = $self->results->last_run_time; 212 my $now = $self->get_time; 213 214 my @switches = map { split /,/ } @opts; 215 216 my %handler = ( 217 last => sub { 218 $self->_select( 219 where => sub { $_->generation >= $last_gen }, 220 order => sub { $_->sequence } 221 ); 222 }, 223 failed => sub { 224 $self->_select( 225 where => sub { $_->result != 0 }, 226 order => sub { -$_->result } 227 ); 228 }, 229 passed => sub { 230 $self->_select( where => sub { $_->result == 0 } ); 231 }, 232 all => sub { 233 $self->_select(); 234 }, 235 todo => sub { 236 $self->_select( 237 where => sub { $_->num_todo != 0 }, 238 order => sub { -$_->num_todo; } 239 ); 240 }, 241 hot => sub { 242 $self->_select( 243 where => sub { defined $_->last_fail_time }, 244 order => sub { $now - $_->last_fail_time } 245 ); 246 }, 247 slow => sub { 248 $self->_select( order => sub { -$_->elapsed } ); 249 }, 250 fast => sub { 251 $self->_select( order => sub { $_->elapsed } ); 252 }, 253 new => sub { 254 $self->_select( order => sub { -$_->mtime } ); 255 }, 256 old => sub { 257 $self->_select( order => sub { $_->mtime } ); 258 }, 259 fresh => sub { 260 $self->_select( where => sub { $_->mtime >= $last_run_time } ); 261 }, 262 save => sub { 263 $self->{should_save}++; 264 }, 265 adrian => sub { 266 unshift @switches, qw( hot all save ); 267 }, 268 ); 269 270 while ( defined( my $ele = shift @switches ) ) { 271 my ( $opt, $arg ) 272 = ( $ele =~ /^([^:]+):(.*)/ ) 273 ? ( $1, $2 ) 274 : ( $ele, undef ); 275 my $code = $handler{$opt} 276 || croak "Illegal state option: $opt"; 277 $code->($arg); 278 } 279 return; 280} 281 282sub _select { 283 my ( $self, %spec ) = @_; 284 push @{ $self->{select} }, \%spec; 285} 286 287=head3 C<get_tests> 288 289Given a list of args get the names of tests that should run 290 291=cut 292 293sub get_tests { 294 my $self = shift; 295 my $recurse = shift; 296 my @argv = @_; 297 my %seen; 298 299 my @selected = $self->_query; 300 301 unless ( @argv || @{ $self->{select} } ) { 302 @argv = $recurse ? '.' : 't'; 303 croak qq{No tests named and '@argv' directory not found} 304 unless -d $argv[0]; 305 } 306 307 push @selected, $self->_get_raw_tests( $recurse, @argv ) if @argv; 308 return grep { !$seen{$_}++ } @selected; 309} 310 311sub _query { 312 my $self = shift; 313 if ( my @sel = @{ $self->{select} } ) { 314 warn "No saved state, selection will be empty\n" 315 unless $self->results->num_tests; 316 return map { $self->_query_clause($_) } @sel; 317 } 318 return; 319} 320 321sub _query_clause { 322 my ( $self, $clause ) = @_; 323 my @got; 324 my $results = $self->results; 325 my $where = $clause->{where} || sub {1}; 326 327 # Select 328 for my $name ( $results->test_names ) { 329 next unless -f $name; 330 local $_ = $results->test($name); 331 push @got, $name if $where->(); 332 } 333 334 # Sort 335 if ( my $order = $clause->{order} ) { 336 @got = map { $_->[0] } 337 sort { 338 ( defined $b->[1] <=> defined $a->[1] ) 339 || ( ( $a->[1] || 0 ) <=> ( $b->[1] || 0 ) ) 340 } map { 341 [ $_, 342 do { local $_ = $results->test($_); $order->() } 343 ] 344 } @got; 345 } 346 347 return @got; 348} 349 350sub _get_raw_tests { 351 my $self = shift; 352 my $recurse = shift; 353 my @argv = @_; 354 my @tests; 355 356 # Do globbing on Win32. 357 @argv = map { glob "$_" } @argv if NEED_GLOB; 358 my $extensions = $self->{extensions}; 359 360 for my $arg (@argv) { 361 if ( '-' eq $arg ) { 362 push @argv => <STDIN>; 363 chomp(@argv); 364 next; 365 } 366 367 push @tests, 368 sort -d $arg 369 ? $recurse 370 ? $self->_expand_dir_recursive( $arg, $extensions ) 371 : map { glob( File::Spec->catfile( $arg, "*$_" ) ) } @{$extensions} 372 : $arg; 373 } 374 return @tests; 375} 376 377sub _expand_dir_recursive { 378 my ( $self, $dir, $extensions ) = @_; 379 380 my @tests; 381 my $ext_string = join( '|', map { quotemeta } @{$extensions} ); 382 383 find( 384 { follow => 1, #21938 385 follow_skip => 2, 386 wanted => sub { 387 -f 388 && /(?:$ext_string)$/ 389 && push @tests => $File::Find::name; 390 } 391 }, 392 $dir 393 ); 394 return @tests; 395} 396 397=head3 C<observe_test> 398 399Store the results of a test. 400 401=cut 402 403# Store: 404# last fail time 405# last pass time 406# last run time 407# most recent result 408# most recent todos 409# total failures 410# total passes 411# state generation 412# parser 413 414sub observe_test { 415 416 my ( $self, $test_info, $parser ) = @_; 417 my $name = $test_info->[0]; 418 my $fail = scalar( $parser->failed ) + ( $parser->has_problems ? 1 : 0 ); 419 my $todo = scalar( $parser->todo ); 420 my $start_time = $parser->start_time; 421 my $end_time = $parser->end_time, 422 423 my $test = $self->results->test($name); 424 425 $test->sequence( $self->{seq}++ ); 426 $test->generation( $self->results->generation ); 427 428 $test->run_time($end_time); 429 $test->result($fail); 430 $test->num_todo($todo); 431 $test->elapsed( $end_time - $start_time ); 432 433 $test->parser($parser); 434 435 if ($fail) { 436 $test->total_failures( $test->total_failures + 1 ); 437 $test->last_fail_time($end_time); 438 } 439 else { 440 $test->total_passes( $test->total_passes + 1 ); 441 $test->last_pass_time($end_time); 442 } 443} 444 445=head3 C<save> 446 447Write the state to a file. 448 449=cut 450 451sub save { 452 my ($self) = @_; 453 454 my $store = $self->{store} or return; 455 $self->results->last_run_time( $self->get_time ); 456 457 my $writer = TAP::Parser::YAMLish::Writer->new; 458 local *FH; 459 open FH, ">$store" or croak "Can't write $store ($!)"; 460 $writer->write( $self->results->raw, \*FH ); 461 close FH; 462} 463 464=head3 C<load> 465 466Load the state from a file 467 468=cut 469 470sub load { 471 my ( $self, $name ) = @_; 472 my $reader = TAP::Parser::YAMLish::Reader->new; 473 local *FH; 474 open FH, "<$name" or croak "Can't read $name ($!)"; 475 476 # XXX this is temporary 477 $self->{_} = $self->result_class->new( 478 $reader->read( 479 sub { 480 my $line = <FH>; 481 defined $line && chomp $line; 482 return $line; 483 } 484 ) 485 ); 486 487 # $writer->write( $self->{tests} || {}, \*FH ); 488 close FH; 489 $self->_regen_seq; 490 $self->_prune_and_stamp; 491 $self->results->generation( $self->results->generation + 1 ); 492} 493 494sub _prune_and_stamp { 495 my $self = shift; 496 497 my $results = $self->results; 498 my @tests = $self->results->tests; 499 for my $test (@tests) { 500 my $name = $test->name; 501 if ( my @stat = stat $name ) { 502 $test->mtime( $stat[9] ); 503 } 504 else { 505 $results->remove($name); 506 } 507 } 508} 509 510sub _regen_seq { 511 my $self = shift; 512 for my $test ( $self->results->tests ) { 513 $self->{seq} = $test->sequence + 1 514 if defined $test->sequence && $test->sequence >= $self->{seq}; 515 } 516} 517 5181; 519