File Coverage

blib/lib/TestRail/Utils/Find.pm
Criterion Covered Total %
statement 234 310 75.4
branch 92 132 69.7
condition 43 60 71.6
subroutine 23 27 85.1
pod 6 6 100.0
total 398 535 74.3


line stmt bran cond sub pod time code
1             # PODNAME: TestRail::Utils::Find
2             # ABSTRACT: Find runs and tests according to user specifications.
3              
4             package TestRail::Utils::Find;
5             $TestRail::Utils::Find::VERSION = '0.050';
6 10     10   78 use strict;
  10         21  
  10         354  
7 10     10   61 use warnings;
  10         21  
  10         369  
8              
9 10     10   61 use Carp qw{confess cluck};
  10         29  
  10         723  
10 10     10   70 use Scalar::Util qw{blessed};
  10         22  
  10         468  
11 10     10   63 use List::Util qw{any first};
  10         39  
  10         754  
12 10     10   5438 use List::MoreUtils qw{uniq};
  10         89777  
  10         104  
13              
14 10     10   11573 use File::Find;
  10         23  
  10         704  
15 10     10   66 use Cwd qw{abs_path};
  10         22  
  10         456  
16 10     10   62 use File::Basename qw{basename};
  10         20  
  10         608  
17              
18 10     10   5261 use Hash::Merge qw{merge};
  10         55620  
  10         705  
19 10     10   5697 use MCE::Loop;
  10         474107  
  10         94  
20              
21 10     10   4613 use TestRail::Utils;
  10         32  
  10         33698  
