File Coverage

blib/lib/App/TestOnTap/Dispenser.pm
Criterion Covered Total %
statement 134 144 93.0
branch 31 40 77.5
condition 14 17 82.3
subroutine 15 15 100.0
pod 0 3 0.0
total 194 219 88.5


line stmt bran cond sub pod time code
1             package App::TestOnTap::Dispenser;
2              
3 19     19   133 use strict;
  19         50  
  19         535  
4 19     19   98 use warnings;
  19         55  
  19         1080  
5              
6             our $VERSION = '1.001';
7             my $version = $VERSION;
8             $VERSION = eval $VERSION;
9              
10 19     19   127 use App::TestOnTap::Util qw(slashify);
  19         41  
  19         1111  
11 19     19   146 use App::TestOnTap::OrderStrategy;
  19         84  
  19         561  
12              
13 19     19   132 use File::Find;
  19         51  
  19         1318  
14 19     19   137 use List::Util qw(shuffle max);
  19         40  
  19         28664  
15              
16             # CTOR
17             #
18             sub new
19             {
20 27     27 0 92 my $class = shift;
21 27         62 my $args = shift;
22              
23 27         293 my $self = bless( { args => $args, inprogress => {}, orderstrategy => App::TestOnTap::OrderStrategy->new() }, $class);
24 27         184 $self->__analyze();
25            
26 26         158 return $self;
27             }
28              
29             sub __analyze
30             {
31 27     27   75 my $self = shift;
32              
33             # find all tests in the suite root
34             # (subject to config skip filtering)
35             #
36 27         187 my $tests = $self->__scan();
37              
38             # create a graph with all the tests as 'from' vertices, begin with no dependencies
39             #
40 27         106 my %graph = map { $_ => [] } @$tests;
  54         184  
41            
42             # figure out the longest dep rule name with some extra to produce some
43             # nice delimiters...
44             #
45 27         129 my @depRuleNames = $self->{args}->getConfig()->getDependencyRuleNames();
46 27         78 my $longestDepRuleName = 0;
47 27         92 $longestDepRuleName = max($longestDepRuleName, length($_)) foreach (@depRuleNames);
48 27         65 $longestDepRuleName += 18;
49 27         113 my $topDelimLine = '*' x $longestDepRuleName;
50              
51             # iterate over all dependency rules and add edges from => to vertices
52             #
53 27         99 foreach my $depRuleName (@depRuleNames)
54             {
55 5         21 my ($fromVertices, $toVertices) = $self->{args}->getConfig()->getMatchesAndDependenciesForRule($depRuleName, $tests);
56 5 50       23 if ($self->{args}->doDryRun())
57             {
58 0         0 print "$topDelimLine\n";
59 0         0 print "Dependency rule '$depRuleName'\n";
60 0         0 print "Query for 'match' matches:\n";
61 0 0       0 if (@$fromVertices)
62             {
63 0         0 print " $_\n" foreach (@$fromVertices);
64             }
65             else
66             {
67 0         0 print " (nothing)\n";
68             }
69 0         0 print "Query for 'dependson' matches:\n";
70 0 0       0 if (@$toVertices)
71             {
72 0         0 print " $_\n" foreach (@$toVertices);
73             }
74             else
75             {
76 0         0 print "(nothing)\n";
77             }
78             }
79             else
80             {
81 5 50       19 warn("WARNING: No tests selected by 'match' in dependency rule '$depRuleName'\n") unless @$fromVertices;
82 5 50       13 warn("WARNING: No tests selected by 'dependson' in dependency rule '$depRuleName'\n") unless @$toVertices;
83             }
84 5         11 push(@{$graph{$_}}, @$toVertices) foreach (@$fromVertices);
  5         21  
85             }
86            
87 27         130 $self->{args}->getWorkDirManager()->recordFullGraph(%graph);
88            
89             # trap any cyclic dependency problems right now
90             #
91 27         161 $self->__toposort(\%graph);
92              
93             # tentatively store the full graph
94             #
95 26         84 $self->{graph} = \%graph;
96            
97             # if user decided to supply a skip filter, now try to create a
98             # graph filtered to matching tests, but without including dependencies...
99             #
100 26         145 my $prunedTests = $self->{args}->include($tests);
101            
102             # ...but now make sure dependencies are brought along whether
103             # they we're filtered in or not...
104             #
105 26 100 66     685 if ($prunedTests && scalar(@$prunedTests) != scalar(@$tests))
106             {
107 2         12 my %prunedGraph;
108              
109             # iteratively pick up all tests and dependencies
110             # as long as we're picking up new deps, we need to iterate again
111             #
112 2         16 my @newDeps = @$prunedTests;
113 2         13 while (my $t = shift(@newDeps))
114             {
115 2 50       8 if (!exists($prunedGraph{$t}))
116             {
117 2         7 $prunedGraph{$t} = $graph{$t};
118 2         5 push(@newDeps, @{$prunedGraph{$t}});
  2         13  
119             }
120             }
121            
122             # store the pruned graph instead
123             #
124 2         9 $self->{graph} = \%prunedGraph;
125            
126 2         10 $self->{args}->getWorkDirManager()->recordPrunedGraph(%prunedGraph);
127             }
128             }
129              
130             sub getAllTests
131             {
132 26     26 0 69 my $self = shift;
133            
134 26         60 return keys(%{$self->{graph}});
  26         264  
135             }
136              
137             sub getEligibleTests
138             {
139 81     81 0 257 my $self = shift;
140 81   50     286 my $completed = shift || [];
141              
142             # remove items that have been completed from the graph
143             #
144 81         382 foreach my $t (@$completed)
145             {
146 48         474 delete($self->{graph}->{$t});
147 48         354 delete($self->{inprogress}->{$t});
148             }
149              
150             # no more items to run at all - we're finished
151             #
152 81 100       245 return unless keys(%{$self->{graph}});
  81         509  
153              
154             # if we're still here, remove any references to completed tests
155             # from the remaining tests
156             #
157 55         169 foreach my $removed (@$completed)
158             {
159 22         66 foreach my $t (keys(%{$self->{graph}}))
  22         151  
160             {
161 32         85 $self->{graph}->{$t} = [ grep( !/^\Q$removed\E$/, @{$self->{graph}->{$t}} ) ];
  32         279  
162             }
163             }
164              
165             # extract those ready to run and separate them into parallelizable and not parallelizable
166             #
167 55         157 my @parallelizable;
168             my @nonParallelizable;
169 55         252 foreach my $t (keys(%{$self->{graph}}))
  55         209  
170             {
171             # tests that have no dependencies and are not already in progress are
172             # now ready to run
173             #
174 98 100 100     183 if (!@{$self->{graph}->{$t}} && !$self->{inprogress}->{$t})
  98         681  
175             {
176 82 100       387 if ($self->{args}->getConfig()->parallelizable($t))
177             {
178 6         771 push(@parallelizable, $t);
179             }
180             else
181             {
182 76         236 push(@nonParallelizable, $t);
183             }
184             }
185             }
186              
187             # order them according to the chosen strategy
188             #
189 55   66     399 my $orderstrategy = $self->{args}->getOrderStrategy() || $self->{args}->getConfig()->getOrderStrategy() || $self->{orderstrategy};
190 55         267 $self->{args}->getWorkDirManager()->recordOrderStrategy($orderstrategy);
191 55         582 @parallelizable = $orderstrategy->orderList(@parallelizable);
192 55         218 @nonParallelizable = $orderstrategy->orderList(@nonParallelizable);
193            
194             # check the parallel groups for max concurrency and cull the overflow (don't forget to account for in progress jobs)
195             #
196 55         218 @parallelizable = $self->{args}->getConfig()->getParallelGroupManager()->cull([ keys(%{$self->{inprogress}}) ], \@parallelizable);
  55         797  
197            
198             # now finally select those eligible - try to do away with parallelizable first
199             #
200 55         171 my @eligible = @parallelizable;
201            
202             # we only deal with non-parallelizables if:
203             # - nothing else already is eligible
204             # - there are any to deal out at all...
205             # - nothing else is presently in progress
206             # if so, just pick the first
207             #
208 55 100 100     511 if (!@eligible && @nonParallelizable && !keys(%{$self->{inprogress}}))
  48   100     239  
209             {
210 42         138 @eligible = $nonParallelizable[0];
211             }
212            
213             # make a note that those we return are in progress
214             #
215 55         233 $self->{inprogress}->{$_} = 1 foreach (@eligible);
216              
217 55         222 $self->{args}->getWorkDirManager()->recordDispensedOrder(@eligible);
218              
219 55         205 return \@eligible;
220             }
221              
222             # SUB helpers
223             #
224              
225             # scan the suite root and find all tests
226             # (subject to the config skip filter)
227             #
228             sub __scan
229             {
230 27     27   81 my $self = shift;
231              
232 27         222 my $config = $self->{args}->getConfig();
233            
234 27         59 my @tests;
235            
236             # to simplify during preprocessing, ensure we have a suite root using forward slashes
237             #
238 27         134 my $srfs = slashify($self->{args}->getSuiteRoot(), '/');
239            
240             # set up a File::Find preprocessor in order to weed out parts of the scanned tree
241             # that are selected by the optional config skip filter
242             #
243             my $preprocess =
244             sub
245             {
246             # stash entries that should be further processed here
247             #
248 27     27   105 my @keep;
249            
250 27         90 foreach my $entry (@_)
251             {
252             # skip the '.' and '..' entries
253             #
254 139 100       925 next if $entry =~ /^\.\.?$/;
255            
256             # skip any config entries
257             #
258 85 100       332 next if $entry eq $config->getName();
259            
260             # skip entries that are not selected by the config filter
261             # however, filters are written assuming they're passed paths
262             # relative from the suiteroot, and delimited by '/' so construct strings
263             # to conform by first normalizing and then stripping the absolute part
264             # to the suite root; ensure dirs are suffixed by a '/'.
265             #
266 58         304 my $p = slashify("$File::Find::dir/$entry", '/');
267 58 50       1045 $p .= '/' if -d $p;
268 58         726 $p =~ s#^\Q$srfs\E/##;
269 58 100       307 next if $config->skip($p);
270            
271 56         438 push(@keep, $entry);
272             }
273            
274             # return the list of entries that should be further processed by the wanted function
275             #
276 27         898 return @keep;
277 27         282 };
278            
279             # set up a wanted processor to select tests based on the execmapper
280             #
281             my $wanted =
282             sub
283             {
284             # normalize the full name
285             #
286 83     83   390 my $fn = slashify($File::Find::name, '/');
287            
288             # ignore any directories (they can't be tests anyway)
289             #
290 83 100       3671 return if -d $fn;
291            
292             # normalize to test name
293             #
294 56         638 $fn =~ s#^\Q$srfs\E/##;
295              
296             # ignore files with extensions not handled by the exec mapper
297             #
298 56 100       311 return unless $config->hasExecMapping($fn);
299              
300             # store it as a test!
301             #
302 54         7868 push(@tests, $fn);
303 27         231 };
304            
305             # execute the find
306             #
307 27         5049 find( { preprocess => $preprocess, wanted => $wanted }, $srfs);
308            
309 27         211 $self->{args}->getWorkDirManager()->recordFoundTests(@tests);
310            
311 27         323 return \@tests;
312             }
313              
314             # essentially follows the algorithm for depth-first search as described
315             # at https://en.wikipedia.org/wiki/Topological_sorting
316             #
317             # minor change is that since we will use the toposort
318             # bottom-up, we push to tail of L instead of unshift to head
319             #
320             sub __toposort
321             {
322 27     27   142 my $self = shift;
323 27         93 my $graph = shift;
324            
325 27         87 my ($unmarked, $tmpmarked, $permmarked) = (0, 1, 2);
326 27         96 my @keys = keys(%$graph);
327 27         75 my %g = map { $_ => { deps => $graph->{$_}, mark => $unmarked } } @keys;
  54         315  
328              
329 27         83 my @sorted;
330              
331             my $visitor;
332             $visitor =
333             sub
334             {
335 57     57   103 my $node = shift;
336 57         140 my @route = @_;
337            
338 57 100       210 die("ERROR: Cyclic dependency detected: " . join(' => ', @route, $node) . "!\n") if ($g{$node}->{mark} == $tmpmarked);
339              
340 56 100       166 return if $g{$node}->{mark};
341            
342 54         111 $g{$node}->{mark} = $tmpmarked;
343 54         80 $visitor->($_, @route, $node) foreach (@{$g{$node}->{deps}});
  54         155  
344 52         104 $g{$node}->{mark} = $permmarked;
345 52         122 push(@sorted, $node);
346 27         247 };
347            
348 27         121 foreach my $node (@keys)
349             {
350 53 100       173 next unless $g{$node}->{mark} == $unmarked;
351 52         122 $visitor->($node);
352             }
353              
354 26         73 return @sorted;
355             }
356              
357             1;