API.pm 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002
  1. # ABSTRACT: Provides an interface to TestRail's REST api via HTTP
  2. # PODNAME: TestRail::API
  3. package TestRail::API;
  4. =head1 SYNOPSIS
  5. use TestRail::API;
  6. my ($username,$password,$host) = ('foo','bar','http://testrail.baz.foo');
  7. my $tr = TestRail::API->new($host, $username, $password);
  8. =head1 DESCRIPTION
  9. C<TestRail::API> provides methods to access an existing TestRail account using API v2. You can then do things like look up tests, set statuses and create runs from lists of cases.
  10. It is by no means exhaustively implementing every TestRail API function.
  11. =head1 IMPORTANT
  12. All the methods aside from the constructor should not die, but return a false value upon failure (see exceptions below).
  13. When the server is not responsive, expect a -500 response, and retry accordingly by setting the num_tries parameter in the constructor.
  14. Also, all *ByName methods are vulnerable to duplicate naming issues. Try not to use the same name for:
  15. * projects
  16. * testsuites within the same project
  17. * sections within the same testsuite that are peers
  18. * test cases
  19. * test plans and runs outside of plans which are not completed
  20. * configurations
  21. To do so will result in the first of said item found being returned rather than an array of possibilities to choose from.
  22. There are two exceptions to this, in the case of 401 and 403 responses, as these failing generally mean your program has no chance of success anyways.
  23. =cut
  24. use 5.010;
  25. use strict;
  26. use warnings;
  27. use Carp qw{cluck confess};
  28. use Scalar::Util qw{reftype looks_like_number};
  29. use Clone 'clone';
  30. use Try::Tiny;
  31. use Types::Standard qw( slurpy ClassName Object Str Int Bool HashRef ArrayRef Maybe Optional);
  32. use Type::Params qw( compile );
  33. use JSON::MaybeXS 1.001000 ();
  34. use HTTP::Request;
  35. use LWP::UserAgent;
  36. use HTTP::CookieJar::LWP;
  37. use Data::Validate::URI qw{is_uri};
  38. use List::Util 1.33;
  39. use Encode ();
  40. =head1 CONSTRUCTOR
  41. =head2 B<new (api_url, user, password, encoding, debug, do_post_redirect)>
  42. Creates new C<TestRail::API> object.
  43. =over 4
  44. =item STRING C<API URL> - base url for your TestRail api server.
  45. =item STRING C<USER> - Your TestRail User.
  46. =item STRING C<PASSWORD> - Your TestRail password, or a valid API key (TestRail 4.2 and above).
  47. =item STRING C<ENCODING> - The character encoding used by the caller. Defaults to 'UTF-8', see L<Encode::Supported> and for supported encodings.
  48. =item BOOLEAN C<DEBUG> (optional) - Print the JSON responses from TestRail with your requests. Default false.
  49. =item BOOLEAN C<DO_POST_REDIRECT> (optional) - Follow redirects on POST requests (most add/edit/delete calls are POSTs). Default false.
  50. =item INTEGER C<MAX_TRIES> (optional) - Try requests up to X number of times if they fail with anything other than 401/403. Useful with flaky external authenticators, or timeout issues. Default 1.
  51. =item HASHREF C<USER_FETCH_OPTS> (optional) - Options relating to getUsers call done during new:
  52. =over 4
  53. =item BOOLEAN C<skip_userfetch> - Skip fetching all TR users during construction. Default false.
  54. This will save you some time on servers with quite a few users, especially if you don't
  55. particularly have a need to know about things related to TR users themselves.
  56. If you do need this info, you don't really save any time, however, as it will fetch them
  57. in the relevant subroutines that need this information.
  58. Also, on newer versions of TestRail, user fetching is not possible unless you either:
  59. * Are an administrator on the server
  60. * Provide the project_id (https://www.gurock.com/testrail/docs/api/reference/users)
  61. =item STRING C<project_id> - String or number corresponding to a project ID to use when fetching users.
  62. =back
  63. =back
  64. Returns C<TestRail::API> object if login is successful.
  65. my $tr = TestRail::API->new('http://tr.test/testrail', 'moo','M000000!');
  66. Dies on all communication errors with the TestRail server.
  67. Does not do above checks if debug is passed.
  68. =cut
  69. sub new {
  70. state $check = compile(ClassName, Str, Str, Str, Optional[Maybe[Str]], Optional[Maybe[Bool]], Optional[Maybe[Bool]], Optional[Maybe[Int]],Optional[Maybe[HashRef]]);
  71. my ($class,$apiurl,$user,$pass,$encoding,$debug, $do_post_redirect,$max_tries,$userfetch_opts) = $check->(@_);
  72. die("Invalid URI passed to constructor") if !is_uri($apiurl);
  73. $debug //= 0;
  74. my $self = {
  75. user => $user,
  76. pass => $pass,
  77. apiurl => $apiurl,
  78. debug => $debug,
  79. encoding => $encoding || 'UTF-8',
  80. testtree => [],
  81. flattree => [],
  82. user_cache => [],
  83. configurations => {},
  84. tr_fields => undef,
  85. tr_project_id => $userfetch_opts->{'project_id'},
  86. default_request => undef,
  87. global_limit => 250, #Discovered by experimentation
  88. browser => LWP::UserAgent->new(
  89. keep_alive => 10,
  90. cookie_jar => HTTP::CookieJar::LWP->new(),
  91. ),
  92. do_post_redirect => $do_post_redirect,
  93. max_tries => $max_tries // 1,
  94. retry_delay => 5,
  95. };
  96. #Allow POST redirects
  97. if ($self->{do_post_redirect}) {
  98. push @{ $self->{'browser'}->requests_redirectable }, 'POST';
  99. }
  100. #Check chara encoding
  101. $self->{'encoding-nonaliased'} = Encode::resolve_alias($self->{'encoding'});
  102. die("Invalid encoding alias '".$self->{'encoding'}."' passed, see Encoding::Supported for a list of allowed encodings")
  103. unless $self->{'encoding-nonaliased'};
  104. die("Invalid encoding '".$self->{'encoding-nonaliased'}."' passed, see Encoding::Supported for a list of allowed encodings")
  105. unless grep {$_ eq $self->{'encoding-nonaliased'}} (Encode->encodings(":all"));
  106. #Create default request to pass on to LWP::UserAgent
  107. $self->{'default_request'} = HTTP::Request->new();
  108. $self->{'default_request'}->authorization_basic($user,$pass);
  109. bless( $self, $class );
  110. return $self if $self->debug; #For easy class testing without mocks
  111. # Manually do the get_users call to check HTTP status...
  112. # Allow users to skip the check if you have a zillion users etc,
  113. # as apparently that is fairly taxing on TR itself.
  114. if( !$userfetch_opts->{skip_usercache} ) {
  115. my $res = $self->getUsers($userfetch_opts->{project_id});
  116. confess "Error: network unreachable" if !defined($res);
  117. if ( (reftype($res) || 'undef') ne 'ARRAY') {
  118. confess "Unexpected return from _doRequest: $res" if !looks_like_number($res);
  119. confess "Could not communicate with TestRail Server! Check that your URI is correct, and your TestRail installation is functioning correctly." if $res == -500;
  120. confess "Could not list testRail users! Check that your TestRail installation has it's API enabled, and your credentials are correct" if $res == -403;
  121. confess "Bad user credentials!" if $res == -401;
  122. confess "HTTP error $res encountered while communicating with TestRail server. Resolve issue and try again." if $res < 0;
  123. confess "Unknown error occurred: $res";
  124. }
  125. confess "No users detected on TestRail Install! Check that your API is functioning correctly." if !scalar(@$res);
  126. }
  127. return $self;
  128. }
  129. =head1 GETTERS
  130. =head2 B<apiurl>
  131. =head2 B<debug>
  132. Accessors for these parameters you pass into the constructor, in case you forget.
  133. =cut
  134. sub apiurl {
  135. state $check = compile(Object);
  136. my ($self) = $check->(@_);
  137. return $self->{'apiurl'}
  138. }
  139. sub debug {
  140. state $check = compile(Object);
  141. my ($self) = $check->(@_);
  142. return $self->{'debug'};
  143. }
  144. =head2 B<retry_delay>
  145. There is no getter/setter for this parameter, but it is worth mentioning.
  146. This is the number of seconds to wait between failed request retries when max_retries > 1.
  147. #Do something other than the default of 5s, like spam the server mercilessly
  148. $tr->{retry_delay} = 0;
  149. ...
  150. =cut
  151. #Convenient JSON-HTTP fetcher
  152. sub _doRequest {
  153. state $check = compile(Object, Str, Optional[Maybe[Str]], Optional[Maybe[HashRef]]);
  154. my ($self,$path,$method,$data) = $check->(@_);
  155. $self->{num_tries}++;
  156. my $req = clone $self->{'default_request'};
  157. $method //= 'GET';
  158. $req->method($method);
  159. $req->url($self->apiurl.'/'.$path);
  160. warn "$method ".$self->apiurl."/$path" if $self->debug;
  161. my $coder = JSON::MaybeXS->new;
  162. #Data sent is JSON, and encoded per user preference
  163. my $content = $data ? Encode::encode( $self->{'encoding-nonaliased'}, $coder->encode($data) ) : '';
  164. $req->content($content);
  165. $req->header( "Content-Type" => "application/json; charset=".$self->{'encoding'} );
  166. my $response = eval { $self->{'browser'}->request($req) };
  167. #Uncomment to generate mocks
  168. #use Data::Dumper;
  169. #open(my $fh, '>>', 'mock.out');
  170. #print $fh "{\n\n";
  171. #print $fh Dumper($path,'200','OK',$response->headers,$response->content);
  172. #print $fh '$mockObject->map_response(qr/\Q$VAR1\E/,HTTP::Response->new($VAR2, $VAR3, $VAR4, $VAR5));';
  173. #print $fh "\n\n}\n\n";
  174. #close $fh;
  175. if ($@) {
  176. #LWP threw an ex, probably a timeout
  177. if ($self->{num_tries} >= $self->{max_tries}) {
  178. $self->{num_tries} = 0;
  179. confess "Failed to satisfy request after $self->{num_tries} tries!";
  180. }
  181. cluck "WARNING: TestRail API request failed due to timeout, or other LWP fatal condition, re-trying request...\n";
  182. sleep $self->{retry_delay} if $self->{retry_delay};
  183. goto &_doRequest;
  184. }
  185. return $response if !defined($response); #worst case
  186. if ($response->code == 403) {
  187. confess "ERROR 403: Access Denied: ".$response->content;
  188. }
  189. if ($response->code == 401) {
  190. confess "ERROR 401: Authentication failed: ".$response->content;
  191. }
  192. if ($response->code != 200) {
  193. #LWP threw an ex, probably a timeout
  194. if ($self->{num_tries} >= $self->{max_tries}) {
  195. $self->{num_tries} = 0;
  196. cluck "ERROR: Arguments Bad? (got code ".$response->code."): ".$response->content;
  197. return -int($response->code);
  198. }
  199. cluck "WARNING: TestRail API request failed (got code ".$response->code."), re-trying request...\n";
  200. sleep $self->{retry_delay} if $self->{retry_delay};
  201. goto &_doRequest;
  202. }
  203. $self->{num_tries} = 0;
  204. try {
  205. return $coder->decode($response->content);
  206. } catch {
  207. if ($response->code == 200 && !$response->content) {
  208. return 1; #This function probably just returns no data
  209. } else {
  210. cluck "ERROR: Malformed JSON returned by API.";
  211. cluck $@;
  212. if (!$self->debug) { #Otherwise we've already printed this, but we need to know if we encounter this
  213. cluck "RAW CONTENT:";
  214. cluck $response->content
  215. }
  216. return 0;
  217. }
  218. }
  219. }
  220. =head1 USER METHODS
  221. =head2 B<getUsers ()>
  222. Get all the user definitions for the provided Test Rail install.
  223. Returns ARRAYREF of user definition HASHREFs.
  224. =cut
  225. sub getUsers {
  226. state $check = compile(Object,Optional[Maybe[Str]]);
  227. my ($self,$project_id) = $check->(@_);
  228. # Return shallow clone of user_cache if set.
  229. return [ @{ $self->{'user_cache'} } ] if ref $self->{'user_cache'} eq 'ARRAY' && scalar(@{$self->{'user_cache'}});
  230. my $maybe_project = $project_id ? "/$project_id" : '';
  231. my $res = $self->_doRequest("index.php?/api/v2/get_users$maybe_project");
  232. return -500 if !$res || (reftype($res) || 'undef') ne 'ARRAY';
  233. $self->{'user_cache'} = $res;
  234. return clone($res);
  235. }
  236. =head2 B<getUserByID(id)>
  237. =cut
  238. =head2 B<getUserByName(name)>
  239. =cut
  240. =head2 B<getUserByEmail(email)>
  241. Get user definition hash by ID, Name or Email.
  242. Returns user definition HASHREF.
  243. For efficiency's sake, these methods cache the result of getUsers until you explicitly run it again.
  244. =cut
  245. sub getUserByID {
  246. state $check = compile(Object, Int);
  247. my ($self,$user) = $check->(@_);
  248. my $users = $self->getUsers();
  249. return $users if ref $users ne 'ARRAY';
  250. foreach my $usr (@$users) {
  251. return $usr if $usr->{'id'} == $user;
  252. }
  253. return 0;
  254. }
  255. sub getUserByName {
  256. state $check = compile(Object, Str);
  257. my ($self,$user) = $check->(@_);
  258. my $users = $self->getUsers();
  259. return $users if ref $users ne 'ARRAY';
  260. foreach my $usr (@$users) {
  261. return $usr if $usr->{'name'} eq $user;
  262. }
  263. return 0;
  264. }
  265. sub getUserByEmail {
  266. state $check = compile(Object, Str);
  267. my ($self,$email) = $check->(@_);
  268. my $users = $self->getUsers();
  269. return $users if ref $users ne 'ARRAY';
  270. foreach my $usr (@$users) {
  271. return $usr if $usr->{'email'} eq $email;
  272. }
  273. return 0;
  274. }
  275. =head2 userNamesToIds(names)
  276. Convenience method to translate a list of user names to TestRail user IDs.
  277. =over 4
  278. =item ARRAY C<NAMES> - Array of user names to translate to IDs.
  279. =back
  280. Returns ARRAY of user IDs.
  281. Throws an exception in the case of one (or more) of the names not corresponding to a valid username.
  282. =cut
  283. sub userNamesToIds {
  284. state $check = compile(Object, slurpy ArrayRef[Str]);
  285. my ($self,$names) = $check->(@_);
  286. confess("At least one user name must be provided") if !scalar(@$names);
  287. my @ret = grep {defined $_} map {my $user = $_; my @list = grep {$user->{'name'} eq $_} @$names; scalar(@list) ? $user->{'id'} : undef} @{$self->getUsers()};
  288. confess("One or more user names provided does not exist in TestRail.") unless scalar(@$names) == scalar(@ret);
  289. return @ret;
  290. };
  291. =head1 PROJECT METHODS
  292. =head2 B<createProject (name, [description,send_announcement])>
  293. Creates new Project (Database of testsuites/tests).
  294. Optionally specify an announcement to go out to the users.
  295. Requires TestRail admin login.
  296. =over 4
  297. =item STRING C<NAME> - Desired name of project.
  298. =item STRING C<DESCRIPTION> (optional) - Description of project. Default value is 'res ipsa loquiter'.
  299. =item BOOLEAN C<SEND ANNOUNCEMENT> (optional) - Whether to confront users with an announcement about your awesome project on next login. Default false.
  300. =back
  301. Returns project definition HASHREF on success, false otherwise.
  302. $tl->createProject('Widgetronic 4000', 'Tests for the whiz-bang new product', true);
  303. =cut
  304. sub createProject {
  305. state $check = compile(Object, Str, Optional[Maybe[Str]], Optional[Maybe[Bool]]);
  306. my ($self,$name,$desc,$announce) = $check->(@_);
  307. $desc //= 'res ipsa loquiter';
  308. $announce //= 0;
  309. my $input = {
  310. name => $name,
  311. announcement => $desc,
  312. show_announcement => $announce
  313. };
  314. return $self->_doRequest('index.php?/api/v2/add_project','POST',$input);
  315. }
  316. =head2 B<deleteProject (id)>
  317. Deletes specified project by ID.
  318. Requires TestRail admin login.
  319. =over 4
  320. =item STRING C<NAME> - Desired name of project.
  321. =back
  322. Returns BOOLEAN.
  323. $success = $tl->deleteProject(1);
  324. =cut
  325. sub deleteProject {
  326. state $check = compile(Object, Int);
  327. my ($self,$proj) = $check->(@_);
  328. return $self->_doRequest('index.php?/api/v2/delete_project/'.$proj,'POST');
  329. }
  330. =head2 B<getProjects (filters)>
  331. Get all available projects
  332. =over 4
  333. =item HASHREF C<FILTERS> (optional) - HASHREF describing parameters to filter cases by.
  334. =back
  335. Returns array of project definition HASHREFs, false otherwise.
  336. $projects = $tl->getProjects;
  337. See:
  338. L<https://www.gurock.com/testrail/docs/api/reference/projects#getprojects>
  339. for details as to the allowable filter keys.
  340. =cut
  341. sub getProjects {
  342. state $check = compile(Object,Optional[Maybe[HashRef]]);
  343. my ($self,$filters) = $check->(@_);
  344. my $result = $self->_doRequest('index.php?/api/v2/get_projects' . _convert_filters_to_string($filters) );
  345. return -500 if !$result || (reftype($result) || 'undef') ne 'HASH';
  346. my $projects = $result->{'projects'};
  347. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  348. #Save state for future use, if needed
  349. $self->{'testtree'} = $projects;
  350. foreach my $pj (@{$projects}) {
  351. $pj->{'type'} = 'project';
  352. }
  353. return $projects;
  354. }
  355. =head2 B<getProjectByName ($project)>
  356. Gets some project definition hash by it's name
  357. =over 4
  358. =item STRING C<PROJECT> - desired project
  359. =back
  360. Returns desired project definition HASHREF, false otherwise.
  361. $project = $tl->getProjectByName('FunProject');
  362. =cut
  363. sub getProjectByName {
  364. state $check = compile(Object, Str);
  365. my ($self,$project) = $check->(@_);
  366. #See if we already have the project list...
  367. my $projects = $self->{'testtree'};
  368. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  369. $projects = $self->getProjects() unless scalar(@$projects);
  370. #Search project list for project
  371. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  372. for my $candidate (@$projects) {
  373. return $candidate if ($candidate->{'name'} eq $project);
  374. }
  375. return 0;
  376. }
  377. =head2 B<getProjectByID ($project)>
  378. Gets some project definition hash by it's ID
  379. =over 4
  380. =item INTEGER C<PROJECT> - desired project
  381. =back
  382. Returns desired project definition HASHREF, false otherwise.
  383. $projects = $tl->getProjectByID(222);
  384. =cut
  385. sub getProjectByID {
  386. state $check = compile(Object, Int);
  387. my ($self,$project) = $check->(@_);
  388. #See if we already have the project list...
  389. my $projects = $self->{'testtree'};
  390. $projects = $self->getProjects() unless scalar(@$projects);
  391. #Search project list for project
  392. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  393. for my $candidate (@$projects) {
  394. return $candidate if ($candidate->{'id'} eq $project);
  395. }
  396. return 0;
  397. }
  398. =head1 TESTSUITE METHODS
  399. =head2 B<createTestSuite (project_id, name, [description])>
  400. Creates new TestSuite (folder of tests) in the database of test specifications under given project id having given name and details.
  401. =over 4
  402. =item INTEGER C<PROJECT ID> - ID of project this test suite should be under.
  403. =item STRING C<NAME> - Desired name of test suite.
  404. =item STRING C<DESCRIPTION> (optional) - Description of test suite. Default value is 'res ipsa loquiter'.
  405. =back
  406. Returns TS definition HASHREF on success, false otherwise.
  407. $tl->createTestSuite(1, 'broken tests', 'Tests that should be reviewed');
  408. =cut
  409. sub createTestSuite {
  410. state $check = compile(Object, Int, Str, Optional[Maybe[Str]]);
  411. my ($self,$project_id,$name,$details) = $check->(@_);
  412. $details //= 'res ipsa loquiter';
  413. my $input = {
  414. name => $name,
  415. description => $details
  416. };
  417. return $self->_doRequest('index.php?/api/v2/add_suite/'.$project_id,'POST',$input);
  418. }
  419. =head2 B<deleteTestSuite (suite_id)>
  420. Deletes specified testsuite.
  421. =over 4
  422. =item INTEGER C<SUITE ID> - ID of testsuite to delete.
  423. =back
  424. Returns BOOLEAN.
  425. $tl->deleteTestSuite(1);
  426. =cut
  427. sub deleteTestSuite {
  428. state $check = compile(Object, Int);
  429. my ($self,$suite_id) = $check->(@_);
  430. return $self->_doRequest('index.php?/api/v2/delete_suite/'.$suite_id,'POST');
  431. }
  432. =head2 B<getTestSuites (project_id)>
  433. Gets the testsuites for a project
  434. =over 4
  435. =item STRING C<PROJECT ID> - desired project's ID
  436. =back
  437. Returns ARRAYREF of testsuite definition HASHREFs, 0 on error.
  438. $suites = $tl->getTestSuites(123);
  439. =cut
  440. sub getTestSuites {
  441. state $check = compile(Object, Int);
  442. my ($self,$proj) = $check->(@_);
  443. return $self->_doRequest('index.php?/api/v2/get_suites/'.$proj);
  444. }
  445. =head2 B<getTestSuiteByName (project_id,testsuite_name)>
  446. Gets the testsuite that matches the given name inside of given project.
  447. =over 4
  448. =item STRING C<PROJECT ID> - ID of project holding this testsuite
  449. =item STRING C<TESTSUITE NAME> - desired parent testsuite name
  450. =back
  451. Returns desired testsuite definition HASHREF, false otherwise.
  452. $suites = $tl->getTestSuitesByName(321, 'hugSuite');
  453. =cut
  454. sub getTestSuiteByName {
  455. state $check = compile(Object, Int, Str);
  456. my ($self,$project_id,$testsuite_name) = $check->(@_);
  457. #TODO cache
  458. my $suites = $self->getTestSuites($project_id);
  459. return -500 if !$suites || (reftype($suites) || 'undef') ne 'ARRAY'; #No suites for project, or no project
  460. foreach my $suite (@$suites) {
  461. return $suite if $suite->{'name'} eq $testsuite_name;
  462. }
  463. return 0; #Couldn't find it
  464. }
  465. =head2 B<getTestSuiteByID (testsuite_id)>
  466. Gets the testsuite with the given ID.
  467. =over 4
  468. =item STRING C<TESTSUITE_ID> - TestSuite ID.
  469. =back
  470. Returns desired testsuite definition HASHREF, false otherwise.
  471. $tests = $tl->getTestSuiteByID(123);
  472. =cut
  473. sub getTestSuiteByID {
  474. state $check = compile(Object, Int);
  475. my ($self,$testsuite_id) = $check->(@_);
  476. return $self->_doRequest('index.php?/api/v2/get_suite/'.$testsuite_id);
  477. }
  478. =head1 SECTION METHODS
  479. =head2 B<createSection(project_id,suite_id,name,[parent_id])>
  480. Creates a section.
  481. =over 4
  482. =item INTEGER C<PROJECT ID> - Parent Project ID.
  483. =item INTEGER C<SUITE ID> - Parent TestSuite ID.
  484. =item STRING C<NAME> - desired section name.
  485. =item INTEGER C<PARENT ID> (optional) - parent section id
  486. =back
  487. Returns new section definition HASHREF, false otherwise.
  488. $section = $tr->createSection(1,1,'nugs',1);
  489. =cut
  490. sub createSection {
  491. state $check = compile(Object, Int, Int, Str, Optional[Maybe[Int]]);
  492. my ($self,$project_id,$suite_id,$name,$parent_id) = $check->(@_);
  493. my $input = {
  494. name => $name,
  495. suite_id => $suite_id
  496. };
  497. $input->{'parent_id'} = $parent_id if $parent_id;
  498. return $self->_doRequest('index.php?/api/v2/add_section/'.$project_id,'POST',$input);
  499. }
  500. =head2 B<deleteSection (section_id)>
  501. Deletes specified section.
  502. =over 4
  503. =item INTEGER C<SECTION ID> - ID of section to delete.
  504. =back
  505. Returns BOOLEAN.
  506. $tr->deleteSection(1);
  507. =cut
  508. sub deleteSection {
  509. state $check = compile(Object, Int);
  510. my ($self,$section_id) = $check->(@_);
  511. return $self->_doRequest('index.php?/api/v2/delete_section/'.$section_id,'POST');
  512. }
  513. =head2 B<getSections (project_id,suite_id)>
  514. Gets sections for a given project and suite.
  515. =over 4
  516. =item INTEGER C<PROJECT ID> - ID of parent project.
  517. =item INTEGER C<SUITE ID> - ID of suite to get sections for.
  518. =back
  519. Returns ARRAYREF of section definition HASHREFs.
  520. $tr->getSections(1,2);
  521. =cut
  522. sub getSections {
  523. state $check = compile(Object, Int, Int);
  524. my ($self,$project_id,$suite_id) = $check->(@_);
  525. #Cache sections to reduce requests in tight loops
  526. return $self->{'sections'}->{$suite_id} if $self->{'sections'}->{$suite_id};
  527. my $response = $self->_doRequest("index.php?/api/v2/get_sections/$project_id&suite_id=$suite_id");
  528. return -500 if !$response || (reftype($response) || 'undef') ne 'HASH';
  529. my $sections = $response->{'sections'};
  530. return -500 if !$sections || (reftype($sections) || 'undef') ne 'ARRAY';
  531. $self->{'sections'}->{$suite_id} = $sections;
  532. return $self->{'sections'}->{$suite_id};
  533. }
  534. =head2 B<getSectionByID (section_id)>
  535. Gets desired section.
  536. =over 4
  537. =item INTEGER C<PROJECT ID> - ID of parent project.
  538. =item INTEGER C<SUITE ID> - ID of suite to get sections for.
  539. =back
  540. Returns section definition HASHREF.
  541. $tr->getSectionByID(344);
  542. =cut
  543. sub getSectionByID {
  544. state $check = compile(Object, Int);
  545. my ($self,$section_id) = $check->(@_);
  546. return $self->_doRequest("index.php?/api/v2/get_section/$section_id");
  547. }
  548. =head2 B<getSectionByName (project_id,suite_id,name)>
  549. Gets desired section.
  550. =over 4
  551. =item INTEGER C<PROJECT ID> - ID of parent project.
  552. =item INTEGER C<SUITE ID> - ID of suite to get section for.
  553. =item STRING C<NAME> - name of section to get
  554. =back
  555. Returns section definition HASHREF.
  556. $tr->getSectionByName(1,2,'nugs');
  557. =cut
  558. sub getSectionByName {
  559. state $check = compile(Object, Int, Int, Str);
  560. my ($self,$project_id,$suite_id,$section_name) = $check->(@_);
  561. my $sections = $self->getSections($project_id,$suite_id);
  562. return -500 if !$sections || (reftype($sections) || 'undef') ne 'ARRAY';
  563. foreach my $sec (@$sections) {
  564. return $sec if $sec->{'name'} eq $section_name;
  565. }
  566. return 0;
  567. }
  568. =head2 B<getChildSections ($project_id, section)>
  569. Gets desired section's child sections.
  570. =over 4
  571. =item INTEGER C<PROJECT_ID> - parent project ID of section.
  572. =item HASHREF C<SECTION> - section definition HASHREF.
  573. =back
  574. Returns ARRAYREF of section definition HASHREF. ARRAYREF is empty if there are none.
  575. Recursively searches for children, so the children of child sections will be returned as well.
  576. $tr->getChildSections($section);
  577. =cut
  578. sub getChildSections {
  579. state $check = compile(Object, Int, HashRef);
  580. my ($self, $project_id, $section) = $check->(@_);
  581. my $sections_orig = $self->getSections($project_id,$section->{suite_id});
  582. return [] if !$sections_orig || (reftype($sections_orig) || 'undef') ne 'ARRAY';
  583. my @sections = grep { $_->{'parent_id'} ? $_->{'parent_id'} == $section->{'id'} : 0 } @$sections_orig;
  584. foreach my $sec (@sections) {
  585. push(@sections, grep { $_->{'parent_id'} ? $_->{'parent_id'} == $sec->{'id'} : 0 } @$sections_orig);
  586. }
  587. return \@sections;
  588. }
  589. =head2 sectionNamesToIds(project_id,suite_id,names)
  590. Convenience method to translate a list of section names to TestRail section IDs.
  591. =over 4
  592. =item INTEGER C<PROJECT ID> - ID of parent project.
  593. =item INTEGER C<SUITE ID> - ID of parent suite.
  594. =item ARRAY C<NAMES> - Array of section names to translate to IDs.
  595. =back
  596. Returns ARRAY of section IDs.
  597. Throws an exception in the case of one (or more) of the names not corresponding to a valid section name.
  598. =cut
  599. sub sectionNamesToIds {
  600. my ($self,$project_id,$suite_id,@names) = @_;
  601. my $sections = $self->getSections($project_id,$suite_id) or confess("Could not find sections in provided project/suite.");
  602. return _X_in_my_Y($self,$sections,'id',@names);
  603. }
  604. =head1 CASE METHODS
  605. =head2 B<getCaseTypes ()>
  606. Gets possible case types.
  607. Returns ARRAYREF of case type definition HASHREFs.
  608. $tr->getCaseTypes();
  609. =cut
  610. sub getCaseTypes {
  611. state $check = compile(Object);
  612. my ($self) = $check->(@_);
  613. return clone($self->{'type_cache'}) if defined($self->{'type_cache'});
  614. my $types = $self->_doRequest("index.php?/api/v2/get_case_types");
  615. return -500 if !$types || (reftype($types) || 'undef') ne 'ARRAY';
  616. $self->{'type_cache'} = $types;
  617. return clone $types;
  618. }
  619. =head2 B<getCaseTypeByName (name)>
  620. Gets case type by name.
  621. =over 4
  622. =item STRING C<NAME> - Name of desired case type
  623. =back
  624. Returns case type definition HASHREF.
  625. Dies if named case type does not exist.
  626. $tr->getCaseTypeByName();
  627. =cut
  628. sub getCaseTypeByName {
  629. state $check = compile(Object, Str);
  630. my ($self,$name) = $check->(@_);
  631. my $types = $self->getCaseTypes();
  632. return -500 if !$types || (reftype($types) || 'undef') ne 'ARRAY';
  633. foreach my $type (@$types) {
  634. return $type if $type->{'name'} eq $name;
  635. }
  636. confess("No such case type '$name'!");
  637. }
  638. =head2 typeNamesToIds(names)
  639. Convenience method to translate a list of case type names to TestRail case type IDs.
  640. =over 4
  641. =item ARRAY C<NAMES> - Array of status names to translate to IDs.
  642. =back
  643. Returns ARRAY of type IDs in the same order as the type names passed.
  644. Throws an exception in the case of one (or more) of the names not corresponding to a valid case type.
  645. =cut
  646. sub typeNamesToIds {
  647. my ($self,@names) = @_;
  648. return _X_in_my_Y($self,$self->getCaseTypes(),'id',@names);
  649. };
  650. =head2 B<createCase(section_id,title,type_id,options,extra_options)>
  651. Creates a test case.
  652. =over 4
  653. =item INTEGER C<SECTION ID> - Parent Section ID.
  654. =item STRING C<TITLE> - Case title.
  655. =item INTEGER C<TYPE_ID> (optional) - desired test type's ID. Defaults to whatever your TR install considers the default type.
  656. =item HASHREF C<OPTIONS> (optional) - Custom fields in the case are the keys, set to the values provided. See TestRail API documentation for more info.
  657. =item HASHREF C<EXTRA OPTIONS> (optional) - contains priority_id, estimate, milestone_id and refs as possible keys. See TestRail API documentation for more info.
  658. =back
  659. Returns new case definition HASHREF, false otherwise.
  660. $custom_opts = {
  661. preconds => "Test harness installed",
  662. steps => "Do the needful",
  663. expected => "cubicle environment transforms into Dali painting"
  664. };
  665. $other_opts = {
  666. priority_id => 4,
  667. milestone_id => 666,
  668. estimate => '2m 45s',
  669. refs => ['TRACE-22','ON-166'] #ARRAYREF of bug IDs.
  670. }
  671. $case = $tr->createCase(1,'Do some stuff',3,$custom_opts,$other_opts);
  672. =cut
  673. sub createCase {
  674. state $check = compile(Object, Int, Str, Optional[Maybe[Int]], Optional[Maybe[HashRef]], Optional[Maybe[HashRef]]);
  675. my ($self,$section_id,$title,$type_id,$opts,$extras) = $check->(@_);
  676. my $stuff = {
  677. title => $title,
  678. type_id => $type_id
  679. };
  680. #Handle sort of optional but baked in options
  681. if (defined($extras) && reftype($extras) eq 'HASH') {
  682. $stuff->{'priority_id'} = $extras->{'priority_id'} if defined($extras->{'priority_id'});
  683. $stuff->{'estimate'} = $extras->{'estimate'} if defined($extras->{'estimate'});
  684. $stuff->{'milestone_id'} = $extras->{'milestone_id'} if defined($extras->{'milestone_id'});
  685. $stuff->{'refs'} = join(',',@{$extras->{'refs'}}) if defined($extras->{'refs'});
  686. }
  687. #Handle custom fields
  688. if (defined($opts) && reftype($opts) eq 'HASH') {
  689. foreach my $key (keys(%$opts)) {
  690. $stuff->{"custom_$key"} = $opts->{$key};
  691. }
  692. }
  693. return $self->_doRequest("index.php?/api/v2/add_case/$section_id",'POST',$stuff);
  694. }
  695. =head2 B<updateCase(case_id,options)>
  696. Updates a test case.
  697. =over 4
  698. =item INTEGER C<CASE ID> - Case ID.
  699. =item HASHREF C<OPTIONS> - Various things about a case to set. Everything except section_id in the output of getCaseBy* methods is a valid input here.
  700. =back
  701. Returns new case definition HASHREF, false otherwise.
  702. =cut
  703. sub updateCase {
  704. state $check = compile(Object, Int, Optional[Maybe[HashRef]]);
  705. my ($self,$case_id,$options) = $check->(@_);
  706. return $self->_doRequest("index.php?/api/v2/update_case/$case_id",'POST',$options);
  707. }
  708. =head2 B<deleteCase (case_id)>
  709. Deletes specified test case.
  710. =over 4
  711. =item INTEGER C<CASE ID> - ID of case to delete.
  712. =back
  713. Returns BOOLEAN.
  714. $tr->deleteCase(1324);
  715. =cut
  716. sub deleteCase {
  717. state $check = compile(Object, Int);
  718. my ($self,$case_id) = $check->(@_);
  719. return $self->_doRequest("index.php?/api/v2/delete_case/$case_id",'POST');
  720. }
  721. =head2 B<getCases (project_id,suite_id,filters)>
  722. Gets cases for provided section.
  723. =over 4
  724. =item INTEGER C<PROJECT ID> - ID of parent project.
  725. =item INTEGER C<SUITE ID> - ID of parent suite.
  726. =item HASHREF C<FILTERS> (optional) - HASHREF describing parameters to filter cases by.
  727. =back
  728. See:
  729. L<http://docs.gurock.com/testrail-api2/reference-cases#get_cases>
  730. for details as to the allowable filter keys.
  731. If the section ID is omitted, all cases for the suite will be returned.
  732. Returns ARRAYREF of test case definition HASHREFs.
  733. $tr->getCases(1,2, {'section_id' => 3} );
  734. =cut
  735. sub getCases {
  736. state $check = compile(Object, Int, Int, Optional[Maybe[HashRef]]);
  737. my ($self,$project_id,$suite_id,$filters) = $check->(@_);
  738. my $url = "index.php?/api/v2/get_cases/$project_id&suite_id=$suite_id";
  739. $url .= _convert_filters_to_string($filters);
  740. my $response = $self->_doRequest($url);
  741. return -500 if !$response || (reftype($response) || 'undef') ne 'HASH';
  742. my $cases = $response->{'cases'};
  743. return -500 if !$cases || (reftype($cases) || 'undef') ne 'ARRAY';
  744. return $cases;
  745. }
  746. =head2 B<getCaseByName (project_id,suite_id,name,filters)>
  747. Gets case by name.
  748. =over 4
  749. =item INTEGER C<PROJECT ID> - ID of parent project.
  750. =item INTEGER C<SUITE ID> - ID of parent suite.
  751. =item STRING C<NAME> - Name of desired test case.
  752. =item HASHREF C<FILTERS> - Filter dictionary acceptable to getCases.
  753. =back
  754. Returns test case definition HASHREF.
  755. $tr->getCaseByName(1,2,'nugs', {'section_id' => 3});
  756. =cut
  757. sub getCaseByName {
  758. state $check = compile(Object, Int, Int, Str, Optional[Maybe[HashRef]]);
  759. my ($self,$project_id,$suite_id,$name,$filters) = $check->(@_);
  760. my $cases = $self->getCases($project_id,$suite_id,$filters);
  761. return -500 if !$cases || (reftype($cases) || 'undef') ne 'ARRAY';
  762. foreach my $case (@$cases) {
  763. return $case if $case->{'title'} eq $name;
  764. }
  765. return 0;
  766. }
  767. =head2 B<getCaseByID (case_id)>
  768. Gets case by ID.
  769. =over 4
  770. =item INTEGER C<CASE ID> - ID of case.
  771. =back
  772. Returns test case definition HASHREF.
  773. $tr->getCaseByID(1345);
  774. =cut
  775. sub getCaseByID {
  776. state $check = compile(Object, Int);
  777. my ($self,$case_id) = $check->(@_);
  778. return $self->_doRequest("index.php?/api/v2/get_case/$case_id");
  779. }
  780. =head2 getCaseFields
  781. Returns ARRAYREF of available test case custom fields.
  782. $tr->getCaseFields();
  783. Output is cached in the case_fields parameter. Cache is invalidated when addCaseField is called.
  784. =cut
  785. sub getCaseFields {
  786. state $check = compile(Object);
  787. my ($self) = $check->(@_);
  788. return $self->{case_fields} if $self->{case_fields};
  789. $self->{case_fields} = $self->_doRequest("index.php?/api/v2/get_case_fields");
  790. return $self->{case_fields};
  791. }
  792. =head2 addCaseField(%options)
  793. Returns HASHREF describing the case field you just added.
  794. $tr->addCaseField(%options)
  795. =cut
  796. sub addCaseField {
  797. state $check = compile(Object,slurpy HashRef);
  798. my ($self,$options) = $check->(@_);
  799. $self->{case_fields} = undef;
  800. return $self->_doRequest("index.php?/api/v2/add_case_field", 'POST', $options);
  801. }
  802. =head1 PRIORITY METHODS
  803. =head2 B<getPriorities ()>
  804. Gets possible priorities.
  805. Returns ARRAYREF of priority definition HASHREFs.
  806. $tr->getPriorities();
  807. =cut
  808. sub getPriorities {
  809. state $check = compile(Object);
  810. my ($self) = $check->(@_);
  811. return clone($self->{'priority_cache'}) if defined($self->{'priority_cache'});
  812. my $priorities = $self->_doRequest("index.php?/api/v2/get_priorities");
  813. return -500 if !$priorities || (reftype($priorities) || 'undef') ne 'ARRAY';
  814. $self->{'priority_cache'} = $priorities;
  815. return clone $priorities;
  816. }
  817. =head2 B<getPriorityByName (name)>
  818. Gets priority by name.
  819. =over 4
  820. =item STRING C<NAME> - Name of desired priority
  821. =back
  822. Returns priority definition HASHREF.
  823. Dies if named priority does not exist.
  824. $tr->getPriorityByName();
  825. =cut
  826. sub getPriorityByName {
  827. state $check = compile(Object, Str);
  828. my ($self,$name) = $check->(@_);
  829. my $priorities = $self->getPriorities();
  830. return -500 if !$priorities || (reftype($priorities) || 'undef') ne 'ARRAY';
  831. foreach my $priority (@$priorities) {
  832. return $priority if $priority->{'name'} eq $name;
  833. }
  834. confess("No such priority '$name'!");
  835. }
  836. =head2 priorityNamesToIds(names)
  837. Convenience method to translate a list of priority names to TestRail priority IDs.
  838. =over 4
  839. =item ARRAY C<NAMES> - Array of priority names to translate to IDs.
  840. =back
  841. Returns ARRAY of priority IDs in the same order as the priority names passed.
  842. Throws an exception in the case of one (or more) of the names not corresponding to a valid priority.
  843. =cut
  844. sub priorityNamesToIds {
  845. my ($self,@names) = @_;
  846. return _X_in_my_Y($self,$self->getPriorities(),'id',@names);
  847. };
  848. =head1 RUN METHODS
  849. =head2 B<createRun (project_id,suite_id,name,description,milestone_id,assigned_to_id,case_ids)>
  850. Create a run.
  851. =over 4
  852. =item INTEGER C<PROJECT ID> - ID of parent project.
  853. =item INTEGER C<SUITE ID> - ID of suite to base run on
  854. =item STRING C<NAME> - Name of run
  855. =item STRING C<DESCRIPTION> (optional) - Description of run
  856. =item INTEGER C<MILESTONE ID> (optional) - ID of milestone
  857. =item INTEGER C<ASSIGNED TO ID> (optional) - User to assign the run to
  858. =item ARRAYREF C<CASE IDS> (optional) - Array of case IDs in case you don't want to use the whole testsuite when making the build.
  859. =back
  860. Returns run definition HASHREF.
  861. $tr->createRun(1,1345,'RUN AWAY','SO FAR AWAY',22,3,[3,4,5,6]);
  862. =cut
  863. #If you pass an array of case ids, it implies include_all is false
  864. sub createRun {
  865. state $check = compile(Object, Int, Int, Str, Optional[Maybe[Str]], Optional[Maybe[Int]], Optional[Maybe[Int]], Optional[Maybe[ArrayRef[Int]]]);
  866. my ($self,$project_id,$suite_id,$name,$desc,$milestone_id,$assignedto_id,$case_ids) = $check->(@_);
  867. my $stuff = {
  868. suite_id => $suite_id,
  869. name => $name,
  870. description => $desc,
  871. milestone_id => $milestone_id,
  872. assignedto_id => $assignedto_id,
  873. include_all => defined($case_ids) ? 0 : 1,
  874. case_ids => $case_ids
  875. };
  876. return $self->_doRequest("index.php?/api/v2/add_run/$project_id",'POST',$stuff);
  877. }
  878. =head2 B<deleteRun (run_id)>
  879. Deletes specified run.
  880. =over 4
  881. =item INTEGER C<RUN ID> - ID of run to delete.
  882. =back
  883. Returns BOOLEAN.
  884. $tr->deleteRun(1324);
  885. =cut
  886. sub deleteRun {
  887. state $check = compile(Object, Int);
  888. my ($self,$run_id) = $check->(@_);
  889. return $self->_doRequest("index.php?/api/v2/delete_run/$run_id",'POST');
  890. }
  891. =head2 B<getRuns (project_id,filters)>
  892. Get all runs for specified project.
  893. To do this, it must make (no. of runs/250) HTTP requests.
  894. This is due to the maximum result set limit enforced by testrail.
  895. =over 4
  896. =item INTEGER C<PROJECT_ID> - ID of parent project
  897. =item HASHREF C<FILTERS> - (optional) dictionary of filters, with keys corresponding to the documented filters for get_runs (other than limit/offset).
  898. =back
  899. Returns ARRAYREF of run definition HASHREFs.
  900. $allRuns = $tr->getRuns(6969);
  901. Possible filters:
  902. =over 4
  903. =item created_after (UNIX timestamp)
  904. =item created_before (UNIX timestamp)
  905. =item created_by (csv of ints) IDs of users plans were created by
  906. =item is_completed (bool)
  907. =item milestone_id (csv of ints) IDs of milestone assigned to plans
  908. =item refs_filter (string) A single Reference ID (e.g. TR-a, 4291, etc.)
  909. =item suite_id (csv of ints) A comma-separated list of test suite IDs to filter by.
  910. =back
  911. =cut
  912. sub getRuns {
  913. state $check = compile(Object, Int, Optional[Maybe[HashRef]]);
  914. my ($self,$project_id,$filters) = $check->(@_);
  915. my $initial_runs = $self->getRunsPaginated($project_id,$self->{'global_limit'},0,$filters);
  916. return $initial_runs unless (reftype($initial_runs) || 'undef') eq 'ARRAY';
  917. my $runs = [];
  918. push(@$runs,@$initial_runs);
  919. my $offset = 1;
  920. while (scalar(@$initial_runs) == $self->{'global_limit'}) {
  921. $initial_runs = $self->getRunsPaginated($project_id,$self->{'global_limit'},($self->{'global_limit'} * $offset),$filters);
  922. return $initial_runs unless (reftype($initial_runs) || 'undef') eq 'ARRAY';
  923. push(@$runs,@$initial_runs);
  924. $offset++;
  925. }
  926. return $runs;
  927. }
  928. =head2 B<getRunsPaginated (project_id,limit,offset,filters)>
  929. Get some runs for specified project.
  930. =over 4
  931. =item INTEGER C<PROJECT_ID> - ID of parent project
  932. =item INTEGER C<LIMIT> - Number of runs to return.
  933. =item INTEGER C<OFFSET> - Page of runs to return.
  934. =item HASHREF C<FILTERS> - (optional) other filters to apply to the requests other than limit/offset. See getRuns for more information.
  935. =back
  936. Returns ARRAYREF of run definition HASHREFs.
  937. $someRuns = $tr->getRunsPaginated(6969,22,4);
  938. =cut
  939. sub getRunsPaginated {
  940. state $check = compile(Object, Int, Optional[Maybe[Int]], Optional[Maybe[Int]], Optional[Maybe[HashRef]]);
  941. my ($self,$project_id,$limit,$offset,$filters) = $check->(@_);
  942. confess("Limit greater than ".$self->{'global_limit'}) if $limit > $self->{'global_limit'};
  943. my $apiurl = "index.php?/api/v2/get_runs/$project_id";
  944. $apiurl .= "&offset=$offset" if defined($offset);
  945. $apiurl .= "&limit=$limit" if $limit; #You have problems if you want 0 results
  946. $apiurl .= _convert_filters_to_string($filters);
  947. my $response = $self->_doRequest($apiurl);
  948. return -500 if !$response || (reftype($response) || 'undef') ne 'HASH';
  949. my $runs = $response->{'runs'};
  950. return -500 if !$runs || (reftype($runs) || 'undef') ne 'ARRAY';
  951. return $runs;
  952. }
  953. =head2 B<getRunByName (project_id,name)>
  954. Gets run by name.
  955. =over 4
  956. =item INTEGER C<PROJECT ID> - ID of parent project.
  957. =item STRING <NAME> - Name of desired run.
  958. =back
  959. Returns run definition HASHREF.
  960. $tr->getRunByName(1,'R2');
  961. =cut
  962. sub getRunByName {
  963. state $check = compile(Object, Int, Str);
  964. my ($self,$project_id,$name) = $check->(@_);
  965. my $runs = $self->getRuns($project_id);
  966. return -500 if !$runs || (reftype($runs) || 'undef') ne 'ARRAY';
  967. foreach my $run (@$runs) {
  968. return $run if $run->{'name'} eq $name;
  969. }
  970. return 0;
  971. }
  972. =head2 B<getRunByID (run_id)>
  973. Gets run by ID.
  974. =over 4
  975. =item INTEGER C<RUN ID> - ID of desired run.
  976. =back
  977. Returns run definition HASHREF.
  978. $tr->getRunByID(7779311);
  979. =cut
  980. sub getRunByID {
  981. state $check = compile(Object, Int);
  982. my ($self,$run_id) = $check->(@_);
  983. return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
  984. }
  985. =head2 B<closeRun (run_id)>
  986. Close the specified run.
  987. =over 4
  988. =item INTEGER C<RUN ID> - ID of desired run.
  989. =back
  990. Returns run definition HASHREF on success, false on failure.
  991. $tr->closeRun(90210);
  992. =cut
  993. sub closeRun {
  994. state $check = compile(Object, Int);
  995. my ($self,$run_id) = $check->(@_);
  996. return $self->_doRequest("index.php?/api/v2/close_run/$run_id",'POST');
  997. }
  998. =head2 B<getRunSummary(runs)>
  999. Returns array of hashrefs describing the # of tests in the run(s) with the available statuses.
  1000. Translates custom_statuses into their system names for you.
  1001. =over 4
  1002. =item ARRAY C<RUNS> - runs obtained from getRun* or getChildRun* methods.
  1003. =back
  1004. Returns ARRAY of run HASHREFs with the added key 'run_status' holding a hashref where status_name => count.
  1005. $tr->getRunSummary($run,$run2);
  1006. =cut
  1007. sub getRunSummary {
  1008. state $check = compile(Object, slurpy ArrayRef[HashRef]);
  1009. my ($self,$runs) = $check->(@_);
  1010. confess("At least one run must be passed!") unless scalar(@$runs);
  1011. #Translate custom statuses
  1012. my $statuses = $self->getPossibleTestStatuses();
  1013. my %shash;
  1014. #XXX so, they do these tricks with the status names, see...so map the counts to their relevant status ids.
  1015. @shash{map { ( $_->{'id'} < 6 ) ? $_->{'name'}."_count" : "custom_status".($_->{'id'} - 5)."_count" } @$statuses } = map { $_->{'id'} } @$statuses;
  1016. my @sname;
  1017. #Create listing of keys/values
  1018. @$runs = map {
  1019. my $run = $_;
  1020. @{$run->{statuses}}{grep {$_ =~ m/_count$/} keys(%$run)} = grep {$_ =~ m/_count$/} keys(%$run);
  1021. foreach my $status (keys(%{$run->{'statuses'}})) {
  1022. next if !exists($shash{$status});
  1023. @sname = grep {exists($shash{$status}) && $_->{'id'} == $shash{$status}} @$statuses;
  1024. $run->{'statuses_clean'}->{$sname[0]->{'label'}} = $run->{$status};
  1025. }
  1026. $run;
  1027. } @$runs;
  1028. return map { {'id' => $_->{'id'}, 'name' => $_->{'name'}, 'run_status' => $_->{'statuses_clean'}, 'config_ids' => $_->{'config_ids'} } } @$runs;
  1029. }
  1030. =head2 B<getRunResults(run_id)>
  1031. Returns array of hashrefs describing the results of the run.
  1032. Warning: This only returns the most recent results of a run.
  1033. If you want to know about the tortured journey a test may have taken to get to it's final status,
  1034. you will need to use getTestResults.
  1035. =over 4
  1036. =item INTEGER C<RUN_ID> - Relevant Run's ID.
  1037. =back
  1038. =cut
  1039. sub getRunResults {
  1040. state $check = compile(Object, Int, Optional[Maybe[HashRef]]);
  1041. my ($self,$run_id, $filters) = $check->(@_);
  1042. my $initial_results = $self->getRunResultsPaginated($run_id,$self->{'global_limit'},undef,$filters);
  1043. return $initial_results unless (reftype($initial_results) || 'undef') eq 'ARRAY';
  1044. my $results = [];
  1045. push(@$results,@$initial_results);
  1046. my $offset = 1;
  1047. while (scalar(@$initial_results) == $self->{'global_limit'}) {
  1048. $initial_results = $self->getRunResultsPaginated($run_id,$self->{'global_limit'},($self->{'global_limit'} * $offset),$filters);
  1049. return $initial_results unless (reftype($initial_results) || 'undef') eq 'ARRAY';
  1050. push(@$results,@$initial_results);
  1051. $offset++;
  1052. }
  1053. return $results;
  1054. }
  1055. =head2 B<getRunResultsPaginated(run_id,limit,offset,filters)>
  1056. =cut
  1057. sub getRunResultsPaginated {
  1058. state $check = compile(Object, Int, Optional[Maybe[Int]], Optional[Maybe[Int]], Optional[Maybe[HashRef]]);
  1059. my ($self,$run_id,$limit,$offset,$filters) = $check->(@_);
  1060. confess("Limit greater than ".$self->{'global_limit'}) if $limit > $self->{'global_limit'};
  1061. my $apiurl = "index.php?/api/v2/get_results_for_run/$run_id";
  1062. $apiurl .= "&offset=$offset" if defined($offset);
  1063. $apiurl .= "&limit=$limit" if $limit; #You have problems if you want 0 results
  1064. $apiurl .= _convert_filters_to_string($filters);
  1065. my $response = $self->_doRequest($apiurl);
  1066. return $response->{'results'};
  1067. }
  1068. =head1 RUN AS CHILD OF PLAN METHODS
  1069. =head2 B<getChildRuns(plan)>
  1070. Extract the child runs from a plan. Convenient, as the structure of this hash is deep, and correct error handling can be tedious.
  1071. =over 4
  1072. =item HASHREF C<PLAN> - Test Plan definition HASHREF returned by any of the PLAN methods below.
  1073. =back
  1074. Returns ARRAYREF of run definition HASHREFs. Returns 0 upon failure to extract the data.
  1075. =cut
  1076. sub getChildRuns {
  1077. state $check = compile(Object, HashRef);
  1078. my ($self,$plan) = $check->(@_);
  1079. return 0 unless defined($plan->{'entries'}) && (reftype($plan->{'entries'}) || 'undef') eq 'ARRAY';
  1080. my $entries = $plan->{'entries'};
  1081. my $plans = [];
  1082. foreach my $entry (@$entries) {
  1083. push(@$plans,@{$entry->{'runs'}}) if defined($entry->{'runs'}) && ((reftype($entry->{'runs'}) || 'undef') eq 'ARRAY')
  1084. }
  1085. return $plans;
  1086. }
  1087. =head2 B<getChildRunByName(plan,name,configurations,testsuite_id)>
  1088. =over 4
  1089. =item HASHREF C<PLAN> - Test Plan definition HASHREF returned by any of the PLAN methods below.
  1090. =item STRING C<NAME> - Name of run to search for within plan.
  1091. =item ARRAYREF C<CONFIGURATIONS> (optional) - Names of configurations to filter runs by.
  1092. =item INTEGER C<TESTSUITE_ID> (optional) - Filter by the provided Testsuite ID. Helpful for when child runs have duplicate names, but are from differing testsuites.
  1093. =back
  1094. Returns run definition HASHREF, or false if no such run is found.
  1095. Convenience method using getChildRuns.
  1096. Will throw a fatal error if one or more of the configurations passed does not exist in the project.
  1097. =cut
  1098. sub getChildRunByName {
  1099. state $check = compile(Object, HashRef, Str, Optional[Maybe[ArrayRef[Str]]], Optional[Maybe[Int]]);
  1100. my ($self,$plan,$name,$configurations,$testsuite_id) = $check->(@_);
  1101. my $runs = $self->getChildRuns($plan);
  1102. @$runs = grep {$_->{suite_id} == $testsuite_id} @$runs if $testsuite_id;
  1103. return 0 if !$runs;
  1104. my @pconfigs = ();
  1105. #Figure out desired config IDs
  1106. if (defined $configurations) {
  1107. my $avail_configs = $self->getConfigurations($plan->{'project_id'});
  1108. my ($cname);
  1109. @pconfigs = map {$_->{'id'}} grep { $cname = $_->{'name'}; grep {$_ eq $cname} @$configurations } @$avail_configs; #Get a list of IDs from the names passed
  1110. }
  1111. confess("One or more configurations passed does not exist in your project!") if defined($configurations) && (scalar(@pconfigs) != scalar(@$configurations));
  1112. my $found;
  1113. foreach my $run (@$runs) {
  1114. next if $run->{name} ne $name;
  1115. next if scalar(@pconfigs) != scalar(@{$run->{'config_ids'}});
  1116. #Compare run config IDs against desired, invalidate run if all conditions not satisfied
  1117. $found = 0;
  1118. foreach my $cid (@{$run->{'config_ids'}}) {
  1119. $found++ if grep {$_ == $cid} @pconfigs;
  1120. }
  1121. return $run if $found == scalar(@{$run->{'config_ids'}});
  1122. }
  1123. return 0;
  1124. }
  1125. =head1 PLAN METHODS
  1126. =head2 B<createPlan (project_id,name,description,milestone_id,entries)>
  1127. Create a test plan.
  1128. =over 4
  1129. =item INTEGER C<PROJECT ID> - ID of parent project.
  1130. =item STRING C<NAME> - Name of plan
  1131. =item STRING C<DESCRIPTION> (optional) - Description of plan
  1132. =item INTEGER C<MILESTONE_ID> (optional) - ID of milestone
  1133. =item ARRAYREF C<ENTRIES> (optional) - New Runs to initially populate the plan with -- See TestRail API documentation for more advanced inputs here.
  1134. =back
  1135. Returns test plan definition HASHREF, or false on failure.
  1136. $entries = [{
  1137. suite_id => 345,
  1138. include_all => 1,
  1139. assignedto_id => 1
  1140. }];
  1141. $tr->createPlan(1,'Gosplan','Robo-Signed Soviet 5-year plan',22,$entries);
  1142. =cut
  1143. sub createPlan {
  1144. state $check = compile(Object, Int, Str, Optional[Maybe[Str]], Optional[Maybe[Int]], Optional[Maybe[ArrayRef[HashRef]]]);
  1145. my ($self,$project_id,$name,$desc,$milestone_id,$entries) = $check->(@_);
  1146. my $stuff = {
  1147. name => $name,
  1148. description => $desc,
  1149. milestone_id => $milestone_id,
  1150. entries => $entries
  1151. };
  1152. return $self->_doRequest("index.php?/api/v2/add_plan/$project_id",'POST',$stuff);
  1153. }
  1154. =head2 B<deletePlan (plan_id)>
  1155. Deletes specified plan.
  1156. =over 4
  1157. =item INTEGER C<PLAN ID> - ID of plan to delete.
  1158. =back
  1159. Returns BOOLEAN.
  1160. $tr->deletePlan(8675309);
  1161. =cut
  1162. sub deletePlan {
  1163. state $check = compile(Object, Int);
  1164. my ($self,$plan_id) = $check->(@_);
  1165. return $self->_doRequest("index.php?/api/v2/delete_plan/$plan_id",'POST');
  1166. }
  1167. =head2 B<getPlans (project_id,filters)>
  1168. Gets all test plans in specified project.
  1169. Like getRuns, must make multiple HTTP requests when the number of results exceeds 250.
  1170. =over 4
  1171. =item INTEGER C<PROJECT ID> - ID of parent project.
  1172. =item HASHREF C<FILTERS> - (optional) dictionary of filters, with keys corresponding to the documented filters for get_plans (other than limit/offset).
  1173. =back
  1174. Returns ARRAYREF of all plan definition HASHREFs in a project.
  1175. $tr->getPlans(8);
  1176. Does not contain any information about child test runs.
  1177. Use getPlanByID or getPlanByName if you want that, in particular if you are interested in using getChildRunByName.
  1178. Possible filters:
  1179. =over 4
  1180. =item created_after (UNIX timestamp)
  1181. =item created_before (UNIX timestamp)
  1182. =item created_by (csv of ints) IDs of users plans were created by
  1183. =item is_completed (bool)
  1184. =item milestone_id (csv of ints) IDs of milestone assigned to plans
  1185. =back
  1186. =cut
  1187. sub getPlans {
  1188. state $check = compile(Object, Int, Optional[Maybe[HashRef]]);
  1189. my ($self,$project_id,$filters) = $check->(@_);
  1190. my $initial_plans = $self->getPlansPaginated($project_id,$self->{'global_limit'},0,$filters);
  1191. return $initial_plans unless (reftype($initial_plans) || 'undef') eq 'ARRAY';
  1192. my $plans = [];
  1193. push(@$plans,@$initial_plans);
  1194. my $offset = 1;
  1195. while (scalar(@$initial_plans) == $self->{'global_limit'}) {
  1196. $initial_plans = $self->getPlansPaginated($project_id,$self->{'global_limit'},($self->{'global_limit'} * $offset),$filters);
  1197. return $initial_plans unless (reftype($initial_plans) || 'undef') eq 'ARRAY';
  1198. push(@$plans,@$initial_plans);
  1199. $offset++;
  1200. }
  1201. return $plans;
  1202. }
  1203. =head2 B<getPlansPaginated (project_id,limit,offset,filters)>
  1204. Get some plans for specified project.
  1205. =over 4
  1206. =item INTEGER C<PROJECT_ID> - ID of parent project
  1207. =item INTEGER C<LIMIT> - Number of plans to return.
  1208. =item INTEGER C<OFFSET> - Page of plans to return.
  1209. =item HASHREF C<FILTERS> - (optional) other filters to apply to the requests (other than limit/offset). See getPlans for more information.
  1210. =back
  1211. Returns ARRAYREF of plan definition HASHREFs.
  1212. $someRuns = $tr->getPlansPaginated(6969,222,44);
  1213. =cut
  1214. sub getPlansPaginated {
  1215. state $check = compile(Object, Int, Optional[Maybe[Int]], Optional[Maybe[Int]], Optional[Maybe[HashRef]]);
  1216. my ($self,$project_id,$limit,$offset,$filters) = $check->(@_);
  1217. confess("Limit greater than ".$self->{'global_limit'}) if $limit > $self->{'global_limit'};
  1218. my $apiurl = "index.php?/api/v2/get_plans/$project_id";
  1219. $apiurl .= "&offset=$offset" if defined($offset);
  1220. $apiurl .= "&limit=$limit" if $limit; #You have problems if you want 0 results
  1221. $apiurl .= _convert_filters_to_string($filters);
  1222. my $response = $self->_doRequest($apiurl);
  1223. return -500 if !$response || (reftype($response) || 'undef') ne 'HASH';
  1224. my $plans = $response->{'plans'};
  1225. return -500 if !$plans || (reftype($plans) || 'undef') ne 'ARRAY';
  1226. return $plans;
  1227. }
  1228. =head2 B<getPlanByName (project_id,name)>
  1229. Gets specified plan by name.
  1230. =over 4
  1231. =item INTEGER C<PROJECT ID> - ID of parent project.
  1232. =item STRING C<NAME> - Name of test plan.
  1233. =back
  1234. Returns plan definition HASHREF.
  1235. $tr->getPlanByName(8,'GosPlan');
  1236. =cut
  1237. sub getPlanByName {
  1238. state $check = compile(Object, Int, Str);
  1239. my ($self,$project_id,$name) = $check->(@_);
  1240. my $plans = $self->getPlans($project_id);
  1241. return -500 if !$plans || (reftype($plans) || 'undef') ne 'ARRAY';
  1242. foreach my $plan (@$plans) {
  1243. if ($plan->{'name'} eq $name) {
  1244. return $self->getPlanByID($plan->{'id'});
  1245. }
  1246. }
  1247. return 0;
  1248. }
  1249. =head2 B<getPlanByID (plan_id)>
  1250. Gets specified plan by ID.
  1251. =over 4
  1252. =item INTEGER C<PLAN ID> - ID of plan.
  1253. =back
  1254. Returns plan definition HASHREF.
  1255. $tr->getPlanByID(2);
  1256. =cut
  1257. sub getPlanByID {
  1258. state $check = compile(Object, Int);
  1259. my ($self,$plan_id) = $check->(@_);
  1260. return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
  1261. }
  1262. =head2 B<getPlanSummary(plan_ID)>
  1263. Returns hashref describing the various pass, fail, etc. percentages for tests in the plan.
  1264. The 'totals' key has total cases in each status ('status' => count)
  1265. The 'percentages' key has the same, but as a percentage of the total.
  1266. =over 4
  1267. =item SCALAR C<plan_ID> - ID of your test plan.
  1268. =back
  1269. $tr->getPlanSummary($plan_id);
  1270. =cut
  1271. sub getPlanSummary {
  1272. state $check = compile(Object, Int);
  1273. my ($self,$plan_id) = $check->(@_);
  1274. my $runs = $self->getPlanByID( $plan_id );
  1275. $runs = $self->getChildRuns( $runs );
  1276. @$runs = $self->getRunSummary(@{$runs});
  1277. my $total_sum = 0;
  1278. my $ret = { plan => $plan_id };
  1279. #Compile totals
  1280. foreach my $summary ( @$runs ) {
  1281. my @elems = keys( %{ $summary->{'run_status'} } );
  1282. foreach my $key (@elems) {
  1283. $ret->{'totals'}->{$key} = 0 if !defined $ret->{'totals'}->{$key};
  1284. $ret->{'totals'}->{$key} += $summary->{'run_status'}->{$key};
  1285. $total_sum += $summary->{'run_status'}->{$key};
  1286. }
  1287. }
  1288. #Compile percentages
  1289. foreach my $key (keys(%{$ret->{'totals'}})) {
  1290. next if grep {$_ eq $key} qw{plan configs percentages};
  1291. $ret->{"percentages"}->{$key} = sprintf( "%.2f%%", ( $ret->{'totals'}->{$key} / $total_sum ) * 100 );
  1292. }
  1293. return $ret;
  1294. }
  1295. =head2 B<createRunInPlan (plan_id,suite_id,name,assigned_to_id,config_ids,case_ids)>
  1296. Create a run in a plan.
  1297. =over 4
  1298. =item INTEGER C<PLAN ID> - ID of parent project.
  1299. =item INTEGER C<SUITE ID> - ID of suite to base run on
  1300. =item STRING C<NAME> - Name of run
  1301. =item INTEGER C<ASSIGNED TO ID> (optional) - User to assign the run to
  1302. =item ARRAYREF C<CONFIG IDS> (optional) - Array of Configuration IDs (see getConfigurations) to apply to the created run
  1303. =item ARRAYREF C<CASE IDS> (optional) - Array of case IDs in case you don't want to use the whole testsuite when making the build.
  1304. =back
  1305. Returns run definition HASHREF.
  1306. $tr->createRun(1,1345,'PlannedRun',3,[1,4,77],[3,4,5,6]);
  1307. =cut
  1308. #If you pass an array of case ids, it implies include_all is false
  1309. sub createRunInPlan {
  1310. state $check = compile(Object, Int, Int, Str, Optional[Maybe[Int]], Optional[Maybe[ArrayRef[Int]]], Optional[Maybe[ArrayRef[Int]]]);
  1311. my ($self,$plan_id,$suite_id,$name,$assignedto_id,$config_ids,$case_ids) = $check->(@_);
  1312. my $runs = [
  1313. {
  1314. config_ids => $config_ids,
  1315. include_all => defined($case_ids) ? 0 : 1,
  1316. case_ids => $case_ids
  1317. }
  1318. ];
  1319. my $stuff = {
  1320. suite_id => $suite_id,
  1321. name => $name,
  1322. assignedto_id => $assignedto_id,
  1323. include_all => defined($case_ids) ? 0 : 1,
  1324. case_ids => $case_ids,
  1325. config_ids => $config_ids,
  1326. runs => $runs
  1327. };
  1328. return $self->_doRequest("index.php?/api/v2/add_plan_entry/$plan_id",'POST',$stuff);
  1329. }
  1330. =head2 B<closePlan (plan_id)>
  1331. Close the specified plan.
  1332. =over 4
  1333. =item INTEGER C<PLAN ID> - ID of desired plan.
  1334. =back
  1335. Returns plan definition HASHREF on success, false on failure.
  1336. $tr->closePlan(75020);
  1337. =cut
  1338. sub closePlan {
  1339. state $check = compile(Object, Int);
  1340. my ($self,$plan_id) = $check->(@_);
  1341. return $self->_doRequest("index.php?/api/v2/close_plan/$plan_id",'POST');
  1342. }
  1343. =head1 MILESTONE METHODS
  1344. =head2 B<createMilestone (project_id,name,description,due_on)>
  1345. Create a milestone.
  1346. =over 4
  1347. =item INTEGER C<PROJECT ID> - ID of parent project.
  1348. =item STRING C<NAME> - Name of milestone
  1349. =item STRING C<DESCRIPTION> (optional) - Description of milestone
  1350. =item INTEGER C<DUE_ON> - Date at which milestone should be completed. Unix Timestamp.
  1351. =back
  1352. Returns milestone definition HASHREF, or false on failure.
  1353. $tr->createMilestone(1,'Patriotic victory of world perlism','Accomplish by Robo-Signed Soviet 5-year plan',time()+157788000);
  1354. =cut
  1355. sub createMilestone {
  1356. state $check = compile(Object, Int, Str, Optional[Maybe[Str]], Optional[Maybe[Int]]);
  1357. my ($self,$project_id,$name,$desc,$due_on) = $check->(@_);
  1358. my $stuff = {
  1359. name => $name,
  1360. description => $desc,
  1361. due_on => $due_on # unix timestamp
  1362. };
  1363. return $self->_doRequest("index.php?/api/v2/add_milestone/$project_id",'POST',$stuff);
  1364. }
  1365. =head2 B<deleteMilestone (milestone_id)>
  1366. Deletes specified milestone.
  1367. =over 4
  1368. =item INTEGER C<MILESTONE ID> - ID of milestone to delete.
  1369. =back
  1370. Returns BOOLEAN.
  1371. $tr->deleteMilestone(86);
  1372. =cut
  1373. sub deleteMilestone {
  1374. state $check = compile(Object, Int);
  1375. my ($self,$milestone_id) = $check->(@_);
  1376. return $self->_doRequest("index.php?/api/v2/delete_milestone/$milestone_id",'POST');
  1377. }
  1378. =head2 B<getMilestones (project_id,filters)>
  1379. Get milestones for some project.
  1380. =over 4
  1381. =item INTEGER C<PROJECT ID> - ID of parent project.
  1382. =item HASHREF C<FILTERS> (optional) - HASHREF describing parameters to filter milestones by.
  1383. =back
  1384. See:
  1385. L<https://www.gurock.com/testrail/docs/api/reference/milestones#getmilestones>
  1386. for details as to the allowable filter keys.
  1387. Returns ARRAYREF of milestone definition HASHREFs.
  1388. $tr->getMilestones(8);
  1389. =cut
  1390. sub getMilestones {
  1391. state $check = compile(Object, Int, Optional[Maybe[HashRef]]);
  1392. my ($self,$project_id, $filters) = $check->(@_);
  1393. my $response = $self->_doRequest("index.php?/api/v2/get_milestones/$project_id" . _convert_filters_to_string($filters));
  1394. return -500 if !$response || (reftype($response) || 'undef') ne 'HASH';
  1395. my $milestones = $response->{'milestones'};
  1396. return -500 if !$milestones || (reftype($milestones) || 'undef') ne 'ARRAY';
  1397. return $milestones;
  1398. }
  1399. =head2 B<getMilestoneByName (project_id,name)>
  1400. Gets specified milestone by name.
  1401. =over 4
  1402. =item INTEGER C<PROJECT ID> - ID of parent project.
  1403. =item STRING C<NAME> - Name of milestone.
  1404. =back
  1405. Returns milestone definition HASHREF.
  1406. $tr->getMilestoneByName(8,'whee');
  1407. =cut
  1408. sub getMilestoneByName {
  1409. state $check = compile(Object, Int, Str);
  1410. my ($self,$project_id,$name) = $check->(@_);
  1411. my $milestones = $self->getMilestones($project_id);
  1412. return -500 if !$milestones || (reftype($milestones) || 'undef') ne 'ARRAY';
  1413. foreach my $milestone (@$milestones) {
  1414. return $milestone if $milestone->{'name'} eq $name;
  1415. }
  1416. return 0;
  1417. }
  1418. =head2 B<getMilestoneByID (milestone_id)>
  1419. Gets specified milestone by ID.
  1420. =over 4
  1421. =item INTEGER C<MILESTONE ID> - ID of milestone.
  1422. =back
  1423. Returns milestone definition HASHREF.
  1424. $tr->getMilestoneByID(2);
  1425. =cut
  1426. sub getMilestoneByID {
  1427. state $check = compile(Object, Int);
  1428. my ($self,$milestone_id) = $check->(@_);
  1429. return $self->_doRequest("index.php?/api/v2/get_milestone/$milestone_id");
  1430. }
  1431. =head1 TEST METHODS
  1432. =head2 B<getTests (run_id,status_ids,assignedto_ids)>
  1433. Get tests for some run. Optionally filter by provided status_ids and assigned_to ids.
  1434. =over 4
  1435. =item INTEGER C<RUN ID> - ID of parent run.
  1436. =item ARRAYREF C<STATUS IDS> (optional) - IDs of relevant test statuses to filter by. Get with getPossibleTestStatuses.
  1437. =item ARRAYREF C<ASSIGNEDTO IDS> (optional) - IDs of users assigned to test to filter by. Get with getUsers.
  1438. =back
  1439. Returns ARRAYREF of test definition HASHREFs.
  1440. $tr->getTests(8,[1,2,3],[2]);
  1441. =cut
  1442. sub getTests {
  1443. state $check = compile(Object, Int, Optional[Maybe[ArrayRef[Int]]], Optional[Maybe[ArrayRef[Int]]]);
  1444. my ($self,$run_id,$status_ids,$assignedto_ids) = $check->(@_);
  1445. my $query_string = '';
  1446. $query_string = '&status_id='.join(',',@$status_ids) if defined($status_ids) && scalar(@$status_ids);
  1447. my $response = $self->_doRequest("index.php?/api/v2/get_tests/$run_id$query_string");
  1448. return -500 if !$response || (reftype($response) || 'undef') ne 'HASH';
  1449. my $results = $response->{'tests'};
  1450. return -500 if !$results || (reftype($results) || 'undef') ne 'ARRAY';
  1451. @$results = grep {my $aid = $_->{'assignedto_id'}; grep {defined($aid) && $aid == $_} @$assignedto_ids} @$results if defined($assignedto_ids) && scalar(@$assignedto_ids);
  1452. #Cache stuff for getTestByName
  1453. $self->{tests_cache} //= {};
  1454. $self->{tests_cache}->{$run_id} = $results;
  1455. return clone($results);
  1456. }
  1457. =head2 B<getTestByName (run_id,name)>
  1458. Gets specified test by name.
  1459. This is done by getting the list of all tests in the run and then picking out the relevant test.
  1460. As such, for efficiency the list of tests is cached.
  1461. The cache may be refreshed, or restricted by running getTests (with optional restrictions, such as assignedto_ids, etc).
  1462. =over 4
  1463. =item INTEGER C<RUN ID> - ID of parent run.
  1464. =item STRING C<NAME> - Name of milestone.
  1465. =back
  1466. Returns test definition HASHREF.
  1467. $tr->getTestByName(36,'wheeTest');
  1468. =cut
  1469. sub getTestByName {
  1470. state $check = compile(Object, Int, Str);
  1471. my ($self,$run_id,$name) = $check->(@_);
  1472. $self->{tests_cache} //= {};
  1473. my $tests = $self->{tests_cache}->{$run_id};
  1474. $tests = $self->getTests($run_id) if !$tests;
  1475. return -500 if !$tests || (reftype($tests) || 'undef') ne 'ARRAY';
  1476. foreach my $test (@$tests) {
  1477. return $test if $test->{'title'} eq $name;
  1478. }
  1479. return 0;
  1480. }
  1481. =head2 B<getTestByID (test_id)>
  1482. Gets specified test by ID.
  1483. =over 4
  1484. =item INTEGER C<TEST ID> - ID of test.
  1485. =back
  1486. Returns test definition HASHREF.
  1487. $tr->getTestByID(222222);
  1488. =cut
  1489. sub getTestByID {
  1490. state $check = compile(Object, Int);
  1491. my ($self,$test_id) = $check->(@_);
  1492. return $self->_doRequest("index.php?/api/v2/get_test/$test_id");
  1493. }
  1494. =head2 B<getTestResultFields()>
  1495. Gets custom fields that can be set for tests.
  1496. Returns ARRAYREF of result definition HASHREFs.
  1497. =cut
  1498. sub getTestResultFields {
  1499. state $check = compile(Object);
  1500. my ($self) = $check->(@_);
  1501. return $self->{'tr_fields'} if defined($self->{'tr_fields'}); #cache
  1502. $self->{'tr_fields'} = $self->_doRequest('index.php?/api/v2/get_result_fields');
  1503. return $self->{'tr_fields'};
  1504. }
  1505. =head2 B<getTestResultFieldByName(SYSTEM_NAME,PROJECT_ID)>
  1506. Gets a test result field by it's system name. Optionally filter by project ID.
  1507. =over 4
  1508. =item B<SYSTEM NAME> - STRING: system name of a result field.
  1509. =item B<PROJECT ID> - INTEGER (optional): Filter by whether or not the field is enabled for said project
  1510. =back
  1511. Returns a value less than 0 if unsuccessful.
  1512. =cut
  1513. sub getTestResultFieldByName {
  1514. state $check = compile(Object, Str, Optional[Maybe[Int]]);
  1515. my ($self,$system_name,$project_id) = $check->(@_);
  1516. my @candidates = grep { $_->{'name'} eq $system_name} @{$self->getTestResultFields()};
  1517. return 0 if !scalar(@candidates); #No such name
  1518. return -1 if ref($candidates[0]) ne 'HASH';
  1519. return -2 if ref($candidates[0]->{'configs'}) ne 'ARRAY' && !scalar(@{$candidates[0]->{'configs'}}); #bogofilter
  1520. #Give it to the user
  1521. my $ret = $candidates[0]; #copy/save for later
  1522. return $ret if !defined($project_id);
  1523. #Filter by project ID
  1524. foreach my $config (@{$candidates[0]->{'configs'}}) {
  1525. return $ret if ( grep { $_ == $project_id} @{ $config->{'context'}->{'project_ids'} } )
  1526. }
  1527. return -3;
  1528. }
  1529. =head2 B<getPossibleTestStatuses()>
  1530. Gets all possible statuses a test can be set to.
  1531. Returns ARRAYREF of status definition HASHREFs.
  1532. Caches the result for the lifetime of the TestRail::API object.
  1533. =cut
  1534. sub getPossibleTestStatuses {
  1535. state $check = compile(Object);
  1536. my ($self) = $check->(@_);
  1537. return $self->{'status_cache'} if $self->{'status_cache'};
  1538. $self->{'status_cache'} = $self->_doRequest('index.php?/api/v2/get_statuses');
  1539. return clone $self->{'status_cache'};
  1540. }
  1541. =head2 statusNamesToIds(names)
  1542. Convenience method to translate a list of statuses to TestRail status IDs.
  1543. The names referred to here are 'internal names' rather than the labels shown in TestRail.
  1544. =over 4
  1545. =item ARRAY C<NAMES> - Array of status names to translate to IDs.
  1546. =back
  1547. Returns ARRAY of status IDs in the same order as the status names passed.
  1548. Throws an exception in the case of one (or more) of the names not corresponding to a valid test status.
  1549. =cut
  1550. sub statusNamesToIds {
  1551. my ($self,@names) = @_;
  1552. return _X_in_my_Y($self,$self->getPossibleTestStatuses(),'id',@names);
  1553. };
  1554. =head2 statusNamesToLabels(names)
  1555. Convenience method to translate a list of statuses to TestRail status labels (the 'nice' form of status names).
  1556. This is useful when interacting with getRunSummary or getPlanSummary, which uses these labels as hash keys.
  1557. =over 4
  1558. =item ARRAY C<NAMES> - Array of status names to translate to IDs.
  1559. =back
  1560. Returns ARRAY of status labels in the same order as the status names passed.
  1561. Throws an exception in the case of one (or more) of the names not corresponding to a valid test status.
  1562. =cut
  1563. sub statusNamesToLabels {
  1564. my ($self,@names) = @_;
  1565. return _X_in_my_Y($self,$self->getPossibleTestStatuses(),'label',@names);
  1566. };
  1567. # Reduce code duplication with internal methods?
  1568. # It's more likely than you think
  1569. # Free PC check @ cpan.org
  1570. sub _X_in_my_Y {
  1571. state $check = compile(Object, ArrayRef, Str, slurpy ArrayRef[Str]);
  1572. my ($self,$search_arr,$key,$names) = $check->(@_);
  1573. my @ret;
  1574. foreach my $name (@$names) {
  1575. foreach my $member (@$search_arr) {
  1576. if ($member->{'name'} eq $name) {
  1577. push @ret, $member->{$key};
  1578. last;
  1579. }
  1580. }
  1581. }
  1582. confess("One or more names provided does not exist in TestRail.") unless scalar(@$names) == scalar(@ret);
  1583. return @ret;
  1584. }
  1585. =head2 B<createTestResults(test_id,status_id,comment,options,custom_options)>
  1586. Creates a result entry for a test.
  1587. =over 4
  1588. =item INTEGER C<TEST_ID> - ID of desired test
  1589. =item INTEGER C<STATUS_ID> - ID of desired test result status
  1590. =item STRING C<COMMENT> (optional) - Any comments about this result
  1591. =item HASHREF C<OPTIONS> (optional) - Various "Baked-In" options that can be set for test results. See TR docs for more information.
  1592. =item HASHREF C<CUSTOM OPTIONS> (optional) - Options to set for custom fields. See buildStepResults for a simple way to post up custom steps.
  1593. =back
  1594. Returns result definition HASHREF.
  1595. $options = {
  1596. elapsed => '30m 22s',
  1597. defects => ['TSR-3','BOOM-44'],
  1598. version => '6969'
  1599. };
  1600. $custom_options = {
  1601. step_results => [
  1602. {
  1603. content => 'Step 1',
  1604. expected => "Bought Groceries",
  1605. actual => "No Dinero!",
  1606. status_id => 2
  1607. },
  1608. {
  1609. content => 'Step 2',
  1610. expected => 'Ate Dinner',
  1611. actual => 'Went Hungry',
  1612. status_id => 2
  1613. }
  1614. ]
  1615. };
  1616. $res = $tr->createTestResults(1,2,'Test failed because it was all like WAAAAAAA when I poked it',$options,$custom_options);
  1617. =cut
  1618. sub createTestResults {
  1619. state $check = compile(Object, Int, Int, Optional[Maybe[Str]], Optional[Maybe[HashRef]], Optional[Maybe[HashRef]]);
  1620. my ($self,$test_id,$status_id,$comment,$opts,$custom_fields) = $check->(@_);
  1621. my $stuff = {
  1622. status_id => $status_id,
  1623. comment => $comment
  1624. };
  1625. #Handle options
  1626. if (defined($opts) && reftype($opts) eq 'HASH') {
  1627. $stuff->{'version'} = defined($opts->{'version'}) ? $opts->{'version'} : undef;
  1628. $stuff->{'elapsed'} = defined($opts->{'elapsed'}) ? $opts->{'elapsed'} : undef;
  1629. $stuff->{'defects'} = defined($opts->{'defects'}) ? join(',',@{$opts->{'defects'}}) : undef;
  1630. $stuff->{'assignedto_id'} = defined($opts->{'assignedto_id'}) ? $opts->{'assignedto_id'} : undef;
  1631. }
  1632. #Handle custom fields
  1633. if (defined($custom_fields) && reftype($custom_fields) eq 'HASH') {
  1634. foreach my $field (keys(%$custom_fields)) {
  1635. $stuff->{"custom_$field"} = $custom_fields->{$field};
  1636. }
  1637. }
  1638. return $self->_doRequest("index.php?/api/v2/add_result/$test_id",'POST',$stuff);
  1639. }
  1640. =head2 bulkAddResults(run_id,results)
  1641. Add multiple results to a run, where each result is a HASHREF with keys as outlined in the get_results API call documentation.
  1642. =over 4
  1643. =item INTEGER C<RUN_ID> - ID of desired run to add results to
  1644. =item ARRAYREF C<RESULTS> - Array of result HASHREFs to upload.
  1645. =back
  1646. Returns ARRAYREF of result definition HASHREFs.
  1647. =cut
  1648. sub bulkAddResults {
  1649. state $check = compile(Object, Int, ArrayRef[HashRef]);
  1650. my ($self,$run_id, $results) = $check->(@_);
  1651. return $self->_doRequest("index.php?/api/v2/add_results/$run_id", 'POST', { 'results' => $results });
  1652. }
  1653. =head2 bulkAddResultsByCase(run_id,results)
  1654. Basically the same as bulkAddResults, but instead of a test_id for each entry you use a case_id.
  1655. =cut
  1656. sub bulkAddResultsByCase {
  1657. state $check = compile(Object, Int, ArrayRef[HashRef]);
  1658. my ($self,$run_id, $results) = $check->(@_);
  1659. return $self->_doRequest("index.php?/api/v2/add_results_for_cases/$run_id", 'POST', { 'results' => $results });
  1660. }
  1661. =head2 B<getTestResults(test_id,limit,offset,filters)>
  1662. Get the recorded results for desired test, limiting output to 'limit' entries.
  1663. =over 4
  1664. =item INTEGER C<TEST_ID> - ID of desired test
  1665. =item POSITIVE INTEGER C<LIMIT> (OPTIONAL) - provide no more than this number of results.
  1666. =item INTEGER C<OFFSET> (OPTIONAL) - Offset to begin viewing result set at.
  1667. =item HASHREF C<FILTERS> (optional) - HASHREF describing parameters to filter test results by (other than limit/offset).
  1668. =back
  1669. See:
  1670. L<https://www.gurock.com/testrail/docs/api/reference/results#getresults>
  1671. for details as to the allowable filter keys.
  1672. Returns ARRAYREF of result definition HASHREFs.
  1673. =cut
  1674. sub getTestResults {
  1675. state $check = compile(Object, Int, Optional[Maybe[Int]], Optional[Maybe[Int]], Optional[Maybe[HashRef]]);
  1676. my ($self,$test_id,$limit,$offset,$filters) = $check->(@_);
  1677. my $url = "index.php?/api/v2/get_results/$test_id";
  1678. $url .= "&limit=$limit" if $limit;
  1679. $url .= "&offset=$offset" if defined($offset);
  1680. $url .= _convert_filters_to_string($filters);
  1681. my $response = $self->_doRequest($url);
  1682. return -500 if !$response || (reftype($response) || 'undef') ne 'HASH';
  1683. my $results = $response->{'results'};
  1684. return -500 if !$results || (reftype($results) || 'undef') ne 'ARRAY';
  1685. return $results;
  1686. }
  1687. =head2 B<getResultsForCase(run_id,case_id,limit,offset,filters)>
  1688. Get the recorded results for a test run and case combination., limiting output to 'limit' entries.
  1689. =over 4
  1690. =item INTEGER C<RUN_ID> - ID of desired run
  1691. =item INTEGER C<CASE_ID> - ID of desired case
  1692. =item POSITIVE INTEGER C<LIMIT> (OPTIONAL) - provide no more than this number of results.
  1693. =item INTEGER C<OFFSET> (OPTIONAL) - Offset to begin viewing result set at.
  1694. =item HASHREF C<FILTERS> (optional) - HASHREF describing parameters to filter by (other than limit/offset).
  1695. =back
  1696. See:
  1697. L<https://www.gurock.com/testrail/docs/api/reference/results#getresultsforcase>
  1698. for details as to the allowable filter keys.
  1699. Returns ARRAYREF of result definition HASHREFs.
  1700. =cut
  1701. sub getResultsForCase {
  1702. state $check = compile(Object, Int, Int, Optional[Maybe[Int]], Optional[Maybe[Int]], Optional[Maybe[HashRef]]);
  1703. my ($self,$run_id,$case_id,$limit,$offset,$filters) = $check->(@_);
  1704. my $url = "index.php?/api/v2/get_results_for_case/$run_id/$case_id";
  1705. $url .= "&limit=$limit" if $limit;
  1706. $url .= "&offset=$offset" if defined($offset);
  1707. $url .= _convert_filters_to_string($filters);
  1708. my $response = $self->_doRequest($url);
  1709. return $response->{'results'};
  1710. }
  1711. =head1 CONFIGURATION METHODS
  1712. =head2 B<getConfigurationGroups(project_id)>
  1713. Gets the available configuration groups for a project, with their configurations as children.
  1714. =over 4
  1715. =item INTEGER C<PROJECT_ID> - ID of relevant project
  1716. =back
  1717. Returns ARRAYREF of configuration group definition HASHREFs.
  1718. =cut
  1719. sub getConfigurationGroups {
  1720. state $check = compile(Object, Int);
  1721. my ($self,$project_id) = $check->(@_);
  1722. my $url = "index.php?/api/v2/get_configs/$project_id";
  1723. return $self->_doRequest($url);
  1724. }
  1725. =head2 B<getConfigurationGroupByName(project_id,name)>
  1726. Get the provided configuration group by name.
  1727. Returns false if the configuration group could not be found.
  1728. =cut
  1729. sub getConfigurationGroupByName {
  1730. state $check = compile(Object, Int, Str);
  1731. my ($self,$project_id,$name) = $check->(@_);
  1732. my $cgroups = $self->getConfigurationGroups($project_id);
  1733. return 0 if ref($cgroups) ne 'ARRAY';
  1734. @$cgroups = grep {$_->{'name'} eq $name} @$cgroups;
  1735. return 0 unless scalar(@$cgroups);
  1736. return $cgroups->[0];
  1737. }
  1738. =head2 B<addConfigurationGroup(project_id,name)>
  1739. New in TestRail 5.2.
  1740. Add a configuration group to the specified project.
  1741. =over 4
  1742. =item INTEGER C<PROJECT_ID> - ID of relevant project
  1743. =item STRING C<NAME> - Name for new configuration Group.
  1744. =back
  1745. Returns HASHREF with new configuration group.
  1746. =cut
  1747. sub addConfigurationGroup {
  1748. state $check = compile(Object, Int, Str);
  1749. my ($self,$project_id,$name) = $check->(@_);
  1750. my $url = "index.php?/api/v2/add_config_group/$project_id";
  1751. return $self->_doRequest($url,'POST',{'name' => $name});
  1752. }
  1753. =head2 B<editConfigurationGroup(config_group_id,name)>
  1754. New in TestRail 5.2.
  1755. Change the name of a configuration group.
  1756. =over 4
  1757. =item INTEGER C<CONFIG_GROUP_ID> - ID of relevant configuration group
  1758. =item STRING C<NAME> - Name for new configuration Group.
  1759. =back
  1760. Returns HASHREF with new configuration group.
  1761. =cut
  1762. sub editConfigurationGroup {
  1763. state $check = compile(Object, Int, Str);
  1764. my ($self,$config_group_id,$name) = $check->(@_);
  1765. my $url = "index.php?/api/v2/update_config_group/$config_group_id";
  1766. return $self->_doRequest($url,'POST',{'name' => $name});
  1767. }
  1768. =head2 B<deleteConfigurationGroup(config_group_id)>
  1769. New in TestRail 5.2.
  1770. Delete a configuration group.
  1771. =over 4
  1772. =item INTEGER C<CONFIG_GROUP_ID> - ID of relevant configuration group
  1773. =back
  1774. Returns BOOL.
  1775. =cut
  1776. sub deleteConfigurationGroup {
  1777. state $check = compile(Object, Int);
  1778. my ($self,$config_group_id) = $check->(@_);
  1779. my $url = "index.php?/api/v2/delete_config_group/$config_group_id";
  1780. return $self->_doRequest($url,'POST');
  1781. }
  1782. =head2 B<getConfigurations(project_id)>
  1783. Gets the available configurations for a project.
  1784. Mostly for convenience (no need to write a boilerplate loop over the groups).
  1785. =over 4
  1786. =item INTEGER C<PROJECT_ID> - ID of relevant project
  1787. =back
  1788. Returns ARRAYREF of configuration definition HASHREFs.
  1789. Returns result of getConfigurationGroups (likely -500) in the event that call fails.
  1790. =cut
  1791. sub getConfigurations {
  1792. state $check = compile(Object, Int);
  1793. my ($self,$project_id) = $check->(@_);
  1794. my $cgroups = $self->getConfigurationGroups($project_id);
  1795. my $configs = [];
  1796. return $cgroups unless (reftype($cgroups) || 'undef') eq 'ARRAY';
  1797. foreach my $cfg (@$cgroups) {
  1798. push(@$configs, @{$cfg->{'configs'}});
  1799. }
  1800. return $configs;
  1801. }
  1802. =head2 B<addConfiguration(configuration_group_id,name)>
  1803. New in TestRail 5.2.
  1804. Add a configuration to the specified configuration group.
  1805. =over 4
  1806. =item INTEGER C<CONFIGURATION_GROUP_ID> - ID of relevant configuration group
  1807. =item STRING C<NAME> - Name for new configuration.
  1808. =back
  1809. Returns HASHREF with new configuration.
  1810. =cut
  1811. sub addConfiguration {
  1812. state $check = compile(Object, Int, Str);
  1813. my ($self,$configuration_group_id,$name) = $check->(@_);
  1814. my $url = "index.php?/api/v2/add_config/$configuration_group_id";
  1815. return $self->_doRequest($url,'POST',{'name' => $name});
  1816. }
  1817. =head2 B<editConfiguration(config_id,name)>
  1818. New in TestRail 5.2.
  1819. Change the name of a configuration.
  1820. =over 4
  1821. =item INTEGER C<CONFIG_ID> - ID of relevant configuration.
  1822. =item STRING C<NAME> - New name for configuration.
  1823. =back
  1824. Returns HASHREF with new configuration group.
  1825. =cut
  1826. sub editConfiguration {
  1827. state $check = compile(Object, Int, Str);
  1828. my ($self,$config_id,$name) = $check->(@_);
  1829. my $url = "index.php?/api/v2/update_config/$config_id";
  1830. return $self->_doRequest($url,'POST',{'name' => $name});
  1831. }
  1832. =head2 B<deleteConfiguration(config_id)>
  1833. New in TestRail 5.2.
  1834. Delete a configuration.
  1835. =over 4
  1836. =item INTEGER C<CONFIG_ID> - ID of relevant configuration
  1837. =back
  1838. Returns BOOL.
  1839. =cut
  1840. sub deleteConfiguration {
  1841. state $check = compile(Object, Int);
  1842. my ($self,$config_id) = $check->(@_);
  1843. my $url = "index.php?/api/v2/delete_config/$config_id";
  1844. return $self->_doRequest($url,'POST');
  1845. }
  1846. =head2 B<translateConfigNamesToIds(project_id,configs)>
  1847. Transforms a list of configuration names into a list of config IDs.
  1848. =over 4
  1849. =item INTEGER C<PROJECT_ID> - Relevant project ID for configs.
  1850. =item ARRAY C<CONFIGS> - Array of config names
  1851. =back
  1852. Returns ARRAY of configuration names, with undef values for unknown configuration names.
  1853. =cut
  1854. sub translateConfigNamesToIds {
  1855. my ($self,$project_id,@names) = @_;
  1856. my $configs = $self->getConfigurations($project_id) or confess("Could not determine configurations in provided project.");
  1857. return _X_in_my_Y($self,$configs,'id',@names);
  1858. }
  1859. =head1 REPORT METHODS
  1860. =head2 getReports
  1861. Return the ARRAYREF of reports available for the provided project.
  1862. Requires you to mark a particular report as accessible in the API via the TestRail report interface.
  1863. =over 4
  1864. =item INTEGER C<PROJECT_ID> - Relevant project ID.
  1865. =back
  1866. =cut
  1867. sub getReports {
  1868. state $check = compile(Object, Int);
  1869. my ($self,$project_id) = $check->(@_);
  1870. my $url = "index.php?/api/v2/get_reports/$project_id";
  1871. return $self->_doRequest($url,'GET');
  1872. }
  1873. =head2 runReport
  1874. Compute the provided report using currently available data.
  1875. Returns HASHREF describing URLs to access completed reports.
  1876. =over 4
  1877. =item INTEGER C<REPORT_ID> - Relevant report ID.
  1878. =back
  1879. =cut
  1880. sub runReport {
  1881. state $check = compile(Object, Int);
  1882. my ($self,$report_id) = $check->(@_);
  1883. my $url = "index.php?/api/v2/run_report/$report_id";
  1884. return $self->_doRequest($url,'GET');
  1885. }
  1886. =head1 STATIC METHODS
  1887. =head2 B<buildStepResults(content,expected,actual,status_id)>
  1888. Convenience method to build the stepResult hashes seen in the custom options for getTestResults.
  1889. =over 4
  1890. =item STRING C<CONTENT> (optional) - The step itself.
  1891. =item STRING C<EXPECTED> (optional) - Expected result of test step.
  1892. =item STRING C<ACTUAL> (optional) - Actual result of test step
  1893. =item INTEGER C<STATUS ID> (optional) - Status ID of result
  1894. =back
  1895. =cut
  1896. #Convenience method for building stepResults
  1897. sub buildStepResults {
  1898. state $check = compile(Str, Str, Str, Int);
  1899. my ($content,$expected,$actual,$status_id) = $check->(@_);
  1900. return {
  1901. content => $content,
  1902. expected => $expected,
  1903. actual => $actual,
  1904. status_id => $status_id
  1905. };
  1906. }
  1907. # Convenience method for building filter string from filters Hashref
  1908. sub _convert_filters_to_string {
  1909. state $check = compile(Maybe[HashRef]);
  1910. my ($filters) = $check->(@_);
  1911. $filters //= {};
  1912. my $filter_string = '';
  1913. foreach my $filter (keys(%$filters)) {
  1914. if (ref $filters->{$filter} eq 'ARRAY') {
  1915. $filter_string .= "&$filter=".join(',',@{$filters->{$filter}});
  1916. } else {
  1917. $filter_string .= "&$filter=".$filters->{$filter} if defined($filters->{$filter});
  1918. }
  1919. }
  1920. return $filter_string;
  1921. }
  1922. 1;
  1923. __END__
  1924. =head1 SEE ALSO
  1925. L<HTTP::Request>
  1926. L<LWP::UserAgent>
  1927. L<JSON::MaybeXS>
  1928. L<http://docs.gurock.com/testrail-api2/start>
  1929. =head1 SPECIAL THANKS
  1930. Thanks to cPanel Inc, for graciously funding the creation of this module.