22              
23             sub findRuns {
24 13     13 1 20725 my ( $opts, $tr ) = @_;
25 13 50       115 confess("TestRail handle must be provided as argument 2")
26             unless blessed($tr) eq 'TestRail::API';
27              
28 13         35 my ($status_labels);
29              
30             #Process statuses
31 13 100       67 if ( $opts->{'statuses'} ) {
32 3         8 @$status_labels = $tr->statusNamesToLabels( @{ $opts->{'statuses'} } );
  3         23  
33             }
34              
35 13         125 my $project = $tr->getProjectByName( $opts->{'project'} );
36 13 50       48 confess("No such project '$opts->{project}'.\n") if !$project;
37              
38 13         37 my $pconfigs = [];
39             @$pconfigs =
40 3         24 $tr->translateConfigNamesToIds( $project->{'id'}, @{ $opts->{configs} } )
41 13 100       51 if $opts->{'configs'};
42              
43 13         50 my ( $runs, $plans, $planRuns, $cruns, $found ) = ( [], [], [], [], 0 );
44             $runs = $tr->getRuns( $project->{'id'} )
45 13 100       92 if ( !$opts->{'configs'} )
46             ; # If configs are passed, global runs are not in consideration.
47 13         89 $plans = $tr->getPlans( $project->{'id'} );
48 13         48 @$plans = map { $tr->getPlanByID( $_->{'id'} ) } @$plans;
  65         5069  
49 13         2155 foreach my $plan (@$plans) {
50 65         194 $cruns = $tr->getChildRuns($plan);
51 65 50       143 next if !$cruns;
52 65         120 foreach my $run (@$cruns) {
53 143 100       205 next if scalar(@$pconfigs) != scalar( @{ $run->{'config_ids'} } );
  143         338  
54              
55             #Compare run config IDs against desired, invalidate run if all conditions not satisfied
56 11         16 $found = 0;
57 11         14 foreach my $cid ( @{ $run->{'config_ids'} } ) {
  11         20  
58 11 100       19 $found++ if grep { $_ == $cid } @$pconfigs;
  11         87  
59             }
60 11         25 $run->{'created_on'} = $plan->{'created_on'};
61 11         20 $run->{'milestone_id'} = $plan->{'milestone_id'};
62             push( @$planRuns, $run )
63 11 100       14 if $found == scalar( @{ $run->{'config_ids'} } );
  11         28  
64             }
65             }
66              
67 13         35 push( @$runs, @$planRuns );
68              
69 13 100       57 if ( $opts->{'statuses'} ) {
70 3         20 @$runs = $tr->getRunSummary(@$runs);
71 3         10 @$runs = grep { defined( $_->{'run_status'} ) }
  15         37  
72             @$runs; #Filter stuff with no results
73 3         12 foreach my $status (@$status_labels) {
74 3         8 @$runs = grep { $_->{'run_status'}->{$status} }
  12         39  
75             @$runs; #If it's positive, keep it. Otherwise forget it.
76             }
77             }
78              
79             #Sort FIFO/LIFO by milestone or creation date of run
80 13         33 my $sortkey = 'created_on';
81 13 100       53 if ( $opts->{'milesort'} ) {
82             @$runs = map {
83 3         12 my $run = $_;
  15         28  
84             $run->{'milestone'} =
85             $tr->getMilestoneByID( $run->{'milestone_id'} )
86 15 100       56 if $run->{'milestone_id'};
87             my $milestone =
88 15 100       189 $run->{'milestone'} ? $run->{'milestone'}->{'due_on'} : 0;
89 15         29 $run->{'due_on'} = $milestone;
90 15         39 $run
91             } @$runs;
92 3         11 $sortkey = 'due_on';
93             }
94              
95             #Suppress 'no such option' warnings
96 13   100     40 @$runs = map { $_->{$sortkey} //= ''; $_ } @$runs;
  40         91  
  40         77  
97 13 100       54 if ( $opts->{'lifo'} ) {
98 3         25 @$runs = sort { $b->{$sortkey} cmp $a->{$sortkey} } @$runs;
  22         56  
99             }
100             else {
101 10         52 @$runs = sort { $a->{$sortkey} cmp $b->{$sortkey} } @$runs;
  38         120  
102             }
103              
104 13         638 return $runs;
105             }
106              
107             sub getTests {
108 27     27 1 13379 my ( $opts, $tr ) = @_;
109 27 50       164 confess("TestRail handle must be provided as argument 2")
110             unless blessed($tr) eq 'TestRail::API';
111              
112 27         103 my ( undef, undef, $run ) =
113             TestRail::Utils::getRunInformation( $tr, $opts );
114 25         138 my ( $status_ids, $user_ids );
115              
116             #Process statuses
117 4         18 @$status_ids = $tr->statusNamesToIds( @{ $opts->{'statuses'} } )
118 25 100       100 if $opts->{'statuses'};
119              
120             #Process assignedto ids
121 4         20 @$user_ids = $tr->userNamesToIds( @{ $opts->{'users'} } )
122 25 100       85 if $opts->{'users'};
123              
124 25         130 my $cases = $tr->getTests( $run->{'id'}, $status_ids, $user_ids );
125 25         156 return ( $cases, $run );
126             }
127              
128             sub findTests {
129 46     46 1 3531 my ( $opts, @cases ) = @_;
130              
131             confess "Error! match and no-match options are mutually exclusive.\n"
132 46 100 100     393 if ( $opts->{'match'} && $opts->{'no-match'} );
133             confess "Error! match and orphans options are mutually exclusive.\n"
134 45 100 100     352 if ( $opts->{'match'} && $opts->{'orphans'} );
135             confess "Error! no-match and orphans options are mutually exclusive.\n"
136 44 100 100     354 if ( $opts->{'orphans'} && $opts->{'no-match'} );
137 43         108 my @tests = @cases;
138 43         74 my (@realtests);
139 43   100     161 my $ext = $opts->{'extension'} // '';
140              
141 43 100 100     202 if ( $opts->{'match'} || $opts->{'no-match'} || $opts->{'orphans'} ) {
      100        
142 32         55 my @tmpArr = ();
143             my $dir =
144             ( $opts->{'match'} || $opts->{'orphans'} )
145             ? ( $opts->{'match'} || $opts->{'orphans'} )
146 32 100 100     154 : $opts->{'no-match'};
      66        
147 32 50       679 confess "No such directory '$dir'" if !-d $dir;
148              
149 32 100       156 if ( ref( $opts->{finder} ) eq 'CODE' ) {
150 1         7 @realtests = $opts->{finder}->( $dir, $ext );
151             }
152             else {
153 31 100       85 if ( !$opts->{'no-recurse'} ) {
154             File::Find::find(
155             sub {
156 1449 100 100 1449   48256 push( @realtests, $File::Find::name )
157             if -f && m/\Q$ext\E$/;
158             },
159 23         1810 $dir
160             );
161             }
162             else {
163 8         3620 @realtests = glob("$dir/*$ext");
164             }
165             }
166 31         221 foreach my $case (@cases) {
167 105         176 foreach my $path (@realtests) {
168              
169             #Filter obviously bogus stuff first to not incur basename() cost except for when we're right, or have a name that contains this name
170 2917 100       5449 next unless index( $path, $case->{'title'} ) > 0;
171 59 100       1816 next unless basename($path) eq $case->{title};
172 57         186 $case->{'path'} = $path;
173 57         96 push( @tmpArr, $case );
174 57         115 last;
175             }
176             }
177             @tmpArr = grep {
178 11         21 my $otest = $_;
179 11         17 !( grep { $otest->{'title'} eq $_->{'title'} } @tmpArr )
  21         56  
180 31 100       87 } @tests if $opts->{'orphans'};
181 31         79 @tests = @tmpArr;
182 154         294 @tests = map { { 'title' => $_ } } grep {
183 172         3007 my $otest = basename($_);
184 172         378 scalar( grep { basename( $_->{'title'} ) eq $otest } @tests ) == 0
  444         6372  
185 31 100       98 } @realtests if $opts->{'no-match'}; #invert the list in this case.
186             }
187              
188 21         745 @tests = map { abs_path( $_->{'path'} ) } @tests
189 42 100 100     179 if $opts->{'match'} && $opts->{'names-only'};
190 12         520 @tests = map { $_->{'full_title'} = abs_path( $_->{'path'} ); $_ } @tests
  12         92  
191 42 100 100     189 if $opts->{'match'} && !$opts->{'names-only'};
192 158         270 @tests = map { $_->{'title'} } @tests
193 42 100 100     170 if !$opts->{'match'} && $opts->{'names-only'};
194              
195 42         268 return @tests;
196             }
197              
198             sub getCases {
199 4     4 1 753 my ( $opts, $tr ) = @_;
200 4 50       28 confess("First argument must be instance of TestRail::API")
201             unless blessed($tr) eq 'TestRail::API';
202              
203 4         21 my $project = $tr->getProjectByName( $opts->{'project'} );
204 4 50       13 confess "No such project '$opts->{project}'.\n" if !$project;
205              
206             my $suite =
207 4         36 $tr->getTestSuiteByName( $project->{'id'}, $opts->{'testsuite'} );
208 4 50       13 confess "No such testsuite '$opts->{testsuite}'.\n" if !$suite;
209 4         11 $opts->{'testsuite_id'} = $suite->{'id'};
210              
211 4         22 my $section;
212             $section =
213             $tr->getSectionByName( $project->{'id'}, $suite->{'id'},
214             $opts->{'section'} )
215 4 50       15 if $opts->{'section'};
216             confess "No such section '$opts->{section}.\n"
217 4 50 33     15 if $opts->{'section'} && !$section;
218              
219 4         8 my $section_id;
220 4 50       12 $section_id = $section->{'id'} if ref $section eq "HASH";
221              
222 4         8 my $type_ids;
223 0         0 @$type_ids = $tr->typeNamesToIds( @{ $opts->{'types'} } )
224 4 50       13 if ref $opts->{'types'} eq 'ARRAY';
225              
226             #Above will confess if anything's the matter
227              
228             #TODO Translate opts into filters
229 4         15 my $filters = {
230             'section_id' => $section_id,
231             'type_id' => $type_ids
232             };
233              
234 4         24 return $tr->getCases( $project->{'id'}, $suite->{'id'}, $filters );
235             }
236              
237             sub findCases {
238 11     11 1 12688 my ( $opts, @cases ) = @_;
239              
240             confess('testsuite_id parameter mandatory in options HASHREF')
241 11 100       324 unless defined $opts->{'testsuite_id'};
242             confess('Directory parameter mandatory in options HASHREF.')
243 10 100       238 unless defined $opts->{'directory'};
244             confess( 'No such directory "' . $opts->{'directory'} . "\"\n" )
245 9 100       395 unless -d $opts->{'directory'};
246              
247 8         77 my $ret = { 'testsuite_id' => $opts->{'testsuite_id'} };
248 8 100       24 if ( !$opts->{'no-missing'} ) {
249             my $mopts = {
250             'no-match' => $opts->{'directory'},
251             'names-only' => 1,
252 3         15 'extension' => $opts->{'extension'}
253             };
254 3         26 my @missing = findTests( $mopts, @cases );
255 3         13 $ret->{'missing'} = \@missing;
256             }
257 8 100       33 if ( $opts->{'orphans'} ) {
258             my $oopts = {
259             'orphans' => $opts->{'directory'},
260 3         15 'extension' => $opts->{'extension'}
261             };
262 3         12 my @orphans = findTests( $oopts, @cases );
263 3         13 $ret->{'orphans'} = \@orphans;
264             }
265 8 100       23 if ( $opts->{'update'} ) {
266             my $uopts = {
267             'match' => $opts->{'directory'},
268 3         15 'extension' => $opts->{'extension'}
269             };
270 3         26 my @updates = findTests( $uopts, @cases );
271 3         14 $ret->{'update'} = \@updates;
272             }
273 8         31 return $ret;
274             }
275              
276             sub getResults {
277 13     13 1 79 my ( $tr, $opts, @cases ) = @_;
278 13         35 my $res = {};
279 13         247 my $projects = $tr->getProjects();
280              
281 13         51 my ( @seenRunIds, @seenPlanIds );
282              
283 13         0 my @results;
284              
285             #TODO obey status filtering
286             #TODO obey result notes text grepping
287 13         43 foreach my $project (@$projects) {
288             next
289             if $opts->{projects}
290 39 100 100     1729880 && !( grep { $_ eq $project->{'name'} } @{ $opts->{'projects'} } );
  3         37  
  3         15  
291 37         1183 my $runs = $tr->getRuns( $project->{'id'} );
292              
293             #XXX No runs, or temporary error to ignore
294 37 50       355 next unless ref($runs) eq 'ARRAY';
295              
296 37         198 push( @seenRunIds, map { $_->{id} } @$runs );
  3101         6149  
297              
298             #Translate plan names to ids
299 37   50     1022 my $plans = $tr->getPlans( $project->{'id'} ) || [];
300 37         142 push( @seenPlanIds, map { $_->{id} } @$plans );
  3089         6098  
301              
302             #Filter out plans which do not match our filters to prevent a call to getPlanByID
303 37 100       474 if ( $opts->{'plans'} ) {
304             @$plans = grep {
305 3         41 my $p = $_;
  257         366  
306 257     257   542 any { $p->{'name'} eq $_ } @{ $opts->{'plans'} }
  257         1909  
  257         490  
307             } @$plans;
308             }
309              
310             #Filter out runs which do not match our filters
311 37 100       206 if ( $opts->{'runs'} ) {
312             @$runs = grep {
313 25         119 my $r = $_;
  324         525  
314 324     258   1411 any { $r->{'name'} eq $_ } @{ $opts->{'runs'} }
  258         1315  
  324         1285  
315             } @$runs;
316             }
317              
318             #Filter out prior plans
319 37 100       275 if ( $opts->{'plan_ids'} ) {
320             @$plans = grep {
321 6         58 my $p = $_;
  514         805  
322 514     65794   1115 !any { $p->{'id'} eq $_ } @{ $opts->{'plan_ids'} }
  65794         85925  
  514         1976  
323             } @$plans;
324             }
325              
326             #Filter out prior runs
327 37 100       225 if ( $opts->{'run_ids'} ) {
328             @$runs = grep {
329 6         50 my $r = $_;
  504         871  
330 504     63756   1085 !any { $r->{'id'} eq $_ } @{ $opts->{'run_ids'} }
  63756         83851  
  504         1982  
331             } @$runs;
332             }
333              
334 37   100     552 $opts->{'runs'} //= [];
335 37         283 foreach my $plan (@$plans) {
336 2319         5744 $plan = $tr->getPlanByID( $plan->{'id'} );
337 2319         22556 my $plan_runs = $tr->getChildRuns($plan);
338 2319 50       5537 push( @$runs, @$plan_runs ) if $plan_runs;
339             }
340              
341 37         462 my $configs = $tr->getConfigurations( $project->{id} );
342 37         114 my %config_map;
343 136         779 @config_map{ map { $_->{'id'} } @$configs } =
344 37         121 map { $_->{'name'} } @$configs;
  136         321  
345              
346 37         948 MCE::Loop::init {
347             max_workers => 'auto',
348             chunk_size => 'auto'
349             };
350              
351             push(
352             @results,
353             mce_loop {
354 0     0   0 my $runz = $_;
355              
356             #XXX it appears as though some versions of MCE do not have uniform passing convention
357 0 0       0 $runz = [$runz] if ref($runz) ne 'ARRAY';
358              
359 0         0 my $res = {};
360 0         0 foreach my $run (@$runz) {
361              
362             #XXX super bad bug in some versions of MCE, apparently causes data loss, or is duping jobs with incomplete info!
363 0 0       0 next if !$run->{id};
364              
365             #Translate config ids to names, also remove any gone configs
366 0         0 my @run_configs = grep { defined $_ }
367 0         0 map { $config_map{$_} } @{ $run->{config_ids} };
  0         0  
  0         0  
368             next
369 0         0 if scalar( @{ $opts->{runs} } )
370 0         0 && !( grep { $_ eq $run->{'name'} }
371 0 0 0     0 @{ $opts->{'runs'} } );
  0         0  
372 0 0       0 if ( $opts->{fast} ) {
373 0         0 my @csz = @cases;
374 0         0 @csz = grep { ref($_) eq 'HASH' } map {
375 0         0 my $cname = basename($_);
  0         0  
376 0         0 my $c = $tr->getTestByName( $run->{id}, $cname );
377 0         0 $c->{config_ids} = \@run_configs;
378 0 0       0 $c->{name} = $cname if $c;
379 0         0 $c
380             } @csz;
381 0 0       0 next unless scalar(@csz);
382              
383 0         0 my $results = $tr->getRunResults( $run->{id} );
384 0         0 foreach my $c (@csz) {
385 0   0     0 $res->{ $c->{name} } //= [];
386             my $cres =
387 0         0 first { $c->{id} == $_->{test_id} } @$results;
  0         0  
388 0 0       0 return unless $cres;
389              
390 0         0 $c->{results} = [$cres];
391 0         0 $c = _filterResults( $opts, $c );
392              
393 0         0 push( @{ $res->{ $c->{name} } }, $c )
394 0 0       0 if scalar( @{ $c->{results} } );
  0         0  
395             }
396 0         0 next;
397             }
398              
399 0         0 foreach my $case (@cases) {
400             my $c =
401 0         0 $tr->getTestByName( $run->{'id'}, basename($case) );
402 0 0       0 next unless ref $c eq 'HASH';
403              
404 0   0     0 $res->{$case} //= [];
405             $c->{results} = $tr->getTestResults( $c->{'id'},
406 0         0 $tr->{'global_limit'}, 0 );
407 0         0 $c->{config_ids} = \@run_configs;
408 0         0 $c = _filterResults( $opts, $c );
409              
410 0         0 push( @{ $res->{$case} }, $c )
411 0 0       0 if scalar( @{ $c->{results} } )
  0         0  
412             ; #Make sure they weren't filtered out
413             }
414             }
415 0         0 return MCE->gather( MCE->chunk_id, $res );
416             }
417 37         2536 $runs
418             );
419              
420             }
421              
422 13         469205 foreach my $result (@results) {
423 3258         20977723 $res = merge( $res, $result );
424             }
425              
426 13         215098 return ( $res, \@seenPlanIds, \@seenRunIds );
427             }
428              
429             sub _filterResults {
430 0     0     my ( $opts, $c ) = @_;
431              
432             #Filter by provided pattern, if any
433 0 0         if ( $opts->{'pattern'} ) {
434 0           my $pattern = $opts->{pattern};
435 0           @{ $c->{results} } =
436 0   0       grep { my $comment = $_->{comment} || ''; $comment =~ m/$pattern/i }
  0            
437 0           @{ $c->{results} };
  0            
438             }
439              
440             #Filter by the provided case IDs, if any
441 0 0 0       if ( ref( $opts->{'defects'} ) eq 'ARRAY'
442 0           && scalar( @{ $opts->{defects} } ) )
443             {
444 0           @{ $c->{results} } = grep {
445 0           my $defects = $_->{defects};
446             any {
447 0     0     my $df_case = $_;
448 0           any { $df_case eq $_ } @{ $opts->{defects} };
  0            
  0            
449             }
450 0           @$defects
451 0           } @{ $c->{results} };
  0            
452             }
453              
454             #Filter by the provided versions, if any
455 0 0 0       if ( ref( $opts->{'versions'} ) eq 'ARRAY'
456 0           && scalar( @{ $opts->{versions} } ) )
457             {
458 0           @{ $c->{results} } = grep {
459 0           my $version = $_->{version};
460 0     0     any { $version eq $_ } @{ $opts->{versions} };
  0            
  0            
461 0           } @{ $c->{results} };
  0            
462             }
463              
464 0           return $c;
465             }
466              
467             1;
468              
469             __END__