Find.pm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. # PODNAME: TestRail::Utils::Find
  2. # ABSTRACT: Find runs and tests according to user specifications.
  3. package TestRail::Utils::Find;
  4. use strict;
  5. use warnings;
  6. use Carp qw{confess cluck};
  7. use Scalar::Util qw{blessed};
  8. use File::Find;
  9. use Cwd qw{abs_path};
  10. use File::Basename qw{basename};
  11. use TestRail::Utils;
  12. =head1 DESCRIPTION
  13. =head1 FUNCTIONS
  14. =head2 findRuns
  15. Find runs based on the options HASHREF provided.
  16. See the documentation for L<testrail-runs>, as the long argument names there correspond to hash keys.
  17. The primary routine of testrail-runs.
  18. =over 4
  19. =item HASHREF C<OPTIONS> - flags acceptable by testrail-tests
  20. =item TestRail::API C<HANDLE> - TestRail::API object
  21. =back
  22. Returns ARRAYREF of run definition HASHREFs.
  23. =cut
  24. sub findRuns {
  25. my ($opts,$tr) = @_;
  26. confess("TestRail handle must be provided as argument 2") unless blessed($tr) eq 'TestRail::API';
  27. my ($status_labels);
  28. #Process statuses
  29. if ($opts->{'statuses'}) {
  30. @$status_labels = $tr->statusNamesToLabels(@{$opts->{'statuses'}});
  31. }
  32. my $project = $tr->getProjectByName($opts->{'project'});
  33. confess("No such project '$opts->{project}'.\n") if !$project;
  34. my $pconfigs = [];
  35. @$pconfigs = $tr->translateConfigNamesToIds($project->{'id'},@{$opts->{configs}}) if $opts->{'configs'};
  36. my ($runs,$plans,$planRuns,$cruns,$found) = ([],[],[],[],0);
  37. $runs = $tr->getRuns($project->{'id'}) if (!$opts->{'configs'}); # If configs are passed, global runs are not in consideration.
  38. $plans = $tr->getPlans($project->{'id'});
  39. @$plans = map {$tr->getPlanByID($_->{'id'})} @$plans;
  40. foreach my $plan (@$plans) {
  41. $cruns = $tr->getChildRuns($plan);
  42. next if !$cruns;
  43. foreach my $run (@$cruns) {
  44. next if scalar(@$pconfigs) != scalar(@{$run->{'config_ids'}});
  45. #Compare run config IDs against desired, invalidate run if all conditions not satisfied
  46. $found = 0;
  47. foreach my $cid (@{$run->{'config_ids'}}) {
  48. $found++ if grep {$_ == $cid} @$pconfigs;
  49. }
  50. $run->{'created_on'} = $plan->{'created_on'};
  51. $run->{'milestone_id'} = $plan->{'milestone_id'};
  52. push(@$planRuns, $run) if $found == scalar(@{$run->{'config_ids'}});
  53. }
  54. }
  55. push(@$runs,@$planRuns);
  56. if ($opts->{'statuses'}) {
  57. @$runs = $tr->getRunSummary(@$runs);
  58. @$runs = grep { defined($_->{'run_status'}) } @$runs; #Filter stuff with no results
  59. foreach my $status (@$status_labels) {
  60. @$runs = grep { $_->{'run_status'}->{$status} } @$runs; #If it's positive, keep it. Otherwise forget it.
  61. }
  62. }
  63. #Sort FIFO/LIFO by milestone or creation date of run
  64. my $sortkey = 'created_on';
  65. if ($opts->{'milesort'}) {
  66. @$runs = map {
  67. my $run = $_;
  68. $run->{'milestone'} = $tr->getMilestoneByID($run->{'milestone_id'}) if $run->{'milestone_id'};
  69. my $milestone = $run->{'milestone'} ? $run->{'milestone'}->{'due_on'} : 0;
  70. $run->{'due_on'} = $milestone;
  71. $run
  72. } @$runs;
  73. $sortkey = 'due_on';
  74. }
  75. #Suppress 'no such option' warnings
  76. @$runs = map { $_->{$sortkey} //= ''; $_ } @$runs;
  77. if ($opts->{'lifo'}) {
  78. @$runs = sort { $b->{$sortkey} cmp $a->{$sortkey} } @$runs;
  79. } else {
  80. @$runs = sort { $a->{$sortkey} cmp $b->{$sortkey} } @$runs;
  81. }
  82. return $runs;
  83. }
  84. =head2 getTests(opts,testrail)
  85. Get the tests specified by the options passed.
  86. =over 4
  87. =item HASHREF C<OPTS> - Options for getting the tests
  88. =over 4
  89. =item STRING C<PROJECT> - name of Project to look for tests in
  90. =item STRING C<RUN> - name of Run to get tests from
  91. =item STRING C<PLAN> (optional) - name of Plan to get run from
  92. =item ARRAYREF[STRING] C<CONFIGS> (optional) - names of configs run must satisfy, if part of a plan
  93. =item ARRAYREF[STRING] C<USERS> (optional) - names of users to filter cases by assignee
  94. =item ARRAYREF[STRING] C<STATUSES> (optional) - names of statuses to filter cases by
  95. =back
  96. =back
  97. Returns ARRAYREF of tests, and the run in which they belong.
  98. =cut
  99. sub getTests {
  100. my ($opts,$tr) = @_;
  101. confess("TestRail handle must be provided as argument 2") unless blessed($tr) eq 'TestRail::API';
  102. my (undef,undef,$run) = TestRail::Utils::getRunInformation($tr,$opts);
  103. my ($status_ids,$user_ids);
  104. #Process statuses
  105. @$status_ids = $tr->statusNamesToIds(@{$opts->{'statuses'}}) if $opts->{'statuses'};
  106. #Process assignedto ids
  107. @$user_ids = $tr->userNamesToIds(@{$opts->{'users'}}) if $opts->{'users'};
  108. my $cases = $tr->getTests($run->{'id'},$status_ids,$user_ids);
  109. return ($cases,$run);
  110. }
  111. =head2 findTests(opts,case1,...,caseN)
  112. Given an ARRAY of tests, find tests meeting your criteria (or not) in the specified directory.
  113. =over 4
  114. =item HASHREF C<OPTS> - Options for finding tests:
  115. =over 4
  116. =item STRING C<MATCH> - Only return tests which exist in the path provided, and in TestRail. Mutually exclusive with no-match, orphans.
  117. =item STRING C<NO-MATCH> - Only return tests which are in the path provided, but not in TestRail. Mutually exclusive with match, orphans.
  118. =item STRING C<ORPHANS> - Only return tests which are in TestRail, and not in the path provided. Mutually exclusive with match, no-match
  119. =item BOOL C<NO-RECURSE> - Do not do a recursive scan for files.
  120. =item BOOL C<NAMES-ONLY> - Only return the names of the tests rather than the entire test objects.
  121. =item STRING C<EXTENSION> (optional) - Only return files ending with the provided text (e.g. .t, .test, .pl, .pm)
  122. =back
  123. =item ARRAY C<CASES> - Array of cases to translate to pathnames based on above options.
  124. =back
  125. Returns tests found that meet the criteria laid out in the options.
  126. Provides absolute path to tests if match is passed; this is the 'full_title' key if names-only is false/undef.
  127. Dies if mutually exclusive options are passed.
  128. =cut
  129. sub findTests {
  130. my ($opts,@cases) = @_;
  131. confess "Error! match and no-match options are mutually exclusive.\n" if ($opts->{'match'} && $opts->{'no-match'});
  132. confess "Error! match and orphans options are mutually exclusive.\n" if ($opts->{'match'} && $opts->{'orphans'});
  133. confess "Error! no-match and orphans options are mutually exclusive.\n" if ($opts->{'orphans'} && $opts->{'no-match'});
  134. my @tests = @cases;
  135. my (@realtests);
  136. my $ext = $opts->{'extension'} // '';
  137. if ($opts->{'match'} || $opts->{'no-match'} || $opts->{'orphans'}) {
  138. my @tmpArr = ();
  139. my $dir = ($opts->{'match'} || $opts->{'orphans'}) ? ($opts->{'match'} || $opts->{'orphans'}) : $opts->{'no-match'};
  140. confess "No such directory '$dir'" if ! -d $dir;
  141. if (!$opts->{'no-recurse'}) {
  142. File::Find::find( sub { push(@realtests,$File::Find::name) if -f && m/\Q$ext\E$/ }, $dir );
  143. } else {
  144. @realtests = glob("$dir/*$ext");
  145. }
  146. foreach my $case (@cases) {
  147. foreach my $path (@realtests) {
  148. next unless $case->{'title'} eq basename($path);
  149. $case->{'path'} = $path;
  150. push(@tmpArr, $case);
  151. last;
  152. }
  153. }
  154. @tmpArr = grep {my $otest = $_; !(grep {$otest->{'title'} eq $_->{'title'}} @tmpArr) } @tests if $opts->{'orphans'};
  155. @tests = @tmpArr;
  156. @tests = map {{'title' => $_}} grep {my $otest = basename($_); scalar(grep {basename($_->{'title'}) eq $otest} @tests) == 0} @realtests if $opts->{'no-match'}; #invert the list in this case.
  157. }
  158. @tests = map { abs_path($_->{'path'}) } @tests if $opts->{'match'} && $opts->{'names-only'};
  159. @tests = map { $_->{'full_title'} = abs_path($_->{'path'}); $_ } @tests if $opts->{'match'} && !$opts->{'names-only'};
  160. @tests = map { $_->{'title'} } @tests if !$opts->{'match'} && $opts->{'names-only'};
  161. return @tests;
  162. }
  163. =head2 getCases
  164. Get cases in a testsuite matching your parameters passed
  165. =cut
  166. sub getCases {
  167. my ($opts,$tr) = @_;
  168. confess("First argument must be instance of TestRail::API") unless blessed($tr) eq 'TestRail::API';
  169. my $project = $tr->getProjectByName($opts->{'project'});
  170. confess "No such project '$opts->{project}'.\n" if !$project;
  171. my $suite = $tr->getTestSuiteByName($project->{'id'},$opts->{'testsuite'});
  172. confess "No such testsuite '$opts->{testsuite}'.\n" if !$suite;
  173. $opts->{'testsuite_id'} = $suite->{'id'};
  174. my $section;
  175. $section = $tr->getSectionByName($project->{'id'},$suite->{'id'},$opts->{'section'}) if $opts->{'section'};
  176. confess "No such section '$opts->{section}.\n" if $opts->{'section'} && !$section;
  177. my $section_id;
  178. $section_id = $section->{'id'} if ref $section eq "HASH";
  179. my $type_ids;
  180. @$type_ids = $tr->typeNamesToIds(@{$opts->{'types'}}) if ref $opts->{'types'} eq 'ARRAY';
  181. #Above will confess if anything's the matter
  182. #TODO Translate opts into filters
  183. my $filters = {
  184. 'section_id' => $section_id,
  185. 'type_id' => $type_ids
  186. };
  187. return $tr->getCases($project->{'id'},$suite->{'id'},$filters);
  188. }
  189. =head2 findCases(opts,@cases)
  190. Find orphan, missing and needing-update cases.
  191. They are returned as the hash keys 'orphans', 'missing', and 'updates' respectively.
  192. The testsuite_id is also returned in the output hashref.
  193. Option hash keys for input are 'no-missing', 'orphans', and 'update'.
  194. Returns HASHREF.
  195. =cut
  196. sub findCases {
  197. my ($opts,@cases) = @_;
  198. confess('testsuite_id parameter mandatory in options HASHREF') unless defined $opts->{'testsuite_id'};
  199. confess('Directory parameter mandatory in options HASHREF.') unless defined $opts->{'directory'};
  200. confess('No such directory "'.$opts->{'directory'}."\"\n") unless -d $opts->{'directory'};
  201. my $ret = {'testsuite_id' => $opts->{'testsuite_id'}};
  202. if (!$opts->{'no-missing'}) {
  203. my $mopts = {
  204. 'no-match' => $opts->{'directory'},
  205. 'names-only' => 1,
  206. 'extension' => $opts->{'extension'}
  207. };
  208. my @missing = findTests($mopts,@cases);
  209. $ret->{'missing'} = \@missing;
  210. }
  211. if ($opts->{'orphans'}) {
  212. my $oopts = {
  213. 'orphans' => $opts->{'directory'},
  214. 'extension' => $opts->{'extension'}
  215. };
  216. my @orphans = findTests($oopts,@cases);
  217. $ret->{'orphans'} = \@orphans;
  218. }
  219. if ($opts->{'update'}) {
  220. my $uopts = {
  221. 'match' => $opts->{'directory'},
  222. 'extension' => $opts->{'extension'}
  223. };
  224. my @updates = findTests($uopts,@cases);
  225. $ret->{'update'} = \@updates;
  226. }
  227. return $ret;
  228. }
  229. 1;
  230. __END__
  231. =head1 SPECIAL THANKS
  232. Thanks to cPanel Inc, for graciously funding the creation of this module.