1 package TAP::Parser::Scheduler;
6 use TAP::Parser::Scheduler::Job;
7 use TAP::Parser::Scheduler::Spinner;
11 TAP::Parser::Scheduler - Schedule tests during parallel testing
23 use TAP::Parser::Scheduler;
33 my $sched = TAP::Parser::Scheduler->new;
35 Returns a new C<TAP::Parser::Scheduler> object.
42 croak "Need a number of key, value pairs" if @_ % 2;
45 my $tests = delete $args{tests} || croak "Need a 'tests' argument";
46 my $rules = delete $args{rules} || { par => '**' };
48 croak "Unknown arg(s): ", join ', ', sort keys %args
51 # Turn any simple names into a name, description pair. TODO: Maybe
52 # construct jobs here?
53 my $self = bless {}, $class;
55 $self->_set_rules( $rules, $tests );
60 # Build the scheduler data structure.
62 # SCHEDULER-DATA ::= JOB
63 # || ARRAY OF ARRAY OF SCHEDULER-DATA
65 # The nested arrays are the key to scheduling. The outer array contains
66 # a list of things that may be executed in parallel. Whenever an
67 # eligible job is sought any element of the outer array that is ready to
68 # execute can be selected. The inner arrays represent sequential
69 # execution. They can only proceed when the first job is ready to run.
72 my ( $self, $rules, $tests ) = @_;
73 my @tests = map { TAP::Parser::Scheduler::Job->new(@$_) }
74 map { 'ARRAY' eq ref $_ ? $_ : [ $_, $_ ] } @$tests;
75 my $schedule = $self->_rule_clause( $rules, \@tests );
77 # If any tests are left add them as a sequential block at the end of
79 $schedule = [ [ $schedule, @tests ] ] if @tests;
81 $self->{schedule} = $schedule;
85 my ( $self, $rule, $tests ) = @_;
86 croak 'Rule clause must be a hash'
87 unless 'HASH' eq ref $rule;
89 my @type = keys %$rule;
90 croak 'Rule clause must have exactly one key'
97 seq => sub { [ [@_] ] },
100 my $handler = $handlers{ $type[0] }
101 || croak 'Unknown scheduler type: ', $type[0];
102 my $val = $rule->{ $type[0] };
107 ? $self->_rule_clause( $_, $tests )
108 : $self->_expand( $_, $tests )
109 } 'ARRAY' eq ref $val ? @$val : $val
114 my ( $self, $name, $tests ) = @_;
116 $name =~ s{(\?|\*\*?|.)}{
118 : $1 eq '*' ? '[^/]*'
123 my $pattern = qr{^$name$};
126 for ( my $ti = 0; $ti < @$tests; $ti++ ) {
127 if ( $tests->[$ti]->filename =~ $pattern ) {
128 push @match, splice @$tests, $ti, 1;
138 Get a list of all remaining tests.
144 $self->_gather( $self->{schedule} );
148 my ( $self, $rule ) = @_;
149 return unless defined $rule;
150 return $rule unless 'ARRAY' eq ref $rule;
151 return map { $self->_gather($_) } grep {defined} map {@$_} @$rule;
156 Return the next available job or C<undef> if none are available. Returns
157 a C<TAP::Parser::Scheduler::Spinner> if the scheduler still has pending
158 jobs but none are available to run right now.
164 my @jobs = $self->_find_next_job( $self->{schedule} );
165 return $jobs[0] if @jobs;
167 # TODO: This isn't very efficient...
168 return TAP::Parser::Scheduler::Spinner->new
176 return 1 unless defined $ar && 'ARRAY' eq ref $ar;
177 return 1 if grep { _not_empty($_) } @$ar;
181 sub _is_empty { !_not_empty(@_) }
184 my ( $self, $rule ) = @_;
187 for my $seq (@$rule) {
189 # Prune any exhausted items.
190 shift @$seq while @$seq && _is_empty( $seq->[0] );
191 if ( @$seq && defined $seq->[0] ) {
192 if ( 'ARRAY' eq ref $seq->[0] ) {
196 my $job = splice @$seq, 0, 1, undef;
197 $job->on_finish( sub { shift @$seq } );
203 for my $seq (@queue) {
204 if ( my @jobs = $self->_find_next_job( $seq->[0] ) ) {
214 Return a human readable representation of the scheduling tree.
220 return $self->_as_string( $self->{schedule} );
224 my ( $self, $rule, $depth ) = ( shift, shift, shift || 0 );
226 my $indent = $pad x $depth;
227 if ( !defined $rule ) {
228 return "$indent(undef)\n";
230 elsif ( 'ARRAY' eq ref $rule ) {
231 return unless @$rule;
232 my $type = ( 'par', 'seq' )[ $depth % 2 ];
234 '', "$indent$type:\n",
235 map { $self->_as_string( $_, $depth + 1 ) } @$rule
239 return "$indent'" . $rule->filename . "'\n";