xref: /openbsd-src/gnu/usr.bin/perl/cpan/Test-Harness/lib/App/Prove/State.pm (revision d13be5d47e4149db2549a9828e244d59dbc43f15)
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;
518