File Coverage

blib/lib/App/ape/plan.pm
Criterion Covered Total %
statement 159 233 68.2
branch 39 74 52.7
condition 16 31 51.6
subroutine 14 15 93.3
pod 2 2 100.0
total 230 355 64.7


line stmt bran cond sub pod time code
1             # PODNAME: App::ape::plan
2             # ABSTRACT: plan testing using elasticsearch
3              
4             package App::ape::plan;
5             $App::ape::plan::VERSION = '0.001';
6 3     3   116998 use strict;
  3         13  
  3         74  
7 3     3   12 use warnings;
  3         5  
  3         81  
8              
9 3     3   535 use Getopt::Long qw{GetOptionsFromArray};
  3         8293  
  3         18  
10 3     3   746 use App::Prove::Elasticsearch::Utils;
  3         28  
  3         72  
11 3     3   1170 use App::Prove::State;
  3         29824  
  3         86  
12 3     3   427 use Pod::Usage;
  3         35654  
  3         318  
13 3     3   1975 use IO::Prompter [ -yesno, -single, -stdio, -style => 'bold' ];
  3         74296  
  3         49  
14 3     3   216 use List::Util qw{shuffle};
  3         6  
  3         170  
15 3     3   15 use File::Basename qw{basename};
  3         5  
  3         132  
16 3     3   14 use POSIX qw{strftime};
  3         6  
  3         22  
