testrail-lock 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #!/usr/bin/env perl
  2. # ABSTRACT: Lock a test in a TestRail, and return the test name if successful.
  3. # PODNAME: testrail-lock
  4. =head1 SYNOPSIS
  5. # Lock a group of tests and execute them
  6. testrail-tests [OPTIONS] | xargs testrail-lock [OPTIONS] | xargs prove -PTestrail=...
  7. =head1 DESCRIPTION
  8. testrail-lock - pick an untested/retest test in TestRail, lock it, and return the test name if successful.
  9. It is useful to lock the test in situations where you have multiple disconnected test running processes trying to allocate resources toward testing outstanding cases so that effort is not duplicated.
  10. This is accomplished via setting a special locking result on a test rather than simple assignment, as detecting lock conflicts is impossible then due to a lack of assignment history.
  11. Results, however have a history of results set, so we use that fact to detect if a locking collision occured (race condition) and fail to return a result when another process locked during our attempt to lock.
  12. Will respect test priority when making the choice of what test to lock.
  13. This obviously does not make sense with case_per_ok test upload; support for locking entire sections when in case_per_ok upload mode is not supported at this time.
  14. =head1 PARAMETERS:
  15. =head2 MANDATORY PARAMETERS
  16. =over 4
  17. --apiurl : full URL to get to TestRail index document
  18. --password : Your TestRail Password, or a valid API key (TestRail 4.2 and above).
  19. --user : Your TestRail User Name.
  20. -j --project : desired project name.
  21. -r --run : desired run name.
  22. -l --lockname : internal name of lock status.
  23. =back
  24. All mandatory options not passed with the above switches, or in your ~/.testrailrc will be prompted for.
  25. =head2 SEMI-OPTIONAL PARAMETERS
  26. =over 4
  27. -p --plan : desired plan name. Required if the run passed is a child of a plan.
  28. -e --encoding : Character encoding of arguments. Defaults to UTF-8. See L<Encode::Supported> for supported encodings.
  29. =back
  30. =head2 OPTIONAL PARAMETERS
  31. =over 4
  32. -c --config : configuration name to filter plans in run. Can be passed multiple times.
  33. =back
  34. =head1 CONFIGURATION FILE
  35. In your $HOME, (or the current directory, if your system has no concept of a home directory) put a file called .testrailrc with key=value syntax separated by newlines.
  36. Valid Keys are the same as documented by L<App::Prove::Plugin::TestRail>.
  37. All options specified thereby are overridden by passing the command-line switches above.
  38. =head1 MISCELLANEOUS OPTIONS:
  39. =over 4
  40. --mock : don't do any real HTTP requests. Used only by tests.
  41. --help : show this output
  42. =back
  43. =cut
  44. use strict;
  45. use warnings;
  46. use utf8;
  47. use TestRail::API;
  48. use TestRail::Utils;
  49. use Getopt::Long;
  50. use File::HomeDir qw{my_home};
  51. use File::Find;
  52. use Cwd qw{abs_path};
  53. use File::Basename qw{basename};
  54. use Sys::Hostname qw{hostname};
  55. my $hostname = hostname();
  56. my $opts = {};
  57. #Parse config file if we are missing api url/key or user
  58. my $homedir = my_home() || '.';
  59. if (-e $homedir . '/.testrailrc') {
  60. $opts = TestRail::Utils::parseConfig($homedir);
  61. }
  62. GetOptions(
  63. 'apiurl=s' => \$opts->{'apiurl'},
  64. 'password=s' => \$opts->{'password'},
  65. 'user=s' => \$opts->{'user'},
  66. 'l|lockname=s' => \$opts->{'lockname'},
  67. 'j|project=s' => \$opts->{'project'},
  68. 'p|plan=s' => \$opts->{'plan'},
  69. 'r|run=s' => \$opts->{'run'},
  70. 'c|config=s@' => \$opts->{'configs'},
  71. 'mock' => \$opts->{'mock'},
  72. 'e|encoding=s' => \$opts->{'encoding'},
  73. 'h|help' => \$opts->{'help'}
  74. );
  75. if ($opts->{help}) { help(); }
  76. TestRail::Utils::interrogateUser($opts,qw{apiurl user password project run lockname});
  77. if ($opts->{mock}) {
  78. use Test::LWP::UserAgent::TestRailMock;
  79. $opts->{browser} = $Test::LWP::UserAgent::TestRailMock::mockObject;
  80. $opts->{debug} = 1;
  81. }
  82. my $tr = TestRail::API->new($opts->{apiurl},$opts->{user},$opts->{password},$opts->{'encoding'},$opts->{'debug'});
  83. $tr->{'browser'} = $opts->{'browser'} if $opts->{'browser'};
  84. $tr->{'debug'} = 0;
  85. my $project = $tr->getProjectByName($opts->{'project'});
  86. if (!$project) {
  87. warn "No such project '$opts->{project}'.\n";
  88. exit 6;
  89. }
  90. my ($run,$plan);
  91. if ($opts->{'plan'}) {
  92. $plan = $tr->getPlanByName($project->{'id'},$opts->{'plan'});
  93. if (!$plan) {
  94. warn "No such plan '$opts->{plan}'!\n";
  95. exit 1;
  96. }
  97. $run = $tr->getChildRunByName($plan,$opts->{'run'}, $opts->{'configs'});
  98. } else {
  99. $run = $tr->getRunByName($project->{'id'},$opts->{'run'});
  100. }
  101. if (!$run) {
  102. warn "No such run '$opts->{run}' matching the provided configs (if any).\n";
  103. exit 2;
  104. }
  105. my $status_ids;
  106. # Process statuses
  107. @$status_ids = $tr->statusNamesToIds($opts->{'lockname'},'untested','retest');
  108. my ($lock_status_id,$untested_id,$retest_id) = @$status_ids;
  109. my $cases = $tr->getTests($run->{'id'});
  110. my @statuses_to_check_for = ($untested_id,$retest_id);
  111. @statuses_to_check_for = ($lock_status_id) if $opts->{'simulate_race_condition'}; #Unit test stuff
  112. # Limit to only non-locked and open cases
  113. @$cases = grep { my $tstatus = $_->{'status_id'}; scalar(grep { $tstatus eq $_ } @statuses_to_check_for) } @$cases;
  114. @$cases = sort { $a->{'priority_id'} <=> $b->{'priority_id'} } @$cases; #Sort by priority
  115. TRY_AGAIN:
  116. my $test = shift @$cases;
  117. if (!$test) {
  118. warn "No outstanding cases in the provided run.\n";
  119. exit 3;
  120. }
  121. my $res = $tr->createTestResults($test->{'id'},$lock_status_id,"Test Locked by $hostname.\n\nIf this result is preceded immediately by another lock statement like this, please disregard, as a lock collision occurred.");
  122. #If we've got more than 100 lock conflicts, we have big-time problems
  123. my $results = $tr->getTestResults($test->{'id'},100);
  124. #Remember, we're returned results from newest to oldest...
  125. my $next_one = 0;
  126. foreach my $result (@$results) {
  127. unless ($result->{'status_id'} == $lock_status_id) {
  128. #Clearly no lock conflict going on here if next_one is true
  129. last if $next_one;
  130. #Otherwise just skip it until we get to the test we locked
  131. next;
  132. }
  133. if ($result->{id} == $res->{'id'}) {
  134. $next_one = 1;
  135. next;
  136. }
  137. if ($next_one) {
  138. #If we got this far, a lock conflict occurred. Try the next one.
  139. warn "Lock conflict detected. Trying again...\n";
  140. goto TRY_AGAIN;
  141. }
  142. }
  143. if (!$next_one) {
  144. warn "Failed to lock case!";
  145. exit 4;
  146. }
  147. print $test->{'title'}."\n";
  148. exit 0;
  149. __END__
  150. L<TestRail::API>
  151. L<File::HomeDir> for the finding of .testrailrc
  152. =head1 SPECIAL THANKS
  153. Thanks to cPanel Inc, for graciously funding the creation of this distribution.