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.051';
6 10     10   76 use strict;
  10         23  
  10         333  
7 10     10   59 use warnings;
  10         22  
  10         320  
8              
9 10     10   61 use Carp qw{confess cluck};
  10         17  
  10         573  
10 10     10   66 use Scalar::Util qw{blessed};
  10         36  
  10         473  
11 10     10   60 use List::Util qw{any first};
  10         29  
  10         761  
12 10     10   5319 use List::MoreUtils qw{uniq};
  10         87676  
  10         139  
13              
14 10     10   10712 use File::Find;
  10         24  
  10         613  
15 10     10   61 use Cwd qw{abs_path};
  10         24  
  10         428  
16 10     10   65 use File::Basename qw{basename};
  10         20  
  10         623  
17              
18 10     10   4969 use Hash::Merge qw{merge};
  10         54830  
  10         594  
19 10     10   5683 use MCE::Loop;
  10         460663  
  10         79  
20              
21 10     10   3988 use TestRail::Utils;
  10         26  
  10         33389  
22              
23             sub findRuns {
24 13     13 1 17114 my ( $opts, $tr ) = @_;
25 13 50       80 confess("TestRail handle must be provided as argument 2")
26             unless blessed($tr) eq 'TestRail::API';
27              
28 13         29 my ($status_labels);
29              
30             #Process statuses
31 13 100       43 if ( $opts->{'statuses'} ) {
32 3         7 @$status_labels = $tr->statusNamesToLabels( @{ $opts->{'statuses'} } );
  3         17  
33             }
34              
35 13         107 my $project = $tr->getProjectByName( $opts->{'project'} );
36 13 50       36 confess("No such project '$opts->{project}'.\n") if !$project;
37              
38 13         28 my $pconfigs = [];
39             @$pconfigs =
40 3         18 $tr->translateConfigNamesToIds( $project->{'id'}, @{ $opts->{configs} } )
41 13 100       37 if $opts->{'configs'};
42              
43 13         48 my ( $runs, $plans, $planRuns, $cruns, $found ) = ( [], [], [], [], 0 );
44             $runs = $tr->getRuns( $project->{'id'} )
45 13 100       68 if ( !$opts->{'configs'} )
46             ; # If configs are passed, global runs are not in consideration.
47 13         60 $plans = $tr->getPlans( $project->{'id'} );
48 13         33 @$plans = map { $tr->getPlanByID( $_->{'id'} ) } @$plans;
  65         4274  
49 13         1702 foreach my $plan (@$plans) {
50 65         185 $cruns = $tr->getChildRuns($plan);
51 65 50       132 next if !$cruns;
52 65         108 foreach my $run (@$cruns) {
53 143 100       202 next if scalar(@$pconfigs) != scalar( @{ $run->{'config_ids'} } );
  143         300  
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       16 $found++ if grep { $_ == $cid } @$pconfigs;
  11         30  
59             }
60 11         21 $run->{'created_on'} = $plan->{'created_on'};
61 11         14 $run->{'milestone_id'} = $plan->{'milestone_id'};
62             push( @$planRuns, $run )
63 11 100       15 if $found == scalar( @{ $run->{'config_ids'} } );
  11         25  
64             }
65             }
66              
67 13         24 push( @$runs, @$planRuns );
68              
69 13 100       41 if ( $opts->{'statuses'} ) {
70 3         18 @$runs = $tr->getRunSummary(@$runs);
71 3         9 @$runs = grep { defined( $_->{'run_status'} ) }
  15         32  
72             @$runs; #Filter stuff with no results
73 3         10 foreach my $status (@$status_labels) {
74 3         8 @$runs = grep { $_->{'run_status'}->{$status} }
  12         34  
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         28 my $sortkey = 'created_on';
81 13 100       36 if ( $opts->{'milesort'} ) {
82             @$runs = map {
83 3         7 my $run = $_;
  15         26  
84             $run->{'milestone'} =
85             $tr->getMilestoneByID( $run->{'milestone_id'} )
86 15 100       44 if $run->{'milestone_id'};
87             my $milestone =
88 15 100       152 $run->{'milestone'} ? $run->{'milestone'}->{'due_on'} : 0;
89 15         23 $run->{'due_on'} = $milestone;
90 15         34 $run
91             } @$runs;
92 3         8 $sortkey = 'due_on';
93             }
94              
95             #Suppress 'no such option' warnings
96 13   100     31 @$runs = map { $_->{$sortkey} //= ''; $_ } @$runs;
  40         88  
  40         66  
97 13 100       35 if ( $opts->{'lifo'} ) {
98 3         16 @$runs = sort { $b->{$sortkey} cmp $a->{$sortkey} } @$runs;
  22         49  
99             }
100             else {
101 10         38 @$runs = sort { $a->{$sortkey} cmp $b->{$sortkey} } @$runs;
  38         84  
102             }
103              
104 13         432 return $runs;
105             }
106              
107             sub getTests {
108 27     27 1 12533 my ( $opts, $tr ) = @_;
109 27 50       158 confess("TestRail handle must be provided as argument 2")
110             unless blessed($tr) eq 'TestRail::API';
111              
112 27         97 my ( undef, undef, $run ) =
113             TestRail::Utils::getRunInformation( $tr, $opts );
114 25         133 my ( $status_ids, $user_ids );
115              
116             #Process statuses
117 4         23 @$status_ids = $tr->statusNamesToIds( @{ $opts->{'statuses'} } )
118 25 100       106 if $opts->{'statuses'};
119              
120             #Process assignedto ids
121 4         19 @$user_ids = $tr->userNamesToIds( @{ $opts->{'users'} } )
122 25 100       90 if $opts->{'users'};
123              
124 25         99 my $cases = $tr->getTests( $run->{'id'}, $status_ids, $user_ids );
125 25         139 return ( $cases, $run );
126             }
127              
128             sub findTests {
129 46     46 1 3100 my ( $opts, @cases ) = @_;
130              
131             confess "Error! match and no-match options are mutually exclusive.\n"
132 46 100 100     378 if ( $opts->{'match'} && $opts->{'no-match'} );
133             confess "Error! match and orphans options are mutually exclusive.\n"
134 45 100 100     382 if ( $opts->{'match'} && $opts->{'orphans'} );
135             confess "Error! no-match and orphans options are mutually exclusive.\n"
136 44 100 100     328 if ( $opts->{'orphans'} && $opts->{'no-match'} );
137 43         91 my @tests = @cases;
138 43         61 my (@realtests);
139 43   100     148 my $ext = $opts->{'extension'} // '';
140              
141 43 100 100     185 if ( $opts->{'match'} || $opts->{'no-match'} || $opts->{'orphans'} ) {
      100        
142 32         60 my @tmpArr = ();
143             my $dir =
144             ( $opts->{'match'} || $opts->{'orphans'} )
145             ? ( $opts->{'match'} || $opts->{'orphans'} )
146 32 100 100     141 : $opts->{'no-match'};
      66        
147 32 50       622 confess "No such directory '$dir'" if !-d $dir;
148              
149 32 100       178 if ( ref( $opts->{finder} ) eq 'CODE' ) {
150 1         6 @realtests = $opts->{finder}->( $dir, $ext );
151             }
152             else {
153 31 100       79 if ( !$opts->{'no-recurse'} ) {
154             File::Find::find(
155             sub {
156 1449 100 100 1449   47487 push( @realtests, $File::Find::name )
157             if -f && m/\Q$ext\E$/;
158             },
159 23         1750 $dir
160             );
161             }
162             else {
163 8         3543 @realtests = glob("$dir/*$ext");
164             }
165             }
166 31         211 foreach my $case (@cases) {
167 105         181 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       5432 next unless index( $path, $case->{'title'} ) > 0;
171 59 100       1797 next unless basename($path) eq $case->{title};
172 57         195 $case->{'path'} = $path;
173 57         104 push( @tmpArr, $case );
174 57         113 last;
175             }
176             }
177             @tmpArr = grep {
178 11         21 my $otest = $_;
179 11         17 !( grep { $otest->{'title'} eq $_->{'title'} } @tmpArr )
  21         53  
180 31 100       91 } @tests if $opts->{'orphans'};
181 31         71 @tests = @tmpArr;
182 154         626 @tests = map { { 'title' => $_ } } grep {
183 172         3007 my $otest = basename($_);
184 172         335 scalar( grep { basename( $_->{'title'} ) eq $otest } @tests ) == 0
  444         6382  
185 31 100       96 } @realtests if $opts->{'no-match'}; #invert the list in this case.
186             }
187              
188 21         712 @tests = map { abs_path( $_->{'path'} ) } @tests
189 42 100 100     218 if $opts->{'match'} && $opts->{'names-only'};
190 12         457 @tests = map { $_->{'full_title'} = abs_path( $_->{'path'} ); $_ } @tests
  12         51  
191 42 100 100     170 if $opts->{'match'} && !$opts->{'names-only'};
192 158         281 @tests = map { $_->{'title'} } @tests
193 42 100 100     172 if !$opts->{'match'} && $opts->{'names-only'};
194              
195 42         271 return @tests;
196             }
197              
198             sub getCases {
199 4     4 1 714 my ( $opts, $tr ) = @_;
200 4 50       25 confess("First argument must be instance of TestRail::API")
201             unless blessed($tr) eq 'TestRail::API';
202              
203 4         20 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         22 $tr->getTestSuiteByName( $project->{'id'}, $opts->{'testsuite'} );
208 4 50       12 confess "No such testsuite '$opts->{testsuite}'.\n" if !$suite;
209 4         10 $opts->{'testsuite_id'} = $suite->{'id'};
210              
211 4         10 my $section;
212             $section =
213             $tr->getSectionByName( $project->{'id'}, $suite->{'id'},
214             $opts->{'section'} )
215 4 50       12 if $opts->{'section'};
216             confess "No such section '$opts->{section}.\n"
217 4 50 33     13 if $opts->{'section'} && !$section;
218              
219 4         7 my $section_id;
220 4 50       11 $section_id = $section->{'id'} if ref $section eq "HASH";
221              
222 4         7 my $type_ids;
223 0         0 @$type_ids = $tr->typeNamesToIds( @{ $opts->{'types'} } )
224 4 50       11 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         27 my $filters = {
230             'section_id' => $section_id,
231             'type_id' => $type_ids
232             };
233              
234 4         21 return $tr->getCases( $project->{'id'}, $suite->{'id'}, $filters );
235             }
236              
237             sub findCases {
238 11     11 1 12180 my ( $opts, @cases ) = @_;
239              
240             confess('testsuite_id parameter mandatory in options HASHREF')
241 11 100       249 unless defined $opts->{'testsuite_id'};
242             confess('Directory parameter mandatory in options HASHREF.')
243 10 100       216 unless defined $opts->{'directory'};
244             confess( 'No such directory "' . $opts->{'directory'} . "\"\n" )
245 9 100       376 unless -d $opts->{'directory'};
246              
247 8         43 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         14 my @missing = findTests( $mopts, @cases );
255 3         11 $ret->{'missing'} = \@missing;
256             }
257 8 100       23 if ( $opts->{'orphans'} ) {
258             my $oopts = {
259             'orphans' => $opts->{'directory'},
260 3         12 'extension' => $opts->{'extension'}
261             };
262 3         12 my @orphans = findTests( $oopts, @cases );
263 3         12 $ret->{'orphans'} = \@orphans;
264             }
265 8 100       20 if ( $opts->{'update'} ) {
266             my $uopts = {
267             'match' => $opts->{'directory'},
268 3         13 'extension' => $opts->{'extension'}
269             };
270 3         21 my @updates = findTests( $uopts, @cases );
271 3         13 $ret->{'update'} = \@updates;
272             }
273 8         31 return $ret;
274             }
275              
276             sub getResults {
277 13     13 1 56 my ( $tr, $opts, @cases ) = @_;
278 13         34 my $res = {};
279 13         342 my $projects = $tr->getProjects();
280              
281 13         48 my ( @seenRunIds, @seenPlanIds );
282              
283 13         0 my @results;
284              
285             #TODO obey status filtering
286             #TODO obey result notes text grepping
287 13         42 foreach my $project (@$projects) {
288             next
289             if $opts->{projects}
290 39 100 100     1737612 && !( grep { $_ eq $project->{'name'} } @{ $opts->{'projects'} } );
  3         47  
  3         14  
291 37         1059 my $runs = $tr->getRuns( $project->{'id'} );
292              
293             #XXX No runs, or temporary error to ignore
294 37 50       246 next unless ref($runs) eq 'ARRAY';
295              
296 37         198 push( @seenRunIds, map { $_->{id} } @$runs );
  3101         6084  
297              
298             #Translate plan names to ids
299 37   50     869 my $plans = $tr->getPlans( $project->{'id'} ) || [];
300 37         173 push( @seenPlanIds, map { $_->{id} } @$plans );
  3089         5584  
301              
302             #Filter out plans which do not match our filters to prevent a call to getPlanByID
303 37 100       470 if ( $opts->{'plans'} ) {
304             @$plans = grep {
305 3         43 my $p = $_;
  257         350  
306 257     257   525 any { $p->{'name'} eq $_ } @{ $opts->{'plans'} }
  257         1689  
  257         475  
307             } @$plans;
308             }
309              
310             #Filter out runs which do not match our filters
311 37 100       226 if ( $opts->{'runs'} ) {
312             @$runs = grep {
313 25         143 my $r = $_;
  324         475  
314 324     258   1306 any { $r->{'name'} eq $_ } @{ $opts->{'runs'} }
  258         1150  
  324         1323  
315             } @$runs;
316             }
317              
318             #Filter out prior plans
319 37 100       183 if ( $opts->{'plan_ids'} ) {
320             @$plans = grep {
321 6         63 my $p = $_;
  514         770  
322 514     65794   1128 !any { $p->{'id'} eq $_ } @{ $opts->{'plan_ids'} }
  65794         85189  
  514         1892  
323             } @$plans;
324             }
325              
326             #Filter out prior runs
327 37 100       249 if ( $opts->{'run_ids'} ) {
328             @$runs = grep {
329 6         63 my $r = $_;
  504         763  
330 504     63756   1049 !any { $r->{'id'} eq $_ } @{ $opts->{'run_ids'} }
  63756         82260  
  504         1901  
331             } @$runs;
332             }
333              
334 37   100     423 $opts->{'runs'} //= [];
335 37         205 foreach my $plan (@$plans) {
336 2319         5854 $plan = $tr->getPlanByID( $plan->{'id'} );
337 2319         24364 my $plan_runs = $tr->getChildRuns($plan);
338 2319 50       6010 push( @$runs, @$plan_runs ) if $plan_runs;
339             }
340              
341 37         521 my $configs = $tr->getConfigurations( $project->{id} );
342 37         97 my %config_map;
343 136         721 @config_map{ map { $_->{'id'} } @$configs } =
344 37         130 map { $_->{'name'} } @$configs;
  136         315  
345              
346 37         1645 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         2692 $runs
418             );
419              
420             }
421              
422 13         456955 foreach my $result (@results) {
423 3258         23166296 $res = merge( $res, $result );
424             }
425              
426 13         263986 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__