Find.pm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. =head2 getResults(options, $prior_runs, @cases)
  230. Get results for tests by name, filtered by the provided options, and skipping any runs found in the provided ARRAYREF of run IDs.
  231. Probably should have called this findResults, but we all prefer to get results right?
  232. =cut
  233. sub getResults {
  234. my ($tr,$opts,$prior_runs,@cases) = @_;
  235. my $res = {};
  236. my $projects = $tr->getProjects();
  237. #TODO obey status filtering
  238. #TODO obey result notes text grepping
  239. foreach my $project (@$projects) {
  240. next if $opts->{projects} && !( grep { $_ eq $project->{'name'} } @{$opts->{'projects'}} );
  241. my $runs = $tr->getRuns($project->{'id'});
  242. #Translate plan names to ids
  243. my $plans = $tr->getPlans($project->{'id'}) || [];
  244. $opts->{'runs'} //= [];
  245. my $plan_filters = [];
  246. foreach my $plan (@$plans) {
  247. $plan = $tr->getPlanByID($plan->{'id'});
  248. my $plan_runs = $tr->getChildRuns($plan);
  249. push(@$runs,@$plan_runs) if $plan_runs;
  250. }
  251. if ($opts->{'plans'}) {
  252. @$plan_filters = map { $_->{'id'} } grep { my $p = $_; grep { $p->{'name'} eq $_} @{$opts->{'plans'}} } @$plans;
  253. }
  254. foreach my $run (@$runs) {
  255. next if scalar(@{$opts->{runs}}) && !( grep { $_ eq $run->{'name'} } @{$opts->{'runs'}} );
  256. next if scalar(@$plan_filters) && !( grep { $run->{'plan_id'} ? $_ eq $run->{'plan_id'} : undef } @$plan_filters );
  257. next if grep { $run->{id} eq $_ } @$prior_runs;
  258. foreach my $case (@cases) {
  259. my $c = $tr->getTestByName($run->{'id'},basename($case));
  260. next unless ref $c eq 'HASH';
  261. $res->{$case} //= [];
  262. $c->{results} = $tr->getTestResults($c->{'id'},$tr->{'global_limit'},0);
  263. #Filter by provided pattern, if any
  264. if ($opts->{'pattern'}) {
  265. my $pattern = $opts->{pattern};
  266. @{$c->{results}} = grep { my $comment = $_->{comment} || ''; $comment =~ m/$pattern/i } @{$c->{results}};
  267. }
  268. push(@{$res->{$case}}, $c) if scalar(@{$c->{results}}); #Make sure they weren't filtered out
  269. }
  270. }
  271. }
  272. return $res;
  273. }
  274. 1;
  275. __END__
  276. =head1 SPECIAL THANKS
  277. Thanks to cPanel Inc, for graciously funding the creation of this module.