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