17              
18             sub new {
19 4     4 1 2382 my ($class, @args) = @_;
20              
21 4         7 my (%options, @conf, $help);
22             GetOptionsFromArray(
23             \@args,
24             'platform=s@' => \$options{platforms},
25             'version=s' => \$options{version},
26             'show' => \$options{show},
27             'prompt' => \$options{prompt},
28             'pairwise' => \$options{pairwise},
29             'all-platforms' => \$options{allplatforms},
30             'recurse' => \$options{recurse},
31             'extension=s@' => \$options{exts},
32             'name' => \$options{name},
33             'requeue' => \$options{requeue},
34             'replay' => \$options{replay},
35 4         37 'help' => \$help,
36             );
37 4   100     2694 $options{platforms} //= [];
38              
39             #Deliberately exiting here, as I "unit" test this as the binary
40 4 50       9 pod2usage(0) if $help;
41              
42 4 100       8 if (!$options{version}) {
43 1         7 pod2usage(
44             -exitval => "NOEXIT",
45             -msg => "Insufficient arguments. You must pass --version.",
46             );
47 1         4732 return 2;
48             }
49              
50 3 50 66     10 if ($options{prompt} && $options{show}) {
51 1         4 pod2usage(
52             -exitval => "NOEXIT",
53             -msg =>
54             "--prompt and --show are mutually exclusive options. You must pass one or the other.",
55             );
56 1         4221 return 3;
57             }
58              
59             #Store platform groups in the configuration to differentiate further plans
60 2         5 my $conf = App::Prove::Elasticsearch::Utils::process_configuration(@conf);
61              
62 2 100       12 if (
63             scalar(
64             grep {
65 2         3 my $subj = $_;
66 2         4 grep { $subj eq $_ } qw{server.host server.port}
  4         8  
67             } keys(%$conf)
68             ) != 2
69             ) {
70 1         5 pod2usage(
71             -exitval => "NOEXIT",
72             -msg =>
73             "Insufficient information provided to associate defect with test results to elasticsearch",
74             );
75 1         4156 return 4;
76             }
77              
78 1         2 my $self = {};
79              
80             #default platforms to whatever platformer can figure out
81 1 0 33     1 if (!scalar(@{$options{platforms}}) && !$options{allplatforms}) {
  1         6  
82 0         0 my $platformer =
83             App::Prove::Elasticsearch::Utils::require_platformer($conf);
84 0         0 $options{platforms} = &{\&{$platformer . "::get_platforms"}}();
  0         0  
  0         0  
85             }
86              
87 1         3 $self->{planner} = App::Prove::Elasticsearch::Utils::require_planner($conf);
88 1         5 &{\&{$self->{planner} . "::check_index"}}($conf);
  1         1  
  1         7  
89              
90 1         3 my $queue = App::Prove::Elasticsearch::Utils::require_queue($conf);
91 1         3 $self->{queue} = &{\&{$queue . "::new"}}($queue, \@conf);
  1         1  
  1         5  
92 1         14 $self->{queue}->{requeue} = $options{requeue};
93              
94 1         4 $self->{searcher} = $self->{queue}->_get_searcher();
95              
96             #Use Prove's arg parser to grab tests & globs correctly
97 1         6 my $proveState = App::Prove::State->new();
98 1 50       7 $proveState->extensions($options{exts}) if $options{exts};
99 1         4 my @tests_filtered = $proveState->get_tests($options{'recurse'}, @args);
100 1         6 @args = map { basename $_ } grep { -f $_ } @tests_filtered;
  0         0  
  0         0  
101 1         2 $self->{cases} = \@args;
102              
103 1         2 $self->{conf} = $conf;
104 1         2 $self->{options} = \%options;
105              
106 1         13 return bless($self, $class);
107             }
108              
109             sub run {
110 5     5 1 1787 my $self = shift;
111              
112             my @plans = _build_plans(
113             $self->{planner}, $self->{conf}, $self->{cases},
114 5         11 %{$self->{options}}
  5         18  
115             );
116              
117 5         17 my $global_result = 0;
118 5         6 my $queue_result = 0;
119 5         17 foreach my $plan (@plans) {
120              
121 5 100       11 if ($self->{options}{show}) {
122 1 50       4 $plan->{replay} = $self->{cases} if $self->{options}{replay};
123              
124             #Get the state of the plan
125 1         2 $plan->{state} = [];
126 1         3 @{$plan->{state}} = &{\&{$self->{planner} . "::get_plan_status"}}
  1         2  
  1         4  
127 1         2 ($plan, $self->{searcher});
128              
129 1         6 _print_plan($plan, 1);
130 1         3 next;
131             }
132 4 100       10 if ($self->{options}{prompt}) {
133 3         6 _print_plan($plan);
134 3 100       10 if (!$plan->{noop}) {
135 2 50       3 IO::Prompter::prompt("Do you want to enact the above changes?")
136             or next;
137             } else {
138             (
139             IO::Prompter::prompt("Do you want to re-queue the plan?")
140             or next
141 1 50 50     4 ) unless $self->{options}{requeue};
142 1         8 $self->{queue}->{requeue} = 1;
143 1         4 $queue_result += $self->{queue}->queue_jobs($plan);
144 1         3 next;
145             }
146             }
147              
148             #Ensure bogus data doesn't get into ES
149 3         9 delete $plan->{replay};
150 3         4 delete $plan->{requeue};
151              
152             $global_result +=
153 3         5 &{\&{$self->{planner} . "::add_plan_to_index"}}($plan);
  3         3  
  3         11  
154             $queue_result += $self->{queue}->queue_jobs($plan)
155 3 50 66     21 if !$plan->{noop} || $self->{options}{requeue};
156             }
157 5 100       43 print "$global_result plans failed to be created, examine above output\n"
158             if $global_result;
159 5 100       16 print "$queue_result plans failed to be queued, examine above output\n"
160             if $queue_result;
161 5 100       28 return $global_result ? 2 : 0;
162             }
163              
164             sub _build_plans {
165 0     0   0 my ($planner, $conf, $tests, %options) = @_;
166              
167 0         0 my @plans;
168 0         0 my @pgroups = grep { $_ =~ m/PlatformGroups/ } keys(%$conf);
  0         0  
169              
170             #filter groups by what we actually passed, if we have any
171 0 0 0     0 if (scalar(@{$options{platforms}}) && !$options{allplatforms}) {
  0         0  
172              
173 0         0 foreach my $grp (@pgroups) {
174 0         0 @{$conf->{$grp}} = grep {
175 0         0 my $grp = $_;
176 0         0 grep { $grp eq $_ } @{$options{platforms}};
  0         0  
  0         0  
177 0         0 } @{$conf->{$grp}};
  0         0  
178 0 0       0 delete $conf->{$grp} unless scalar(@{$conf->{$grp}});
  0         0  
179             }
180 0         0 @pgroups = grep { $_ =~ m/PlatformGroups/ } keys(%$conf);
  0         0  
181             }
182              
183 0 0       0 if (scalar(@pgroups)) {
184              
185             #break out the groups depending if we are pairwise or not
186 0 0       0 if ($options{pairwise}) {
187              
188             #Randomize execution order
189 0         0 @$tests = shuffle(@$tests);
190              
191             # The idea here is to have at least one pigeon in each hole.
192             # This is accomplished by finding the longest list of groups, and then iterating over everything we have modulo their size.
193 0         0 my $longest;
194 0         0 foreach my $pgroup (@pgroups) {
195 0   0     0 $longest ||= $pgroup;
196             $longest = $pgroup
197 0 0       0 if scalar(@{$conf->{$pgroup}}) > scalar(@{$conf->{$longest}});
  0         0  
  0         0  
198             }
199              
200 0         0 my @last_tests_apportioned;
201 0         0 for (my $i = 0; $i < scalar(@{$conf->{$longest}}); $i++) {
  0         0  
202 0         0 my %cloned = %options;
203 0         0 my @newplats;
204 0         0 foreach my $pgroup (@pgroups) {
205 0         0 my $idx = $i % scalar(@{$conf->{$pgroup}});
  0         0  
206 0         0 push(@newplats, $conf->{$pgroup}->[$idx]);
207             }
208 0         0 $cloned{platforms} = \@newplats;
209              
210             #Figure out how many tests to dole out to the run
211 0         0 my @tests_apportioned;
212             my $tests_picked =
213 0         0 int(scalar(@$tests) / scalar(@{$conf->{$longest}}));
  0         0  
214 0         0 for (0 .. $tests_picked) {
215 0         0 my $picked = shift @$tests;
216 0 0       0 push(@tests_apportioned, $picked) if $picked;
217             }
218              
219             #Handle the corner case where we are passed less tests than we have platforms
220             @tests_apportioned = @last_tests_apportioned
221 0 0       0 if !scalar(@tests_apportioned);
222 0         0 @last_tests_apportioned = @tests_apportioned;
223              
224 0         0 push(
225             @plans,
226             _build_plan($planner, \@tests_apportioned, %cloned)
227             );
228             }
229             } else {
230              
231             #construct iterator
232 0         0 my @pigeonholes = map { $conf->{$_} } @pgroups;
  0         0  
233              
234 0         0 my @iterator = @{$pigeonholes[0]};
  0         0  
235 0         0 while (scalar(@iterator)) {
236 0         0 my $subj = shift @iterator;
237              
238             #Handle initial elements
239 0 0       0 $subj = [$subj] if ref $subj ne 'ARRAY';
240              
241             #Break out of the loop if we have no more possibilities to exploit
242 0 0       0 if (scalar(@$subj) == scalar(@pigeonholes)) {
243 0         0 my %cloned = %options;
244 0         0 $cloned{platforms} = $subj;
245 0         0 push(@plans, _build_plan($planner, $tests, %cloned));
246 0         0 next;
247             }
248              
249             #Keep pushing partials on to the end of the iterator, until we run out of categories to add
250 0         0 foreach my $element (@{$pigeonholes[ scalar(@$subj) ]}) {
  0         0  
251 0         0 my @partial = @$subj;
252 0         0 push(@partial, $element);
253 0         0 push(@iterator, \@partial);
254             }
255             }
256              
257             }
258             } else {
259 0         0 push(@plans, _build_plan($planner, $tests, %options));
260             }
261              
262             #TODO inject creator & created time into plans
263             @plans =
264 0         0 map { $_->{created} = strftime("%Y-%m-%d %H:%M:%S", localtime()); $_ }
  0         0  
  0         0  
265             @plans;
266              
267 0         0 return @plans;
268             }
269              
270             sub _build_plan {
271 2     2   2184 my ($planner, $tests, %options) = @_;
272 2         5 $options{tests} = $tests;
273              
274             #First, see if we already have a plan like this.
275 2         3 my $existing = &{\&{$planner . "::get_plan"}}(%options);
  2         2  
  2         10  
276              
277             #If not, make the plan. Otherwise, construct the update statements needed to 'make it so'.
278 2 100       11 if (!$existing) {
279 1         2 $existing = &{\&{$planner . "::make_plan"}}(%options);
  1         2  
  1         4  
280             } else {
281 1         2 $existing = &{\&{$planner . "::make_plan_update"}}($existing, %options);
  1         2  
  1         4  
282             }
283              
284 2         12 return $existing;
285             }
286              
287             sub _print_plan {
288 7     7   14266 my ($plan, $force) = @_;
289 7 100 100     30 if (!$plan->{noop} || $force) {
290 6 50       118 print "Name: $plan->{name}\n" if $plan->{name};
291 6         54 print "SUT version: $plan->{version}\n";
292 6         11 print "Platforms: " . join(', ', @{$plan->{platforms}}) . "\n";
  6         46  
293             print "Pairwise? "
294 6 50       52 . ($plan->{pairwise} ne 'false' ? 'yes' : 'no') . "\n";
295 6         43 print "Created at $plan->{created}\n";
296 6         38 print "=========================\n";
297 6 100       19 if ($plan->{state}) {
298 4         5 foreach my $t (@{$plan->{state}}) {
  4         10  
299 8 50 66     23 if ($plan->{replay} && $t->{body}) {
300             next
301 2         6 if (scalar(@{$plan->{replay}})
302 2 50 33     3 && !grep { $_ eq $t->{name} } @{$plan->{replay}});
  4         11  
  2         4  
303 2         34 print "\n$t->{name}..\n";
304             print "Test Version: $t->{test_version}\n"
305 2 50       19 if $t->{test_version};
306 2         13 print "=========================\n";
307 2         15 print "$t->{body}";
308             }
309              
310 8         13 my $pln = '';
311 8 100 66     26 if ( ($t->{status} ne 'UNTESTED')
312             && (ref($t->{steps}) eq 'ARRAY')) {
313 4         6 my $executed = scalar(@{$t->{steps}});
  4         6  
314 4         7 my $planned = $t->{steps_planned};
315 4         7 $pln = "$executed/$planned ";
316             }
317 8         92 printf "%-60s %-10s %s\n", $t->{name}, $pln, $t->{status};
318             }
319             } else {
320 2         3 foreach my $t (@{$plan->{tests}}) {
  2         5  
321 4         28 print "$t\n";
322             }
323 2 50       10 if ($plan->{update}) {
324 2 50       6 if (ref $plan->{update}->{subtraction}->{tests} eq 'ARRAY') {
325 2         14 print "\nRemove the following from the plan:\n";
326 2         12 print "=========================\n";
327 2         5 foreach my $t (@{$plan->{update}->{subtraction}->{tests}}) {
  2         5  
328 2         13 print "$t\n";
329             }
330             }
331 2 50       9 if (ref $plan->{update}->{addition}->{tests} eq 'ARRAY') {
332 2         13 print "\nAdd the following to the plan:\n";
333 2         13 print "=========================\n";
334 2         4 foreach my $t (@{$plan->{update}->{addition}->{tests}}) {
  2         5  
335 2         24 print "$t\n";
336             }
337             }
338             }
339             }
340             } else {
341 1         36 print "Plan already exists, and no updates will be made.\n";
342             }
343             }
344              
345             1;
346              
347             __END__