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