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.052';
6 10     10   68 use strict;
  10         19  
  10         298  
7 10     10   50 use warnings;
  10         21  
  10         328  
8              
9 10     10   49 use Carp qw{confess cluck};
  10         18  
  10         529  
10 10     10   60 use Scalar::Util qw{blessed};
  10         30  
  10         423  
11 10     10   70 use List::Util qw{any first};
  10         24  
  10         628  
12 10     10   4444 use List::MoreUtils qw{uniq};
  10         75080  
  10         105  
13              
14 10     10   9475 use File::Find;
  10         21  
  10         580  
15 10     10   55 use Cwd qw{abs_path};
  10         18  
  10         388  
16 10     10   56 use File::Basename qw{basename};
  10         24  
  10         516  
17              
18 10     10   4552 use Hash::Merge qw{merge};
  10         46145  
  10         575  
19 10     10   4889 use MCE::Loop;
  10         389182  
  10         80  
20              
21 10     10   3880 use TestRail::Utils;
  10         27  
  10         28628  
22              
23             sub findRuns {
24 13     13 1 15927 my ( $opts, $tr ) = @_;
25 13 50       94 confess("TestRail handle must be provided as argument 2")
26             unless blessed($tr) eq 'TestRail::API';
27              
28 13         26 my ($status_labels);
29              
30             #Process statuses
31 13 100       42 if ( $opts->{'statuses'} ) {
32 3         7 @$status_labels = $tr->statusNamesToLabels( @{ $opts->{'statuses'} } );
  3         17  
33             }
34              
35 13         109 my $project = $tr->getProjectByName( $opts->{'project'} );
36 13 50       40 confess("No such project '$opts->{project}'.\n") if !$project;
37              
38 13         25 my $pconfigs = [];
39             @$pconfigs =
40 3         17 $tr->translateConfigNamesToIds( $project->{'id'}, @{ $opts->{configs} } )
41 13 100       46 if $opts->{'configs'};
42              
43 13         85 my ( $runs, $plans, $planRuns, $cruns, $found ) = ( [], [], [], [], 0 );
44             $runs = $tr->getRuns( $project->{'id'} )
45 13 100       73 if ( !$opts->{'configs'} )
46             ; # If configs are passed, global runs are not in consideration.
47 13         59 $plans = $tr->getPlans( $project->{'id'} );
48 13         33 @$plans = map { $tr->getPlanByID( $_->{'id'} ) } @$plans;
  65         4294  
49 13         1783 foreach my $plan (@$plans) {
50 65         152 $cruns = $tr->getChildRuns($plan);
51 65 50       124 next if !$cruns;
52 65         96 foreach my $run (@$cruns) {
53 143 100       154 next if scalar(@$pconfigs) != scalar( @{ $run->{'config_ids'} } );
  143         293  
54              
55             #Compare run config IDs against desired, invalidate run if all conditions not satisfied
56 11         12 $found = 0;
57 11         12 foreach my $cid ( @{ $run->{'config_ids'} } ) {
  11         14  
58 11 100       16 $found++ if grep { $_ == $cid } @$pconfigs;
  11         26  
59             }
60 11         19 $run->{'created_on'} = $plan->{'created_on'};
61 11         13 $run->{'milestone_id'} = $plan->{'milestone_id'};
62             push( @$planRuns, $run )
63 11 100       12 if $found == scalar( @{ $run->{'config_ids'} } );
  11         20  
64             }
65             }
66              
67 13         26 push( @$runs, @$planRuns );
68              
69 13 100       42 if ( $opts->{'statuses'} ) {
70 3         16 @$runs = $tr->getRunSummary(@$runs);
71 3         9 @$runs = grep { defined( $_->{'run_status'} ) }
  15         31  
72             @$runs; #Filter stuff with no results
73 3         9 foreach my $status (@$status_labels) {
74 3         8 @$runs = grep { $_->{'run_status'}->{$status} }
  12         32  
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         27 my $sortkey = 'created_on';
81 13 100       35 if ( $opts->{'milesort'} ) {
82             @$runs = map {
83 3         8 my $run = $_;
  15         24  
84             $run->{'milestone'} =
85             $tr->getMilestoneByID( $run->{'milestone_id'} )
86 15 100       45 if $run->{'milestone_id'};
87             my $milestone =
88 15 100       155 $run->{'milestone'} ? $run->{'milestone'}->{'due_on'} : 0;
89 15         23 $run->{'due_on'} = $milestone;
90 15         28 $run
91             } @$runs;
92 3         8 $sortkey = 'due_on';
93             }
94              
95             #Suppress 'no such option' warnings
96 13   100     36 @$runs = map { $_->{$sortkey} //= ''; $_ } @$runs;
  40         79  
  40         62  
97 13 100       40 if ( $opts->{'lifo'} ) {
98 3         18 @$runs = sort { $b->{$sortkey} cmp $a->{$sortkey} } @$runs;
  22         42  
99             }
100             else {
101 10         43 @$runs = sort { $a->{$sortkey} cmp $b->{$sortkey} } @$runs;
  38         75  
102             }
103              
104 13         521 return $runs;
105             }
106              
107             sub getTests {
108 27     27 1 12242 my ( $opts, $tr ) = @_;
109 27 50       202 confess("TestRail handle must be provided as argument 2")
110             unless blessed($tr) eq 'TestRail::API';
111              
112 27         90 my ( undef, undef, $run ) =
113             TestRail::Utils::getRunInformation( $tr, $opts );
114 25         109 my ( $status_ids, $user_ids );
115              
116             #Process statuses
117 4         18 @$status_ids = $tr->statusNamesToIds( @{ $opts->{'statuses'} } )
118 25 100       82 if $opts->{'statuses'};
119              
120             #Process assignedto ids
121 4         17 @$user_ids = $tr->userNamesToIds( @{ $opts->{'users'} } )
122 25 100       72 if $opts->{'users'};
123              
124 25         82 my $cases = $tr->getTests( $run->{'id'}, $status_ids, $user_ids );
125 25         126 return ( $cases, $run );
126             }
127              
128             sub findTests {
129 46     46 1 3024 my ( $opts, @cases ) = @_;
130              
131             confess "Error! match and no-match options are mutually exclusive.\n"
132 46 100 100     308 if ( $opts->{'match'} && $opts->{'no-match'} );
133             confess "Error! match and orphans options are mutually exclusive.\n"
134 45 100 100     312 if ( $opts->{'match'} && $opts->{'orphans'} );
135             confess "Error! no-match and orphans options are mutually exclusive.\n"
136 44 100 100     252 if ( $opts->{'orphans'} && $opts->{'no-match'} );
137 43         74 my @tests = @cases;
138 43         54 my (@realtests);
139 43   100     127 my $ext = $opts->{'extension'} // '';
140              
141 43 100 100     154 if ( $opts->{'match'} || $opts->{'no-match'} || $opts->{'orphans'} ) {
      100        
142 32         45 my @tmpArr = ();
143             my $dir =
144             ( $opts->{'match'} || $opts->{'orphans'} )
145             ? ( $opts->{'match'} || $opts->{'orphans'} )
146 32 100 100     131 : $opts->{'no-match'};
      66        
147 32 50       495 confess "No such directory '$dir'" if !-d $dir;
148              
149 32 100       116 if ( ref( $opts->{finder} ) eq 'CODE' ) {
150 1         6 @realtests = $opts->{finder}->( $dir, $ext );
151             }
152             else {
153 31 100       71 if ( !$opts->{'no-recurse'} ) {
154             File::Find::find(
155             sub {
156 1449 100 100 1449   38828 push( @realtests, $File::Find::name )
157             if -f && m/\Q$ext\E$/;
158             },
159 23         1425 $dir
160             );
161             }
162             else {
163 8         2872 @realtests = glob("$dir/*$ext");
164             }
165             }
166 31         175 foreach my $case (@cases) {
167 105         140 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       4406 next unless index( $path, $case->{'title'} ) > 0;
171 59 100       1581 next unless basename($path) eq $case->{title};
172 57         160 $case->{'path'} = $path;
173 57         86 push( @tmpArr, $case );
174 57         82 last;
175             }
176             }
177             @tmpArr = grep {
178 11         16 my $otest = $_;
179 11         15 !( grep { $otest->{'title'} eq $_->{'title'} } @tmpArr )
  21         44  
180 31 100       85 } @tests if $opts->{'orphans'};
181 31         63 @tests = @tmpArr;
182 154         238 @tests = map { { 'title' => $_ } } grep {
183 172         2435 my $otest = basename($_);
184 172         285 scalar( grep { basename( $_->{'title'} ) eq $otest } @tests ) == 0
  444         5216  
185 31 100       82 } @realtests if $opts->{'no-match'}; #invert the list in this case.
186             }
187              
188 21         579 @tests = map { abs_path( $_->{'path'} ) } @tests
189 42 100 100     142 if $opts->{'match'} && $opts->{'names-only'};
190 12         376 @tests = map { $_->{'full_title'} = abs_path( $_->{'path'} ); $_ } @tests
  12         44  
191 42 100 100     137 if $opts->{'match'} && !$opts->{'names-only'};
192 158         235 @tests = map { $_->{'title'} } @tests
193 42 100 100     128 if !$opts->{'match'} && $opts->{'names-only'};
194              
195 42         222 return @tests;
196             }
197              
198             sub getCases {
199 4     4 1 708 my ( $opts, $tr ) = @_;
200 4 50       22 confess("First argument must be instance of TestRail::API")
201             unless blessed($tr) eq 'TestRail::API';
202              
203 4         16 my $project = $tr->getProjectByName( $opts->{'project'} );
204 4 50       11 confess "No such project '$opts->{project}'.\n" if !$project;
205              
206             my $suite =
207 4         19 $tr->getTestSuiteByName( $project->{'id'}, $opts->{'testsuite'} );
208 4 50       12 confess "No such testsuite '$opts->{testsuite}'.\n" if !$suite;
209 4         8 $opts->{'testsuite_id'} = $suite->{'id'};
210              
211 4         6 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         5 my $section_id;
220 4 50       11 $section_id = $section->{'id'} if ref $section eq "HASH";
221              
222 4         5 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         22 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 12015 my ( $opts, @cases ) = @_;
239              
240             confess('testsuite_id parameter mandatory in options HASHREF')
241 11 100       241 unless defined $opts->{'testsuite_id'};
242             confess('Directory parameter mandatory in options HASHREF.')
243 10 100       174 unless defined $opts->{'directory'};
244             confess( 'No such directory "' . $opts->{'directory'} . "\"\n" )
245 9 100       314 unless -d $opts->{'directory'};
246              
247 8         45 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         14 'extension' => $opts->{'extension'}
253             };
254 3         12 my @missing = findTests( $mopts, @cases );
255 3         12 $ret->{'missing'} = \@missing;
256             }
257 8 100       19 if ( $opts->{'orphans'} ) {
258             my $oopts = {
259             'orphans' => $opts->{'directory'},
260 3         13 'extension' => $opts->{'extension'}
261             };
262 3         13 my @orphans = findTests( $oopts, @cases );
263 3         9 $ret->{'orphans'} = \@orphans;
264             }
265 8 100       21 if ( $opts->{'update'} ) {
266             my $uopts = {
267             'match' => $opts->{'directory'},
268 3         13 'extension' => $opts->{'extension'}
269             };
270 3         11 my @updates = findTests( $uopts, @cases );
271 3         11 $ret->{'update'} = \@updates;
272             }
273 8         24 return $ret;
274             }
275              
276             sub getResults {
277 13     13 1 49 my ( $tr, $opts, @cases ) = @_;
278 13         26 my $res = {};
279 13         65 my $projects = $tr->getProjects();
280              
281 13         49 my ( @seenRunIds, @seenPlanIds );
282              
283 13         0 my @results;
284              
285             #TODO obey status filtering
286             #TODO obey result notes text grepping
287 13         27 foreach my $project (@$projects) {
288             next
289             if $opts->{projects}
290 39 100 100     1563524 && !( grep { $_ eq $project->{'name'} } @{ $opts->{'projects'} } );
  3         30  
  3         13  
291 37         817 my $runs = $tr->getRuns( $project->{'id'} );
292              
293             #XXX No runs, or temporary error to ignore
294 37 50       298 next unless ref($runs) eq 'ARRAY';
295              
296 37         185 push( @seenRunIds, map { $_->{id} } @$runs );
  3101         5187  
297              
298             #Translate plan names to ids
299 37   50     864 my $plans = $tr->getPlans( $project->{'id'} ) || [];
300 37         165 push( @seenPlanIds, map { $_->{id} } @$plans );
  3089         4909  
301              
302             #Filter out plans which do not match our filters to prevent a call to getPlanByID
303 37 100       287 if ( $opts->{'plans'} ) {
304             @$plans = grep {
305 3         27 my $p = $_;
  257         606  
306 257     257   459 any { $p->{'name'} eq $_ } @{ $opts->{'plans'} }
  257         875  
  257         378  
307             } @$plans;
308             }
309              
310             #Filter out runs which do not match our filters
311 37 100       233 if ( $opts->{'runs'} ) {
312             @$runs = grep {
313 25         83 my $r = $_;
  324         451  
314 324     258   1157 any { $r->{'name'} eq $_ } @{ $opts->{'runs'} }
  258         1531  
  324         1136  
315             } @$runs;
316             }
317              
318             #Filter out prior plans
319 37 100       218 if ( $opts->{'plan_ids'} ) {
320             @$plans = grep {
321 6         36 my $p = $_;
  514         665  
322 514     65794   930 !any { $p->{'id'} eq $_ } @{ $opts->{'plan_ids'} }
  65794         68709  
  514         1487  
323             } @$plans;
324             }
325              
326             #Filter out prior runs
327 37 100       199 if ( $opts->{'run_ids'} ) {
328             @$runs = grep {
329 6         76 my $r = $_;
  504         659  
330 504     63756   848 !any { $r->{'id'} eq $_ } @{ $opts->{'run_ids'} }
  63756         66065  
  504         1558  
331             } @$runs;
332             }
333              
334 37   100     332 $opts->{'runs'} //= [];
335 37         166 foreach my $plan (@$plans) {
336 2319         4919 $plan = $tr->getPlanByID( $plan->{'id'} );
337 2319         22608 my $plan_runs = $tr->getChildRuns($plan);
338 2319 50       4787 push( @$runs, @$plan_runs ) if $plan_runs;
339             }
340              
341 37         896 my $configs = $tr->getConfigurations( $project->{id} );
342 37         121 my %config_map;
343 136         623 @config_map{ map { $_->{'id'} } @$configs } =
344 37         109 map { $_->{'name'} } @$configs;
  136         264  
345              
346 37         1125 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         2703 $runs
418             );
419              
420             }
421              
422 13         495067 foreach my $result (@results) {
423 3258         22665308 $res = merge( $res, $result );
424             }
425              
426 13         219161 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 0           } @$defects
450 0           } @{ $c->{results} };
  0            
451             }
452              
453             #Filter by the provided versions, if any
454 0 0 0       if ( ref( $opts->{'versions'} ) eq 'ARRAY'
455 0           && scalar( @{ $opts->{versions} } ) )
456             {
457 0           @{ $c->{results} } = grep {
458 0           my $version = $_->{version};
459 0     0     any { $version eq $_ } @{ $opts->{versions} };
  0            
  0            
460 0           } @{ $c->{results} };
  0            
461             }
462              
463 0           return $c;
464             }
465              
466             1;
467              
468             __END__