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