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   140 use strict;
  19         57  
  19         551  
4 19     19   111 use warnings;
  19         54  
  19         582  
5              
6 19     19   131 use App::TestOnTap::Util qw(slashify);
  19         55  
  19         994  
7 19     19   127 use App::TestOnTap::OrderStrategy;
  19         37  
  19         560  
8              
9 19     19   113 use File::Find;
  19         54  
  19         1229  
10 19     19   139 use List::Util qw(shuffle max);
  19         53  
  19         29689  
11              
12             # CTOR
13             #
14             sub new
15             {
16 27     27 0 107 my $class = shift;
17 27         76 my $args = shift;
18              
19 27         339 my $self = bless( { args => $args, inprogress => {}, orderstrategy => App::TestOnTap::OrderStrategy->new() }, $class);
20 27         178 $self->__analyze();
21            
22 26         155 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         180 my $tests = $self->__scan();
33              
34             # create a graph with all the tests as 'from' vertices, begin with no dependencies
35             #
36 27         113 my %graph = map { $_ => [] } @$tests;
  54         213  
37            
38             # figure out the longest dep rule name with some extra to produce some
39             # nice delimiters...
40             #
41 27         175 my @depRuleNames = $self->{args}->getConfig()->getDependencyRuleNames();
42 27         99 my $longestDepRuleName = 0;
43 27         134 $longestDepRuleName = max($longestDepRuleName, length($_)) foreach (@depRuleNames);
44 27         86 $longestDepRuleName += 18;
45 27         137 my $topDelimLine = '*' x $longestDepRuleName;
46              
47             # iterate over all dependency rules and add edges from => to vertices
48             #
49 27         126 foreach my $depRuleName (@depRuleNames)
50             {
51 5         23 my ($fromVertices, $toVertices) = $self->{args}->getConfig()->getMatchesAndDependenciesForRule($depRuleName, $tests);
52 5 50       28 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       42 warn("WARNING: No tests selected by 'match' in dependency rule '$depRuleName'\n") unless @$fromVertices;
78 5 50       26 warn("WARNING: No tests selected by 'dependson' in dependency rule '$depRuleName'\n") unless @$toVertices;
79             }
80 5         23 push(@{$graph{$_}}, @$toVertices) foreach (@$fromVertices);
  5         26  
81             }
82            
83 27         136 $self->{args}->getWorkDirManager()->recordFullGraph(%graph);
84            
85             # trap any cyclic dependency problems right now
86             #
87 27         180 $self->__toposort(\%graph);
88              
89             # tentatively store the full graph
90             #
91 26         79 $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         161 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     651 if ($prunedTests && scalar(@$prunedTests) != scalar(@$tests))
102             {
103 2         6 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         7 my @newDeps = @$prunedTests;
109 2         9 while (my $t = shift(@newDeps))
110             {
111 2 50       12 if (!exists($prunedGraph{$t}))
112             {
113 2         16 $prunedGraph{$t} = $graph{$t};
114 2         6 push(@newDeps, @{$prunedGraph{$t}});
  2         8  
115             }
116             }
117            
118             # store the pruned graph instead
119             #
120 2         6 $self->{graph} = \%prunedGraph;
121            
122 2         10 $self->{args}->getWorkDirManager()->recordPrunedGraph(%prunedGraph);
123             }
124             }
125              
126             sub getAllTests
127             {
128 26     26 0 63 my $self = shift;
129            
130 26         63 return keys(%{$self->{graph}});
  26         279  
131             }
132              
133             sub getEligibleTests
134             {
135 81     81 0 268 my $self = shift;
136 81   50     331 my $completed = shift || [];
137              
138             # remove items that have been completed from the graph
139             #
140 81         493 foreach my $t (@$completed)
141             {
142 48         544 delete($self->{graph}->{$t});
143 48         504 delete($self->{inprogress}->{$t});
144             }
145              
146             # no more items to run at all - we're finished
147             #
148 81 100       255 return unless keys(%{$self->{graph}});
  81         612  
149              
150             # if we're still here, remove any references to completed tests
151             # from the remaining tests
152             #
153 55         187 foreach my $removed (@$completed)
154             {
155 22         71 foreach my $t (keys(%{$self->{graph}}))
  22         211  
156             {
157 32         103 $self->{graph}->{$t} = [ grep( !/^\Q$removed\E$/, @{$self->{graph}->{$t}} ) ];
  32         280  
158             }
159             }
160              
161             # extract those ready to run and separate them into parallelizable and not parallelizable
162             #
163 55         155 my @parallelizable;
164             my @nonParallelizable;
165 55         109 foreach my $t (keys(%{$self->{graph}}))
  55         222  
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         764  
171             {
172 82 100       510 if ($self->{args}->getConfig()->parallelizable($t))
173             {
174 6         621 push(@parallelizable, $t);
175             }
176             else
177             {
178 76         317 push(@nonParallelizable, $t);
179             }
180             }
181             }
182              
183             # order them according to the chosen strategy
184             #
185 55   66     392 my $orderstrategy = $self->{args}->getOrderStrategy() || $self->{args}->getConfig()->getOrderStrategy() || $self->{orderstrategy};
186 55         249 $self->{args}->getWorkDirManager()->recordOrderStrategy($orderstrategy);
187 55         633 @parallelizable = $orderstrategy->orderList(@parallelizable);
188 55         211 @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         251 @parallelizable = $self->{args}->getConfig()->getParallelGroupManager()->cull([ keys(%{$self->{inprogress}}) ], \@parallelizable);
  55         848  
