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