193            
194             # now finally select those eligible - try to do away with parallelizable first
195             #
196 55         178 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     574 if (!@eligible && @nonParallelizable && !keys(%{$self->{inprogress}}))
  48   100     237  
205             {
206 42         176 @eligible = $nonParallelizable[0];
207             }
208            
209             # make a note that those we return are in progress
210             #
211 55         276 $self->{inprogress}->{$_} = 1 foreach (@eligible);
212              
213 55         214 $self->{args}->getWorkDirManager()->recordDispensedOrder(@eligible);
214              
215 55         249 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   76 my $self = shift;
227              
228 27         212 my $config = $self->{args}->getConfig();
229            
230 27         78 my @tests;
231            
232             # to simplify during preprocessing, ensure we have a suite root using forward slashes
233             #
234 27         162 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   116 my @keep;
245            
246 27         121 foreach my $entry (@_)
247             {
248             # skip the '.' and '..' entries
249             #
250 139 100       1015 next if $entry =~ /^\.\.?$/;
251            
252             # skip any config entries
253             #
254 85 100       401 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         326 my $p = slashify("$File::Find::dir/$entry", '/');
263 58 50       1128 $p .= '/' if -d $p;
264 58         815 $p =~ s#^\Q$srfs\E/##;
265 58 100       306 next if $config->skip($p);
266            
267 56         501 push(@keep, $entry);
268             }
269            
270             # return the list of entries that should be further processed by the wanted function
271             #
272 27         927 return @keep;
273 27         320 };
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   419 my $fn = slashify($File::Find::name, '/');
283            
284             # ignore any directories (they can't be tests anyway)
285             #
286 83 100       3741 return if -d $fn;
287            
288             # normalize to test name
289             #
290 56         720 $fn =~ s#^\Q$srfs\E/##;
291              
292             # ignore files with extensions not handled by the exec mapper
293             #
294 56 100       309 return unless $config->hasExecMapping($fn);
295              
296             # store it as a test!
297             #
298 54         8330 push(@tests, $fn);
299 27         241 };
300            
301             # execute the find
302             #
303 27         5134 find( { preprocess => $preprocess, wanted => $wanted }, $srfs);
304            
305 27         214 $self->{args}->getWorkDirManager()->recordFoundTests(@tests);
306            
307 27         359 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   85 my $self = shift;
319 27         60 my $graph = shift;
320            
321 27         106 my ($unmarked, $tmpmarked, $permmarked) = (0, 1, 2);
322 27         121 my @keys = keys(%$graph);
323 27         85 my %g = map { $_ => { deps => $graph->{$_}, mark => $unmarked } } @keys;
  54         329  
324              
325 27         95 my @sorted;
326              
327             my $visitor;
328             $visitor =
329             sub
330             {
331 56     56   111 my $node = shift;
332 56         138 my @route = @_;
333            
334 56 100       211 die("ERROR: Cyclic dependency detected: " . join(' => ', @route, $node) . "!\n") if ($g{$node}->{mark} == $tmpmarked);
335              
336 55 100       190 return if $g{$node}->{mark};
337            
338 54         109 $g{$node}->{mark} = $tmpmarked;
339 54         91 $visitor->($_, @route, $node) foreach (@{$g{$node}->{deps}});
  54         173  
340 52         116 $g{$node}->{mark} = $permmarked;
341 52         132 push(@sorted, $node);
342 27         283 };
343            
344 27         102 foreach my $node (@keys)
345             {
346 53 100       164 next unless $g{$node}->{mark} == $unmarked;
347 51         127 $visitor->($node);
348             }
349              
350 26         91 return @sorted;
351             }
352              
353             1;