API.pm 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315
  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','testlink.baz.foo');
  7. my $tr = TestRail::API->new($username, $password, $host);
  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.
  13. When the server is not responsive, expect a -500 response, and retry accordingly.
  14. I recommend using the excellent L<Attempt> module for this purpose.
  15. =cut
  16. use 5.010;
  17. use strict;
  18. use warnings;
  19. use Carp qw{cluck confess};
  20. use Scalar::Util qw{reftype looks_like_number};
  21. use Clone 'clone';
  22. use Try::Tiny;
  23. use JSON::MaybeXS 1.001000 ();
  24. use HTTP::Request;
  25. use LWP::UserAgent;
  26. use Data::Validate::URI qw{is_uri};
  27. use List::Util 1.33;
  28. use Encode ();
  29. =head1 CONSTRUCTOR
  30. =head2 B<new (api_url, user, password, encoding, debug)>
  31. Creates new C<TestRail::API> object.
  32. =over 4
  33. =item STRING C<API URL> - base url for your TestRail api server.
  34. =item STRING C<USER> - Your TestRail User.
  35. =item STRING C<PASSWORD> - Your TestRail password, or a valid API key (TestRail 4.2 and above).
  36. =item STRING C<ENCODING> - The character encoding used by the caller. Defaults to 'UTF-8', see L<Encode::Supported> and for supported encodings.
  37. =item BOOLEAN C<DEBUG> (optional) - Print the JSON responses from TL with your requests. Default false.
  38. =back
  39. Returns C<TestRail::API> object if login is successful.
  40. my $tr = TestRail::API->new('http://tr.test/testrail', 'moo','M000000!');
  41. Dies on all communication errors with the TestRail server.
  42. Does not do above checks if debug is passed.
  43. =cut
  44. sub new {
  45. my ($class,$apiurl,$user,$pass,$encoding,$debug) = @_;
  46. confess("Constructor must be called statically, not by an instance") if ref($class);
  47. confess("Invalid URI passed to constructor") if !is_uri($apiurl);
  48. $user //= $ENV{'TESTRAIL_USER'};
  49. $pass //= $ENV{'TESTRAIL_PASSWORD'};
  50. $debug //= 0;
  51. my $self = {
  52. user => $user,
  53. pass => $pass,
  54. apiurl => $apiurl,
  55. debug => $debug,
  56. encoding => $encoding || 'UTF-8',
  57. testtree => [],
  58. flattree => [],
  59. user_cache => [],
  60. type_cache => [],
  61. configurations => {},
  62. tr_fields => undef,
  63. default_request => undef,
  64. global_limit => 250, #Discovered by experimentation
  65. browser => new LWP::UserAgent()
  66. };
  67. #Check chara encoding
  68. $self->{'encoding-nonaliased'} = Encode::resolve_alias($self->{'encoding'});
  69. confess("Invalid encoding alias '".$self->{'encoding'}."' passed, see Encoding::Supported for a list of allowed encodings")
  70. unless $self->{'encoding-nonaliased'};
  71. confess("Invalid encoding '".$self->{'encoding-nonaliased'}."' passed, see Encoding::Supported for a list of allowed encodings")
  72. unless grep {$_ eq $self->{'encoding-nonaliased'}} (Encode->encodings(":all"));
  73. #Create default request to pass on to LWP::UserAgent
  74. $self->{'default_request'} = new HTTP::Request();
  75. $self->{'default_request'}->authorization_basic($user,$pass);
  76. bless( $self, $class );
  77. return $self if $self->debug; #For easy class testing without mocks
  78. #Manually do the get_users call to check HTTP status
  79. my $res = $self->_doRequest('index.php?/api/v2/get_users');
  80. confess "Error: network unreachable" if !defined($res);
  81. if ( (reftype($res) || 'undef') ne 'ARRAY') {
  82. confess "Unexpected return from _doRequest: $res" if !looks_like_number($res);
  83. confess "Could not communicate with TestRail Server! Check that your URI is correct, and your TestRail installation is functioning correctly." if $res == -500;
  84. confess "Could not list testRail users! Check that your TestRail installation has it's API enabled, and your credentials are correct" if $res == -403;
  85. confess "Bad user credentials!" if $res == -401;
  86. confess "HTTP error $res encountered while communicating with TestRail server. Resolve issue and try again." if !$res;
  87. confess "Unknown error occurred: $res";
  88. }
  89. confess "No users detected on TestRail Install! Check that your API is functioning correctly." if !scalar(@$res);
  90. $self->{'user_cache'} = $res;
  91. return $self;
  92. }
  93. =head1 GETTERS
  94. =head2 B<apiurl>
  95. =head2 B<debug>
  96. Accessors for these parameters you pass into the constructor, in case you forget.
  97. =cut
  98. sub apiurl {
  99. my $self = shift;
  100. confess("Object methods must be called by an instance") unless ref($self);
  101. return $self->{'apiurl'}
  102. }
  103. sub debug {
  104. my $self = shift;
  105. confess("Object methods must be called by an instance") unless ref($self);
  106. return $self->{'debug'};
  107. }
  108. #Convenient JSON-HTTP fetcher
  109. sub _doRequest {
  110. my ($self,$path,$method,$data) = @_;
  111. confess("Object methods must be called by an instance") unless ref($self);
  112. my $req = clone $self->{'default_request'};
  113. $method //= 'GET';
  114. $req->method($method);
  115. $req->url($self->apiurl.'/'.$path);
  116. warn "$method ".$self->apiurl."/$path" if $self->debug;
  117. my $coder = JSON::MaybeXS->new;
  118. #Data sent is JSON, and encoded per user preference
  119. my $content = $data ? Encode::encode( $self->{'encoding-nonaliased'}, $coder->encode($data) ) : '';
  120. $req->content($content);
  121. $req->header( "Content-Type" => "application/json; charset=".$self->{'encoding'} );
  122. my $response = $self->{'browser'}->request($req);
  123. #Uncomment to generate mocks
  124. #use Data::Dumper;
  125. #print Dumper($path,'200','OK',$response->headers,$response->content);
  126. return $response if !defined($response); #worst case
  127. if ($response->code == 403) {
  128. cluck "ERROR: Access Denied.";
  129. return -403;
  130. }
  131. if ($response->code != 200) {
  132. cluck "ERROR: Arguments Bad: ".$response->content;
  133. return -int($response->code);
  134. }
  135. try {
  136. return $coder->decode($response->content);
  137. } catch {
  138. if ($response->code == 200 && !$response->content) {
  139. return 1; #This function probably just returns no data
  140. } else {
  141. cluck "ERROR: Malformed JSON returned by API.";
  142. cluck $@;
  143. if (!$self->debug) { #Otherwise we've already printed this, but we need to know if we encounter this
  144. cluck "RAW CONTENT:";
  145. cluck $response->content
  146. }
  147. return 0;
  148. }
  149. }
  150. }
  151. =head1 USER METHODS
  152. =head2 B<getUsers ()>
  153. Get all the user definitions for the provided Test Rail install.
  154. Returns ARRAYREF of user definition HASHREFs.
  155. =cut
  156. sub getUsers {
  157. my $self = shift;
  158. confess("Object methods must be called by an instance") unless ref($self);
  159. my $res = $self->_doRequest('index.php?/api/v2/get_users');
  160. return -500 if !$res || (reftype($res) || 'undef') ne 'ARRAY';
  161. $self->{'user_cache'} = $res;
  162. return $res;
  163. }
  164. =head2 B<getUserByID(id)>
  165. =cut
  166. =head2 B<getUserByName(name)>
  167. =cut
  168. =head2 B<getUserByEmail(email)>
  169. Get user definition hash by ID, Name or Email.
  170. Returns user def HASHREF.
  171. =cut
  172. #I'm just using the cache for the following methods because it's more straightforward and faster past 1 call.
  173. sub getUserByID {
  174. my ($self,$user) = @_;
  175. confess("Object methods must be called by an instance") unless ref($self);
  176. confess("User ID must be integer") unless $self->_checkInteger($user);
  177. $self->getUsers() if !defined($self->{'user_cache'});
  178. return -500 if (!defined($self->{'user_cache'}) || (reftype($self->{'user_cache'}) || 'undef') ne 'ARRAY');
  179. foreach my $usr (@{$self->{'user_cache'}}) {
  180. return $usr if $usr->{'id'} == $user;
  181. }
  182. return 0;
  183. }
  184. sub getUserByName {
  185. my ($self,$user) = @_;
  186. confess("Object methods must be called by an instance") unless ref($self);
  187. confess("User must be string") unless $self->_checkString($user);
  188. $self->getUsers() if !defined($self->{'user_cache'});
  189. return -500 if (!defined($self->{'user_cache'}) || (reftype($self->{'user_cache'}) || 'undef') ne 'ARRAY');
  190. foreach my $usr (@{$self->{'user_cache'}}) {
  191. return $usr if $usr->{'name'} eq $user;
  192. }
  193. return 0;
  194. }
  195. sub getUserByEmail {
  196. my ($self,$email) = @_;
  197. confess("Object methods must be called by an instance") unless ref($self);
  198. confess("Email must be string") unless $self->_checkString($email);
  199. $self->getUsers() if !defined($self->{'user_cache'});
  200. return -500 if (!defined($self->{'user_cache'}) || (reftype($self->{'user_cache'}) || 'undef') ne 'ARRAY');
  201. foreach my $usr (@{$self->{'user_cache'}}) {
  202. return $usr if $usr->{'email'} eq $email;
  203. }
  204. return 0;
  205. }
  206. =head2 userNamesToIds(names)
  207. Convenience method to translate a list of user names to TestRail user IDs.
  208. =over 4
  209. =item ARRAY C<NAMES> - Array of user names to translate to IDs.
  210. =back
  211. Returns ARRAY of user IDs.
  212. Throws an exception in the case of one (or more) of the names not corresponding to a valid username.
  213. =cut
  214. sub userNamesToIds {
  215. my ($self,@names) = @_;
  216. confess("Object methods must be called by an instance") unless ref($self);
  217. confess("At least one user name must be provided") if !scalar(@names);
  218. my @ret = grep {defined $_} map {my $user = $_; my @list = grep {$user->{'name'} eq $_} @names; scalar(@list) ? $user->{'id'} : undef} @{$self->getUsers()};
  219. confess("One or more user names provided does not exist in TestRail.") unless scalar(@names) == scalar(@ret);
  220. return @ret;
  221. };
  222. =head1 PROJECT METHODS
  223. =head2 B<createProject (name, [description,send_announcement])>
  224. Creates new Project (Database of testsuites/tests).
  225. Optionally specify an announcement to go out to the users.
  226. Requires TestRail admin login.
  227. =over 4
  228. =item STRING C<NAME> - Desired name of project.
  229. =item STRING C<DESCRIPTION> (optional) - Description of project. Default value is 'res ipsa loquiter'.
  230. =item BOOLEAN C<SEND ANNOUNCEMENT> (optional) - Whether to confront users with an announcement about your awesome project on next login. Default false.
  231. =back
  232. Returns project definition HASHREF on success, false otherwise.
  233. $tl->createProject('Widgetronic 4000', 'Tests for the whiz-bang new product', true);
  234. =cut
  235. sub createProject {
  236. my ($self,$name,$desc,$announce) = @_;
  237. confess("Object methods must be called by an instance") unless ref($self);
  238. confess("Project name must be string") unless $self->_checkString($name);
  239. $desc //= 'res ipsa loquiter';
  240. $announce //= 0;
  241. confess("Project description must be string") unless $self->_checkString($desc);
  242. confess("Announce must be integer") unless $self->_checkInteger($announce);
  243. my $input = {
  244. name => $name,
  245. announcement => $desc,
  246. show_announcement => $announce
  247. };
  248. my $result = $self->_doRequest('index.php?/api/v2/add_project','POST',$input);
  249. return $result;
  250. }
  251. =head2 B<deleteProject (id)>
  252. Deletes specified project by ID.
  253. Requires TestRail admin login.
  254. =over 4
  255. =item STRING C<NAME> - Desired name of project.
  256. =back
  257. Returns BOOLEAN.
  258. $success = $tl->deleteProject(1);
  259. =cut
  260. sub deleteProject {
  261. my ($self,$proj) = @_;
  262. confess("Object methods must be called by an instance") unless ref($self);
  263. confess("Project ID must be integer") unless $self->_checkInteger($proj);
  264. my $result = $self->_doRequest('index.php?/api/v2/delete_project/'.$proj,'POST');
  265. return $result;
  266. }
  267. =head2 B<getProjects ()>
  268. Get all available projects
  269. Returns array of project definition HASHREFs, false otherwise.
  270. $projects = $tl->getProjects;
  271. =cut
  272. sub getProjects {
  273. my $self = shift;
  274. confess("Object methods must be called by an instance") unless ref($self);
  275. my $result = $self->_doRequest('index.php?/api/v2/get_projects');
  276. #Save state for future use, if needed
  277. return -500 if !$result || (reftype($result) || 'undef') ne 'ARRAY';
  278. $self->{'testtree'} = $result;
  279. #Note that it's a project for future reference by recursive tree search
  280. return -500 if !$result || (reftype($result) || 'undef') ne 'ARRAY';
  281. foreach my $pj (@{$result}) {
  282. $pj->{'type'} = 'project';
  283. }
  284. return $result;
  285. }
  286. =head2 B<getProjectByName ($project)>
  287. Gets some project definition hash by it's name
  288. =over 4
  289. =item STRING C<PROJECT> - desired project
  290. =back
  291. Returns desired project def HASHREF, false otherwise.
  292. $project = $tl->getProjectByName('FunProject');
  293. =cut
  294. sub getProjectByName {
  295. my ($self,$project) = @_;
  296. confess("Object methods must be called by an instance") unless ref($self);
  297. confess("Project must be string.") unless $self->_checkString($project);
  298. #See if we already have the project list...
  299. my $projects = $self->{'testtree'};
  300. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  301. $projects = $self->getProjects() unless scalar(@$projects);
  302. #Search project list for project
  303. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  304. for my $candidate (@$projects) {
  305. return $candidate if ($candidate->{'name'} eq $project);
  306. }
  307. return 0;
  308. }
  309. =head2 B<getProjectByID ($project)>
  310. Gets some project definition hash by it's ID
  311. =over 4
  312. =item INTEGER C<PROJECT> - desired project
  313. =back
  314. Returns desired project def HASHREF, false otherwise.
  315. $projects = $tl->getProjectByID(222);
  316. =cut
  317. sub getProjectByID {
  318. my ($self,$project) = @_;
  319. confess("Object methods must be called by an instance") unless ref($self);
  320. confess("No project provided.") unless $project;
  321. confess("Project ID must be integer") unless $self->_checkInteger($project);
  322. #See if we already have the project list...
  323. my $projects = $self->{'testtree'};
  324. $projects = $self->getProjects() unless scalar(@$projects);
  325. #Search project list for project
  326. return -500 if !$projects || (reftype($projects) || 'undef') ne 'ARRAY';
  327. for my $candidate (@$projects) {
  328. return $candidate if ($candidate->{'id'} eq $project);
  329. }
  330. return 0;
  331. }
  332. =head1 TESTSUITE METHODS
  333. =head2 B<createTestSuite (project_id, name, [description])>
  334. Creates new TestSuite (folder of tests) in the database of test specifications under given project id having given name and details.
  335. =over 4
  336. =item INTEGER C<PROJECT ID> - ID of project this test suite should be under.
  337. =item STRING C<NAME> - Desired name of test suite.
  338. =item STRING C<DESCRIPTION> (optional) - Description of test suite. Default value is 'res ipsa loquiter'.
  339. =back
  340. Returns TS definition HASHREF on success, false otherwise.
  341. $tl->createTestSuite(1, 'broken tests', 'Tests that should be reviewed');
  342. =cut
  343. sub createTestSuite {
  344. my ($self,$project_id,$name,$details) = @_;
  345. confess("Object methods must be called by an instance") unless ref($self);
  346. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  347. confess("Name must be a string") unless $self->_checkString($name);
  348. $details ||= 'res ipsa loquiter';
  349. confess("Project details must be a string") unless $self->_checkString($details);
  350. my $input = {
  351. name => $name,
  352. description => $details
  353. };
  354. my $result = $self->_doRequest('index.php?/api/v2/add_suite/'.$project_id,'POST',$input);
  355. return $result;
  356. }
  357. =head2 B<deleteTestSuite (suite_id)>
  358. Deletes specified testsuite.
  359. =over 4
  360. =item INTEGER C<SUITE ID> - ID of testsuite to delete.
  361. =back
  362. Returns BOOLEAN.
  363. $tl->deleteTestSuite(1);
  364. =cut
  365. sub deleteTestSuite {
  366. my ($self,$suite_id) = @_;
  367. confess("Object methods must be called by an instance") unless ref($self);
  368. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  369. my $result = $self->_doRequest('index.php?/api/v2/delete_suite/'.$suite_id,'POST');
  370. return $result;
  371. }
  372. =head2 B<getTestSuites (project_id)>
  373. Gets the testsuites for a project
  374. =over 4
  375. =item STRING C<PROJECT ID> - desired project's ID
  376. =back
  377. Returns ARRAYREF of testsuite definition HASHREFs, 0 on error.
  378. $suites = $tl->getTestSuites(123);
  379. =cut
  380. sub getTestSuites {
  381. my ($self,$proj) = @_;
  382. confess("Object methods must be called by an instance") unless ref($self);
  383. confess("Project ID must be integer") unless $self->_checkInteger($proj);
  384. return $self->_doRequest('index.php?/api/v2/get_suites/'.$proj);
  385. }
  386. =head2 B<getTestSuiteByName (project_id,testsuite_name)>
  387. Gets the testsuite that matches the given name inside of given project.
  388. =over 4
  389. =item STRING C<PROJECT ID> - ID of project holding this testsuite
  390. =item STRING C<TESTSUITE NAME> - desired parent testsuite name
  391. =back
  392. Returns desired testsuite definition HASHREF, false otherwise.
  393. $suites = $tl->getTestSuitesByName(321, 'hugSuite');
  394. =cut
  395. sub getTestSuiteByName {
  396. my ($self,$project_id,$testsuite_name) = @_;
  397. confess("Object methods must be called by an instance") unless ref($self);
  398. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  399. confess("Testsuite Name must be String") unless $self->_checkString($testsuite_name);
  400. #TODO cache
  401. my $suites = $self->getTestSuites($project_id);
  402. return -500 if !$suites || (reftype($suites) || 'undef') ne 'ARRAY'; #No suites for project, or no project
  403. foreach my $suite (@$suites) {
  404. return $suite if $suite->{'name'} eq $testsuite_name;
  405. }
  406. return 0; #Couldn't find it
  407. }
  408. =head2 B<getTestSuiteByID (testsuite_id)>
  409. Gets the testsuite with the given ID.
  410. =over 4
  411. =item STRING C<TESTSUITE_ID> - TestSuite ID.
  412. =back
  413. Returns desired testsuite definition HASHREF, false otherwise.
  414. $tests = $tl->getTestSuiteByID(123);
  415. =cut
  416. sub getTestSuiteByID {
  417. my ($self,$testsuite_id) = @_;
  418. confess("Object methods must be called by an instance") unless ref($self);
  419. confess("Testsuite ID must be integer") unless $self->_checkInteger($testsuite_id);
  420. my $result = $self->_doRequest('index.php?/api/v2/get_suite/'.$testsuite_id);
  421. return $result;
  422. }
  423. =head1 SECTION METHODS
  424. =head2 B<createSection(project_id,suite_id,name,[parent_id])>
  425. Creates a section.
  426. =over 4
  427. =item INTEGER C<PROJECT ID> - Parent Project ID.
  428. =item INTEGER C<SUITE ID> - Parent TestSuite ID.
  429. =item STRING C<NAME> - desired section name.
  430. =item INTEGER C<PARENT ID> (optional) - parent section id
  431. =back
  432. Returns new section definition HASHREF, false otherwise.
  433. $section = $tr->createSection(1,1,'nugs',1);
  434. =cut
  435. sub createSection {
  436. my ($self,$project_id,$suite_id,$name,$parent_id) = @_;
  437. confess("Object methods must be called by an instance") unless ref($self);
  438. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  439. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  440. confess("Section name must be string") unless $self->_checkString($name);
  441. confess("Parent section ID must be integer") unless !defined($parent_id) || $self->_checkInteger($parent_id);
  442. my $input = {
  443. name => $name,
  444. suite_id => $suite_id
  445. };
  446. $input->{'parent_id'} = $parent_id if $parent_id;
  447. my $result = $self->_doRequest('index.php?/api/v2/add_section/'.$project_id,'POST',$input);
  448. return $result;
  449. }
  450. =head2 B<deleteSection (section_id)>
  451. Deletes specified section.
  452. =over 4
  453. =item INTEGER C<SECTION ID> - ID of section to delete.
  454. =back
  455. Returns BOOLEAN.
  456. $tr->deleteSection(1);
  457. =cut
  458. sub deleteSection {
  459. my ($self,$section_id) = @_;
  460. confess("Object methods must be called by an instance") unless ref($self);
  461. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  462. my $result = $self->_doRequest('index.php?/api/v2/delete_section/'.$section_id,'POST');
  463. return $result;
  464. }
  465. =head2 B<getSections (project_id,suite_id)>
  466. Gets sections for a given project and suite.
  467. =over 4
  468. =item INTEGER C<PROJECT ID> - ID of parent project.
  469. =item INTEGER C<SUITE ID> - ID of suite to get sections for.
  470. =back
  471. Returns ARRAYREF of section definition HASHREFs.
  472. $tr->getSections(1,2);
  473. =cut
  474. sub getSections {
  475. my ($self,$project_id,$suite_id) = @_;
  476. confess("Object methods must be called by an instance") unless ref($self);
  477. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  478. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  479. return $self->_doRequest("index.php?/api/v2/get_sections/$project_id&suite_id=$suite_id");
  480. }
  481. =head2 B<getSectionByID (section_id)>
  482. Gets desired section.
  483. =over 4
  484. =item INTEGER C<PROJECT ID> - ID of parent project.
  485. =item INTEGER C<SUITE ID> - ID of suite to get sections for.
  486. =back
  487. Returns section definition HASHREF.
  488. $tr->getSectionByID(344);
  489. =cut
  490. sub getSectionByID {
  491. my ($self,$section_id) = @_;
  492. confess("Object methods must be called by an instance") unless ref($self);
  493. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  494. return $self->_doRequest("index.php?/api/v2/get_section/$section_id");
  495. }
  496. =head2 B<getSectionByName (project_id,suite_id,name)>
  497. Gets desired section.
  498. =over 4
  499. =item INTEGER C<PROJECT ID> - ID of parent project.
  500. =item INTEGER C<SUITE ID> - ID of suite to get section for.
  501. =item STRING C<NAME> - name of section to get
  502. =back
  503. Returns section definition HASHREF.
  504. $tr->getSectionByName(1,2,'nugs');
  505. =cut
  506. sub getSectionByName {
  507. my ($self,$project_id,$suite_id,$section_name) = @_;
  508. confess("Object methods must be called by an instance") unless ref($self);
  509. confess("Project ID must be an integer") unless $self->_checkInteger($project_id);
  510. confess("Suite ID must be an integer") unless $self->_checkInteger($suite_id);
  511. confess("Section Name must be a string") unless $self->_checkString($section_name);
  512. my $sections = $self->getSections($project_id,$suite_id);
  513. return -500 if !$sections || (reftype($sections) || 'undef') ne 'ARRAY';
  514. foreach my $sec (@$sections) {
  515. return $sec if $sec->{'name'} eq $section_name;
  516. }
  517. return 0;
  518. }
  519. =head2 sectionNamesToIds(project_id,suite_id,names)
  520. Convenience method to translate a list of section names to TestRail section IDs.
  521. =over 4
  522. =item INTEGER C<PROJECT ID> - ID of parent project.
  523. =item INTEGER C<SUITE ID> - ID of parent suite.
  524. =item ARRAY C<NAMES> - Array of section names to translate to IDs.
  525. =back
  526. Returns ARRAY of section IDs.
  527. Throws an exception in the case of one (or more) of the names not corresponding to a valid section name.
  528. =cut
  529. sub sectionNamesToIds {
  530. my ($self,$project_id,$suite_id,@names) = @_;
  531. confess("Object methods must be called by an instance") unless ref($self);
  532. confess("Project ID must be an integer") unless $self->_checkInteger($project_id);
  533. confess("Suite ID must be an integer") unless $self->_checkInteger($suite_id);
  534. confess("At least one section name must be provided") if !scalar(@names);
  535. my $sections = $self->getSections($project_id,$suite_id);
  536. confess("Invalid project/suite ($project_id,$suite_id) provided.") unless (reftype($sections) || 'undef') eq 'ARRAY';
  537. my @ret = grep {defined $_} map {my $section = $_; my @list = grep {$section->{'name'} eq $_} @names; scalar(@list) ? $section->{'id'} : undef} @$sections;
  538. confess("One or more user names provided does not exist in TestRail.") unless scalar(@names) == scalar(@ret);
  539. return @ret;
  540. }
  541. =head1 CASE METHODS
  542. =head2 B<getCaseTypes ()>
  543. Gets possible case types.
  544. Returns ARRAYREF of case type definition HASHREFs.
  545. $tr->getCaseTypes();
  546. =cut
  547. sub getCaseTypes {
  548. my $self = shift;
  549. confess("Object methods must be called by an instance") unless ref($self);
  550. my $types = $self->_doRequest("index.php?/api/v2/get_case_types");
  551. return -500 if !$types || (reftype($types) || 'undef') ne 'ARRAY';
  552. $self->{'type_cache'} = $types if !$self->{'type_cache'}; #We can't change this with API, so assume it is static
  553. return $self->{'type_cache'};
  554. }
  555. =head2 B<getCaseTypeByName (name)>
  556. Gets case type by name.
  557. =over 4
  558. =item STRING C<NAME> - Name of desired case type
  559. =back
  560. Returns case type definition HASHREF.
  561. $tr->getCaseTypeByName();
  562. =cut
  563. sub getCaseTypeByName {
  564. #Useful for marking automated tests, etc
  565. my ($self,$name) = @_;
  566. confess("Object methods must be called by an instance") unless ref($self);
  567. confess("Case type must be string") unless $self->_checkString($name);
  568. my $types = $self->getCaseTypes();
  569. return -500 if !$types || (reftype($types) || 'undef') ne 'ARRAY';
  570. foreach my $type (@$types) {
  571. return $type if $type->{'name'} eq $name;
  572. }
  573. return 0;
  574. }
  575. =head2 B<createCase(section_id,title,type_id,options,extra_options)>
  576. Creates a test case.
  577. =over 4
  578. =item INTEGER C<SECTION ID> - Parent Section ID.
  579. =item STRING C<TITLE> - Case title.
  580. =item INTEGER C<TYPE_ID> (optional) - desired test type's ID. Defaults to whatever your TR install considers the default type.
  581. =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.
  582. =item HASHREF C<EXTRA OPTIONS> (optional) - contains priority_id, estimate, milestone_id and refs as possible keys. See TestRail API documentation for more info.
  583. =back
  584. Returns new case definition HASHREF, false otherwise.
  585. $custom_opts = {
  586. preconds => "Test harness installed",
  587. steps => "Do the needful",
  588. expected => "cubicle environment transforms into Dali painting"
  589. };
  590. $other_opts = {
  591. priority_id => 4,
  592. milestone_id => 666,
  593. estimate => '2m 45s',
  594. refs => ['TRACE-22','ON-166'] #ARRAYREF of bug IDs.
  595. }
  596. $case = $tr->createCase(1,'Do some stuff',3,$custom_opts,$other_opts);
  597. =cut
  598. sub createCase {
  599. my ($self,$section_id,$title,$type_id,$opts,$extras) = @_;
  600. confess("Object methods must be called by an instance") unless ref($self);
  601. confess("Section ID ($section_id) must be integer") unless $self->_checkInteger($section_id);
  602. confess("title must be string") unless $self->_checkString($title);
  603. confess("Type ID must be integer") unless !defined($type_id) || $self->_checkInteger($type_id);
  604. confess("Options must be HASHREF") unless !defined($opts) || (reftype($opts) || 'undef') ne 'HASH';
  605. confess("Extras must be HASHREF") unless !defined($extras) || (reftype($extras) || 'undef') ne 'HASH';
  606. my $stuff = {
  607. title => $title,
  608. type_id => $type_id
  609. };
  610. #Handle sort of optional but baked in options
  611. if (defined($extras) && reftype($extras) eq 'HASH') {
  612. $stuff->{'priority_id'} = $extras->{'priority_id'} if defined($extras->{'priority_id'});
  613. $stuff->{'estimate'} = $extras->{'estimate'} if defined($extras->{'estimate'});
  614. $stuff->{'milestone_id'} = $extras->{'milestone_id'} if defined($extras->{'milestone_id'});
  615. $stuff->{'refs'} = join(',',@{$extras->{'refs'}}) if defined($extras->{'refs'});
  616. }
  617. #Handle custom fields
  618. if (defined($opts) && reftype($opts) eq 'HASH') {
  619. foreach my $key (keys(%$opts)) {
  620. $stuff->{"custom_$key"} = $opts->{$key};
  621. }
  622. }
  623. my $result = $self->_doRequest("index.php?/api/v2/add_case/$section_id",'POST',$stuff);
  624. return $result;
  625. }
  626. =head2 B<deleteCase (case_id)>
  627. Deletes specified section.
  628. =over 4
  629. =item INTEGER C<CASE ID> - ID of case to delete.
  630. =back
  631. Returns BOOLEAN.
  632. $tr->deleteCase(1324);
  633. =cut
  634. sub deleteCase {
  635. my ($self,$case_id) = @_;
  636. confess("Object methods must be called by an instance") unless ref($self);
  637. confess("Case ID must be integer") unless $self->_checkInteger($case_id);
  638. my $result = $self->_doRequest("index.php?/api/v2/delete_case/$case_id",'POST');
  639. return $result;
  640. }
  641. =head2 B<getCases (project_id,suite_id,section_id)>
  642. Gets cases for provided section.
  643. =over 4
  644. =item INTEGER C<PROJECT ID> - ID of parent project.
  645. =item INTEGER C<SUITE ID> - ID of parent suite.
  646. =item INTEGER C<SECTION ID> - ID of parent section
  647. =back
  648. Returns ARRAYREF of test case definition HASHREFs.
  649. $tr->getCases(1,2,3);
  650. =cut
  651. sub getCases {
  652. my ($self,$project_id,$suite_id,$section_id) = @_;
  653. confess("Object methods must be called by an instance") unless ref($self);
  654. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  655. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  656. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  657. my $url = "index.php?/api/v2/get_cases/$project_id&suite_id=$suite_id";
  658. $url .= "&section_id=$section_id" if $section_id;
  659. return $self->_doRequest($url);
  660. }
  661. =head2 B<getCaseByName (project_id,suite_id,section_id,name)>
  662. Gets case by name.
  663. =over 4
  664. =item INTEGER C<PROJECT ID> - ID of parent project.
  665. =item INTEGER C<SUITE ID> - ID of parent suite.
  666. =item INTEGER C<SECTION ID> - ID of parent section.
  667. =item STRING <NAME> - Name of desired test case.
  668. =back
  669. Returns test case definition HASHREF.
  670. $tr->getCaseByName(1,2,3,'nugs');
  671. =cut
  672. sub getCaseByName {
  673. my ($self,$project_id,$suite_id,$section_id,$name) = @_;
  674. confess("Object methods must be called by an instance") unless ref($self);
  675. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  676. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  677. confess("Section ID must be integer") unless $self->_checkInteger($section_id);
  678. confess("Test Case name must be string") unless $self->_checkString($name);
  679. my $cases = $self->getCases($project_id,$suite_id,$section_id);
  680. return -500 if !$cases || (reftype($cases) || 'undef') ne 'ARRAY';
  681. foreach my $case (@$cases) {
  682. return $case if $case->{'title'} eq $name;
  683. }
  684. return 0;
  685. }
  686. =head2 B<getCaseByID (case_id)>
  687. Gets case by ID.
  688. =over 4
  689. =item INTEGER C<CASE ID> - ID of case.
  690. =back
  691. Returns test case definition HASHREF.
  692. $tr->getCaseByID(1345);
  693. =cut
  694. sub getCaseByID {
  695. my ($self,$case_id) = @_;
  696. confess("Object methods must be called by an instance") unless ref($self);
  697. confess("Case ID must be integer") unless $self->_checkInteger($case_id);
  698. return $self->_doRequest("index.php?/api/v2/get_case/$case_id");
  699. }
  700. =head1 RUN METHODS
  701. =head2 B<createRun (project_id,suite_id,name,description,milestone_id,assigned_to_id,case_ids)>
  702. Create a run.
  703. =over 4
  704. =item INTEGER C<PROJECT ID> - ID of parent project.
  705. =item INTEGER C<SUITE ID> - ID of suite to base run on
  706. =item STRING C<NAME> - Name of run
  707. =item STRING C<DESCRIPTION> (optional) - Description of run
  708. =item INTEGER C<MILESTONE ID> (optional) - ID of milestone
  709. =item INTEGER C<ASSIGNED TO ID> (optional) - User to assign the run to
  710. =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.
  711. =back
  712. Returns run definition HASHREF.
  713. $tr->createRun(1,1345,'RUN AWAY','SO FAR AWAY',22,3,[3,4,5,6]);
  714. =cut
  715. #If you pass an array of case ids, it implies include_all is false
  716. sub createRun {
  717. my ($self,$project_id,$suite_id,$name,$desc,$milestone_id,$assignedto_id,$case_ids) = @_;
  718. confess("Object methods must be called by an instance") unless ref($self);
  719. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  720. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  721. confess("Name must be string") unless $self->_checkString($name);
  722. confess("Description must be string") unless !defined($desc) || $self->_checkString($desc);
  723. confess("Milestone ID must be integer") unless !defined($milestone_id) || $self->_checkInteger($milestone_id);
  724. confess("Assigned To ID must be integer") unless !defined($assignedto_id) || $self->_checkInteger($assignedto_id);
  725. confess("Case IDs must be ARRAYREF") unless !defined($case_ids) || (reftype($case_ids) || 'undef') eq 'ARRAY';
  726. my $stuff = {
  727. suite_id => $suite_id,
  728. name => $name,
  729. description => $desc,
  730. milestone_id => $milestone_id,
  731. assignedto_id => $assignedto_id,
  732. include_all => defined($case_ids) ? 0 : 1,
  733. case_ids => $case_ids
  734. };
  735. my $result = $self->_doRequest("index.php?/api/v2/add_run/$project_id",'POST',$stuff);
  736. return $result;
  737. }
  738. =head2 B<deleteRun (run_id)>
  739. Deletes specified run.
  740. =over 4
  741. =item INTEGER C<RUN ID> - ID of run to delete.
  742. =back
  743. Returns BOOLEAN.
  744. $tr->deleteRun(1324);
  745. =cut
  746. sub deleteRun {
  747. my ($self,$run_id) = @_;
  748. confess("Object methods must be called by an instance") unless ref($self);
  749. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  750. my $result = $self->_doRequest("index.php?/api/v2/delete_run/$run_id",'POST');
  751. return $result;
  752. }
  753. =head2 B<getRuns (project_id)>
  754. Get all runs for specified project.
  755. To do this, it must make (no. of runs/250) HTTP requests.
  756. This is due to the maximum result set limit enforced by testrail.
  757. =over 4
  758. =item INTEGER C<PROJECT_ID> - ID of parent project
  759. =back
  760. Returns ARRAYREF of run definition HASHREFs.
  761. $allRuns = $tr->getRuns(6969);
  762. =cut
  763. sub getRuns {
  764. my ($self,$project_id) = @_;
  765. confess("Object methods must be called by an instance") unless ref($self);
  766. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  767. my $initial_runs = $self->getRunsPaginated($project_id,$self->{'global_limit'},0);
  768. return $initial_runs unless (reftype($initial_runs) || 'undef') eq 'ARRAY';
  769. my $runs = [];
  770. push(@$runs,@$initial_runs);
  771. my $offset = 1;
  772. while (scalar(@$initial_runs) == $self->{'global_limit'}) {
  773. $initial_runs = $self->getRunsPaginated($project_id,$self->{'global_limit'},($self->{'global_limit'} * $offset));
  774. push(@$runs,@$initial_runs);
  775. $offset++;
  776. }
  777. return $runs;
  778. }
  779. =head2 B<getRunsPaginated (project_id,limit,offset)>
  780. Get some runs for specified project.
  781. =over 4
  782. =item INTEGER C<PROJECT_ID> - ID of parent project
  783. =item INTEGER C<LIMIT> - Number of runs to return.
  784. =item INTEGER C<OFFSET> - Page of runs to return.
  785. =back
  786. Returns ARRAYREF of run definition HASHREFs.
  787. $someRuns = $tr->getRunsPaginated(6969,22,4);
  788. =cut
  789. sub getRunsPaginated {
  790. my ($self,$project_id,$limit,$offset) = @_;
  791. confess("Object methods must be called by an instance") unless ref($self);
  792. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  793. confess("Limit must be integer") unless !defined($limit) || $self->_checkInteger($limit);
  794. confess("Offset must be integer") unless !defined($offset) || $self->_checkInteger($offset);
  795. confess("Limit greater than ".$self->{'global_limit'}) if $limit > $self->{'global_limit'};
  796. my $apiurl = "index.php?/api/v2/get_runs/$project_id";
  797. $apiurl .= "&offset=$offset" if $offset;
  798. $apiurl .= "&limit=$limit" if $limit;
  799. return $self->_doRequest($apiurl);
  800. }
  801. =head2 B<getRunByName (project_id,name)>
  802. Gets run by name.
  803. =over 4
  804. =item INTEGER C<PROJECT ID> - ID of parent project.
  805. =item STRING <NAME> - Name of desired run.
  806. =back
  807. Returns run definition HASHREF.
  808. $tr->getRunByName(1,'R2');
  809. =cut
  810. sub getRunByName {
  811. my ($self,$project_id,$name) = @_;
  812. confess("Object methods must be called by an instance") unless ref($self);
  813. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  814. confess("Run name must be string") unless $self->_checkString($name);
  815. my $runs = $self->getRuns($project_id);
  816. return -500 if !$runs || (reftype($runs) || 'undef') ne 'ARRAY';
  817. foreach my $run (@$runs) {
  818. return $run if $run->{'name'} eq $name;
  819. }
  820. return 0;
  821. }
  822. =head2 B<getRunByID (run_id)>
  823. Gets run by ID.
  824. =over 4
  825. =item INTEGER C<RUN ID> - ID of desired run.
  826. =back
  827. Returns run definition HASHREF.
  828. $tr->getRunByID(7779311);
  829. =cut
  830. sub getRunByID {
  831. my ($self,$run_id) = @_;
  832. confess("Object methods must be called by an instance") unless ref($self);
  833. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  834. return $self->_doRequest("index.php?/api/v2/get_run/$run_id");
  835. }
  836. =head2 B<closeRun (run_id)>
  837. Close the specified run.
  838. =over 4
  839. =item INTEGER C<RUN ID> - ID of desired run.
  840. =back
  841. Returns run definition HASHREF on success, false on failure.
  842. $tr->closeRun(90210);
  843. =cut
  844. sub closeRun {
  845. my ($self,$run_id) = @_;
  846. confess("Object methods must be called by an instance") unless ref($self);
  847. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  848. return $self->_doRequest("index.php?/api/v2/close_run/$run_id",'POST');
  849. }
  850. =head2 B<getRunSummary(runs)>
  851. Returns array of hashrefs describing the # of tests in the run(s) with the available statuses.
  852. Translates custom_statuses into their system names for you.
  853. =over 4
  854. =item ARRAY C<RUNS> - runs obtained from getRun* or getChildRun* methods.
  855. =back
  856. Returns ARRAY of run HASHREFs with the added key 'run_status' holding a hashref where status_name => count.
  857. $tr->getRunSummary($run,$run2);
  858. =cut
  859. sub getRunSummary {
  860. my ($self,@runs) = @_;
  861. confess("Object methods must be called by an instance") unless ref($self);
  862. confess("All Plans passed must be HASHREFs") unless scalar( grep {(reftype($_) || 'undef') eq 'HASH'} @runs ) == scalar(@runs);
  863. #Translate custom statuses
  864. my $statuses = $self->getPossibleTestStatuses();
  865. my %shash;
  866. #XXX so, they do these tricks with the status names, see...so map the counts to their relevant status ids.
  867. @shash{map { ( $_->{'id'} < 6 ) ? $_->{'name'}."_count" : "custom_status".($_->{'id'} - 5)."_count" } @$statuses } = map { $_->{'id'} } @$statuses;
  868. my @sname;
  869. #Create listing of keys/values
  870. @runs = map {
  871. my $run = $_;
  872. @{$run->{statuses}}{grep {$_ =~ m/_count$/} keys(%$run)} = grep {$_ =~ m/_count$/} keys(%$run);
  873. foreach my $status (keys(%{$run->{'statuses'}})) {
  874. next if !exists($shash{$status});
  875. @sname = grep {exists($shash{$status}) && $_->{'id'} == $shash{$status}} @$statuses;
  876. $run->{'statuses_clean'}->{$sname[0]->{'name'}} = $run->{$status};
  877. }
  878. $run;
  879. } @runs;
  880. return map { {'id' => $_->{'id'}, 'name' => $_->{'name'}, 'run_status' => $_->{'statuses_clean'}, 'config_ids' => $_->{'config_ids'} } } @runs;
  881. }
  882. =head1 RUN AS CHILD OF PLAN METHODS
  883. =head2 B<getChildRuns(plan)>
  884. Extract the child runs from a plan. Convenient, as the structure of this hash is deep, and correct error handling can be tedious.
  885. =over 4
  886. =item HASHREF C<PLAN> - Test Plan definition HASHREF returned by any of the PLAN methods below.
  887. =back
  888. Returns ARRAYREF of run definition HASHREFs. Returns 0 upon failure to extract the data.
  889. =cut
  890. sub getChildRuns {
  891. my ($self,$plan) = @_;
  892. confess("Object methods must be called by an instance") unless ref($self);
  893. confess("Plan must be HASHREF") unless defined($plan) && (reftype($plan) || 'undef') eq 'HASH';
  894. return 0 unless defined($plan->{'entries'}) && (reftype($plan->{'entries'}) || 'undef') eq 'ARRAY';
  895. return 0 unless defined($plan->{'entries'}) && (reftype($plan->{'entries'}) || 'undef') eq 'ARRAY';
  896. my $entries = $plan->{'entries'};
  897. my $plans = [];
  898. foreach my $entry (@$entries) {
  899. push(@$plans,@{$entry->{'runs'}}) if defined($entry->{'runs'}) && ((reftype($entry->{'runs'}) || 'undef') eq 'ARRAY')
  900. }
  901. return $plans;
  902. }
  903. =head2 B<getChildRunByName(plan,name,configurations)>
  904. =over 4
  905. =item HASHREF C<PLAN> - Test Plan definition HASHREF returned by any of the PLAN methods below.
  906. =item STRING C<NAME> - Name of run to search for within plan.
  907. =item ARRAYREF C<CONFIGURATIONS> (optional) - Names of configurations to filter runs by.
  908. =back
  909. Returns run definition HASHREF, or false if no such run is found.
  910. Convenience method using getChildRuns.
  911. Will throw a fatal error if one or more of the configurations passed does not exist in the project.
  912. =cut
  913. sub getChildRunByName {
  914. my ($self,$plan,$name,$configurations) = @_;
  915. confess("Object methods must be called by an instance") unless ref($self);
  916. confess("Plan must be HASHREF") unless defined($plan) && (reftype($plan) || 'undef') eq 'HASH';
  917. confess("Run name must be STRING") unless $self->_checkString($name);
  918. confess("Configurations must be ARRAYREF") unless !defined($configurations) || (reftype($configurations) || 'undef') eq 'ARRAY';
  919. my $runs = $self->getChildRuns($plan);
  920. return 0 if !$runs;
  921. my @pconfigs = ();
  922. #Figure out desired config IDs
  923. if (defined $configurations) {
  924. my $avail_configs = $self->getConfigurations($plan->{'project_id'});
  925. my ($cname);
  926. @pconfigs = map {$_->{'id'}} grep { $cname = $_->{'name'}; grep {$_ eq $cname} @$configurations } @$avail_configs; #Get a list of IDs from the names passed
  927. }
  928. confess("One or more configurations passed does not exist in your project!") if defined($configurations) && (scalar(@pconfigs) != scalar(@$configurations));
  929. my $found;
  930. foreach my $run (@$runs) {
  931. next if $run->{name} ne $name;
  932. next if scalar(@pconfigs) != scalar(@{$run->{'config_ids'}});
  933. #Compare run config IDs against desired, invalidate run if all conditions not satisfied
  934. $found = 0;
  935. foreach my $cid (@{$run->{'config_ids'}}) {
  936. $found++ if grep {$_ == $cid} @pconfigs;
  937. }
  938. return $run if $found == scalar(@{$run->{'config_ids'}});
  939. }
  940. return 0;
  941. }
  942. =head1 PLAN METHODS
  943. =head2 B<createPlan (project_id,name,description,milestone_id,entries)>
  944. Create a test plan.
  945. =over 4
  946. =item INTEGER C<PROJECT ID> - ID of parent project.
  947. =item STRING C<NAME> - Name of plan
  948. =item STRING C<DESCRIPTION> (optional) - Description of plan
  949. =item INTEGER C<MILESTONE_ID> (optional) - ID of milestone
  950. =item ARRAYREF C<ENTRIES> (optional) - New Runs to initially populate the plan with -- See TestRail API documentation for more advanced inputs here.
  951. =back
  952. Returns test plan definition HASHREF, or false on failure.
  953. $entries = {
  954. suite_id => 345,
  955. include_all => 1,
  956. assignedto_id => 1
  957. }
  958. $tr->createPlan(1,'Gosplan','Robo-Signed Soviet 5-year plan',22,$entries);
  959. =cut
  960. sub createPlan {
  961. my ($self,$project_id,$name,$desc,$milestone_id,$entries) = @_;
  962. confess("Object methods must be called by an instance") unless ref($self);
  963. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  964. confess("Plan name must be string") unless $self->_checkString($name);
  965. confess("Description must be string") unless !defined($desc) || $self->_checkString($desc);
  966. confess("Milestone ID must be integer") unless !defined($milestone_id) || $self->_checkInteger($milestone_id);
  967. confess("Entries must be ARRAYREF") unless !defined($entries) || (reftype($entries) || 'undef') eq 'ARRAY';
  968. my $stuff = {
  969. name => $name,
  970. description => $desc,
  971. milestone_id => $milestone_id,
  972. entries => $entries
  973. };
  974. my $result = $self->_doRequest("index.php?/api/v2/add_plan/$project_id",'POST',$stuff);
  975. return $result;
  976. }
  977. =head2 B<deletePlan (plan_id)>
  978. Deletes specified plan.
  979. =over 4
  980. =item INTEGER C<PLAN ID> - ID of plan to delete.
  981. =back
  982. Returns BOOLEAN.
  983. $tr->deletePlan(8675309);
  984. =cut
  985. sub deletePlan {
  986. my ($self,$plan_id) = @_;
  987. confess("Object methods must be called by an instance") unless ref($self);
  988. confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
  989. my $result = $self->_doRequest("index.php?/api/v2/delete_plan/$plan_id",'POST');
  990. return $result;
  991. }
  992. =head2 B<getPlans (project_id)>
  993. Gets all test plans in specified project.
  994. Like getRuns, must make multiple HTTP requests when the number of results exceeds 250.
  995. =over 4
  996. =item INTEGER C<PROJECT ID> - ID of parent project.
  997. =back
  998. Returns ARRAYREF of all plan definition HASHREFs in a project.
  999. $tr->getPlans(8);
  1000. Does not contain any information about child test runs.
  1001. Use getPlanByID or getPlanByName if you want that, in particular if you are interested in using getChildRunByName.
  1002. =cut
  1003. sub getPlans {
  1004. my ($self,$project_id) = @_;
  1005. confess("Object methods must be called by an instance") unless ref($self);
  1006. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  1007. my $initial_plans = $self->getPlansPaginated($project_id,$self->{'global_limit'},0);
  1008. return $initial_plans unless (reftype($initial_plans) || 'undef') eq 'ARRAY';
  1009. my $plans = [];
  1010. push(@$plans,@$initial_plans);
  1011. my $offset = 1;
  1012. while (scalar(@$initial_plans) == $self->{'global_limit'}) {
  1013. $initial_plans = $self->getPlansPaginated($project_id,$self->{'global_limit'},($self->{'global_limit'} * $offset));
  1014. push(@$plans,@$initial_plans);
  1015. $offset++;
  1016. }
  1017. return $plans;
  1018. }
  1019. =head2 B<getPlansPaginated (project_id,limit,offset)>
  1020. Get some plans for specified project.
  1021. =over 4
  1022. =item INTEGER C<PROJECT_ID> - ID of parent project
  1023. =item INTEGER C<LIMIT> - Number of plans to return.
  1024. =item INTEGER C<OFFSET> - Page of plans to return.
  1025. =back
  1026. Returns ARRAYREF of plan definition HASHREFs.
  1027. $someRuns = $tr->getPlansPaginated(6969,222,44);
  1028. =cut
  1029. sub getPlansPaginated {
  1030. my ($self,$project_id,$limit,$offset) = @_;
  1031. confess("Object methods must be called by an instance") unless ref($self);
  1032. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  1033. confess("Limit must be integer") unless !defined($limit) || $self->_checkInteger($limit);
  1034. confess("Offset must be integer") unless !defined($offset) || $self->_checkInteger($offset);
  1035. confess("Limit greater than ".$self->{'global_limit'}) if $limit > $self->{'global_limit'};
  1036. my $apiurl = "index.php?/api/v2/get_plans/$project_id";
  1037. $apiurl .= "&offset=$offset" if $offset;
  1038. $apiurl .= "&limit=$limit" if $limit;
  1039. return $self->_doRequest($apiurl);
  1040. }
  1041. =head2 B<getPlanByName (project_id,name)>
  1042. Gets specified plan by name.
  1043. =over 4
  1044. =item INTEGER C<PROJECT ID> - ID of parent project.
  1045. =item STRING C<NAME> - Name of test plan.
  1046. =back
  1047. Returns plan definition HASHREF.
  1048. $tr->getPlanByName(8,'GosPlan');
  1049. =cut
  1050. sub getPlanByName {
  1051. my ($self,$project_id,$name) = @_;
  1052. confess("Object methods must be called by an instance") unless ref($self);
  1053. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  1054. confess("Plan name must be string") unless $self->_checkString($name);
  1055. my $plans = $self->getPlans($project_id);
  1056. return -500 if !$plans || (reftype($plans) || 'undef') ne 'ARRAY';
  1057. foreach my $plan (@$plans) {
  1058. if ($plan->{'name'} eq $name) {
  1059. return $self->getPlanByID($plan->{'id'});
  1060. }
  1061. }
  1062. return 0;
  1063. }
  1064. =head2 B<getPlanByID (plan_id)>
  1065. Gets specified plan by ID.
  1066. =over 4
  1067. =item INTEGER C<PLAN ID> - ID of plan.
  1068. =back
  1069. Returns plan definition HASHREF.
  1070. $tr->getPlanByID(2);
  1071. =cut
  1072. sub getPlanByID {
  1073. my ($self,$plan_id) = @_;
  1074. confess("Object methods must be called by an instance") unless ref($self);
  1075. confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
  1076. return $self->_doRequest("index.php?/api/v2/get_plan/$plan_id");
  1077. }
  1078. =head2 B<getPlanSummary(plan_ID)>
  1079. Returns hashref describing the various pass, fail, etc. percentages for tests in the plan.
  1080. The 'totals' key has total cases in each status ('status' => count)
  1081. The 'percentages' key has the same, but as a percentage of the total.
  1082. =over 4
  1083. =item SCALAR C<plan_ID> - ID of your test plan.
  1084. =back
  1085. $tr->getPlanSummary($plan_id);
  1086. =cut
  1087. sub getPlanSummary {
  1088. my ($self,$plan_id) = @_;
  1089. confess("Object methods must be called by an instance") unless ref($self);
  1090. confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
  1091. my $runs = $self->getPlanByID( $plan_id );
  1092. $runs = $self->getChildRuns( $runs );
  1093. @$runs = $self->getRunSummary(@{$runs});
  1094. my $total_sum = 0;
  1095. my $ret = { plan => $plan_id };
  1096. #Compile totals
  1097. foreach my $summary ( @$runs ) {
  1098. my @elems = keys( %{ $summary->{'run_status'} } );
  1099. foreach my $key (@elems) {
  1100. $ret->{'totals'}->{$key} = 0 if !defined $ret->{'totals'}->{$key};
  1101. $ret->{'totals'}->{$key} += $summary->{'run_status'}->{$key};
  1102. $total_sum += $summary->{'run_status'}->{$key};
  1103. }
  1104. }
  1105. #Compile percentages
  1106. foreach my $key (keys(%{$ret->{'totals'}})) {
  1107. next if grep {$_ eq $key} qw{plan configs percentages};
  1108. $ret->{"percentages"}->{$key} = sprintf( "%.2f%%", ( $ret->{'totals'}->{$key} / $total_sum ) * 100 );
  1109. }
  1110. return $ret;
  1111. }
  1112. =head2 B<createRunInPlan (plan_id,suite_id,name,description,milestone_id,assigned_to_id,config_ids,case_ids)>
  1113. Create a run in a plan.
  1114. =over 4
  1115. =item INTEGER C<PLAN ID> - ID of parent project.
  1116. =item INTEGER C<SUITE ID> - ID of suite to base run on
  1117. =item STRING C<NAME> - Name of run
  1118. =item INTEGER C<ASSIGNED TO ID> (optional) - User to assign the run to
  1119. =item ARRAYREF C<CONFIG IDS> (optional) - Array of Configuration IDs (see getConfigurations) to apply to the created run
  1120. =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.
  1121. =back
  1122. Returns run definition HASHREF.
  1123. $tr->createRun(1,1345,'PlannedRun',3,[1,4,77],[3,4,5,6]);
  1124. =cut
  1125. #If you pass an array of case ids, it implies include_all is false
  1126. sub createRunInPlan {
  1127. my ($self,$plan_id,$suite_id,$name,$assignedto_id,$config_ids,$case_ids) = @_;
  1128. confess("Object methods must be called by an instance") unless ref($self);
  1129. confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
  1130. confess("Suite ID must be integer") unless $self->_checkInteger($suite_id);
  1131. confess("Name must be string") unless $self->_checkString($name);
  1132. confess("Assigned To ID must be integer") unless !defined($assignedto_id) || $self->_checkInteger($assignedto_id);
  1133. confess("Config IDs must be ARRAYREF") unless !defined($config_ids) || (reftype($config_ids) || 'undef') eq 'ARRAY';
  1134. confess("Case IDs must be ARRAYREF") unless !defined($case_ids) || (reftype($case_ids) || 'undef') eq 'ARRAY';
  1135. my $runs = [
  1136. {
  1137. config_ids => $config_ids,
  1138. include_all => defined($case_ids) ? 0 : 1,
  1139. case_ids => $case_ids
  1140. }
  1141. ];
  1142. my $stuff = {
  1143. suite_id => $suite_id,
  1144. name => $name,
  1145. assignedto_id => $assignedto_id,
  1146. include_all => defined($case_ids) ? 0 : 1,
  1147. case_ids => $case_ids,
  1148. config_ids => $config_ids,
  1149. runs => $runs
  1150. };
  1151. my $result = $self->_doRequest("index.php?/api/v2/add_plan_entry/$plan_id",'POST',$stuff);
  1152. return $result;
  1153. }
  1154. =head2 B<closePlan (plan_id)>
  1155. Close the specified plan.
  1156. =over 4
  1157. =item INTEGER C<PLAN ID> - ID of desired plan.
  1158. =back
  1159. Returns plan definition HASHREF on success, false on failure.
  1160. $tr->closePlan(75020);
  1161. =cut
  1162. sub closePlan {
  1163. my ($self,$plan_id) = @_;
  1164. confess("Object methods must be called by an instance") unless ref($self);
  1165. confess("Plan ID must be integer") unless $self->_checkInteger($plan_id);
  1166. return $self->_doRequest("index.php?/api/v2/close_plan/$plan_id",'POST');
  1167. }
  1168. =head1 MILESTONE METHODS
  1169. =head2 B<createMilestone (project_id,name,description,due_on)>
  1170. Create a milestone.
  1171. =over 4
  1172. =item INTEGER C<PROJECT ID> - ID of parent project.
  1173. =item STRING C<NAME> - Name of milestone
  1174. =item STRING C<DESCRIPTION> (optional) - Description of milestone
  1175. =item INTEGER C<DUE_ON> - Date at which milestone should be completed. Unix Timestamp.
  1176. =back
  1177. Returns milestone definition HASHREF, or false on failure.
  1178. $tr->createMilestone(1,'Patriotic victory of world perlism','Accomplish by Robo-Signed Soviet 5-year plan',time()+157788000);
  1179. =cut
  1180. sub createMilestone {
  1181. my ($self,$project_id,$name,$desc,$due_on) = @_;
  1182. confess("Object methods must be called by an instance") unless ref($self);
  1183. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  1184. confess("Name must be string") unless $self->_checkString($name);
  1185. confess("Description must be string") unless !defined($desc) || $self->_checkString($desc);
  1186. confess("Due on must be unix time stamp (integer)") unless !defined($due_on) || $self->_checkInteger($due_on);
  1187. my $stuff = {
  1188. name => $name,
  1189. description => $desc,
  1190. due_on => $due_on # unix timestamp
  1191. };
  1192. my $result = $self->_doRequest("index.php?/api/v2/add_milestone/$project_id",'POST',$stuff);
  1193. return $result;
  1194. }
  1195. =head2 B<deleteMilestone (milestone_id)>
  1196. Deletes specified milestone.
  1197. =over 4
  1198. =item INTEGER C<MILESTONE ID> - ID of milestone to delete.
  1199. =back
  1200. Returns BOOLEAN.
  1201. $tr->deleteMilestone(86);
  1202. =cut
  1203. sub deleteMilestone {
  1204. my ($self,$milestone_id) = @_;
  1205. confess("Object methods must be called by an instance") unless ref($self);
  1206. confess("Milestone ID must be integer") unless $self->_checkInteger($milestone_id);
  1207. my $result = $self->_doRequest("index.php?/api/v2/delete_milestone/$milestone_id",'POST');
  1208. return $result;
  1209. }
  1210. =head2 B<getMilestones (project_id)>
  1211. Get milestones for some project.
  1212. =over 4
  1213. =item INTEGER C<PROJECT ID> - ID of parent project.
  1214. =back
  1215. Returns ARRAYREF of milestone definition HASHREFs.
  1216. $tr->getMilestones(8);
  1217. =cut
  1218. sub getMilestones {
  1219. my ($self,$project_id) = @_;
  1220. confess("Object methods must be called by an instance") unless ref($self);
  1221. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  1222. return $self->_doRequest("index.php?/api/v2/get_milestones/$project_id");
  1223. }
  1224. =head2 B<getMilestoneByName (project_id,name)>
  1225. Gets specified milestone by name.
  1226. =over 4
  1227. =item INTEGER C<PROJECT ID> - ID of parent project.
  1228. =item STRING C<NAME> - Name of milestone.
  1229. =back
  1230. Returns milestone definition HASHREF.
  1231. $tr->getMilestoneByName(8,'whee');
  1232. =cut
  1233. sub getMilestoneByName {
  1234. my ($self,$project_id,$name) = @_;
  1235. confess("Object methods must be called by an instance") unless ref($self);
  1236. confess("Project ID must be integer") unless $self->_checkInteger($project_id);
  1237. confess("Milestone name must be string") unless $self->_checkString($name);
  1238. my $milestones = $self->getMilestones($project_id);
  1239. return -500 if !$milestones || (reftype($milestones) || 'undef') ne 'ARRAY';
  1240. foreach my $milestone (@$milestones) {
  1241. return $milestone if $milestone->{'name'} eq $name;
  1242. }
  1243. return 0;
  1244. }
  1245. =head2 B<getMilestoneByID (milestone_id)>
  1246. Gets specified milestone by ID.
  1247. =over 4
  1248. =item INTEGER C<MILESTONE ID> - ID of milestone.
  1249. =back
  1250. Returns milestone definition HASHREF.
  1251. $tr->getMilestoneByID(2);
  1252. =cut
  1253. sub getMilestoneByID {
  1254. my ($self,$milestone_id) = @_;
  1255. confess("Object methods must be called by an instance") unless ref($self);
  1256. confess("Milestone ID must be integer") unless $self->_checkInteger($milestone_id);
  1257. return $self->_doRequest("index.php?/api/v2/get_milestone/$milestone_id");
  1258. }
  1259. =head1 TEST METHODS
  1260. =head2 B<getTests (run_id,status_ids,assignedto_ids)>
  1261. Get tests for some run. Optionally filter by provided status_ids and assigned_to ids.
  1262. =over 4
  1263. =item INTEGER C<RUN ID> - ID of parent run.
  1264. =item ARRAYREF C<STATUS IDS> (optional) - IDs of relevant test statuses to filter by. Get with getPossibleTestStatuses.
  1265. =item ARRAYREF C<ASSIGNEDTO IDS> (optional) - IDs of users assigned to test to filter by. Get with getUsers.
  1266. =back
  1267. Returns ARRAYREF of test definition HASHREFs.
  1268. $tr->getTests(8,[1,2,3],[2]);
  1269. =cut
  1270. sub getTests {
  1271. my ($self,$run_id,$status_ids,$assignedto_ids,$section_ids) = @_;
  1272. confess("Object methods must be called by an instance") unless ref($self);
  1273. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  1274. confess("Status IDs must be ARRAYREF") unless !defined($status_ids) || ( reftype($status_ids) || 'undef' ) eq 'ARRAY';
  1275. confess("Assigned to IDs must be ARRAYREF") unless !defined($assignedto_ids) || ( reftype($assignedto_ids) || 'undef' ) eq 'ARRAY';
  1276. my $query_string = '';
  1277. $query_string = '&status_id='.join(',',@$status_ids) if defined($status_ids) && scalar(@$status_ids);
  1278. my $results = $self->_doRequest("index.php?/api/v2/get_tests/$run_id$query_string");
  1279. @$results = grep {my $aid = $_->{'assignedto_id'}; grep {defined($aid) && $aid == $_} @$assignedto_ids} @$results if defined($assignedto_ids) && scalar(@$assignedto_ids);
  1280. return $results;
  1281. }
  1282. =head2 B<getTestByName (run_id,name)>
  1283. Gets specified test by name.
  1284. =over 4
  1285. =item INTEGER C<RUN ID> - ID of parent run.
  1286. =item STRING C<NAME> - Name of milestone.
  1287. =back
  1288. Returns test definition HASHREF.
  1289. $tr->getTestByName(36,'wheeTest');
  1290. =cut
  1291. sub getTestByName {
  1292. my ($self,$run_id,$name) = @_;
  1293. confess("Object methods must be called by an instance") unless ref($self);
  1294. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  1295. confess("Test name must be string") unless $self->_checkString($name);
  1296. my $tests = $self->getTests($run_id);
  1297. return -500 if !$tests || (reftype($tests) || 'undef') ne 'ARRAY';
  1298. foreach my $test (@$tests) {
  1299. return $test if $test->{'title'} eq $name;
  1300. }
  1301. return 0;
  1302. }
  1303. =head2 B<getTestByID (test_id)>
  1304. Gets specified test by ID.
  1305. =over 4
  1306. =item INTEGER C<TEST ID> - ID of test.
  1307. =back
  1308. Returns test definition HASHREF.
  1309. $tr->getTestByID(222222);
  1310. =cut
  1311. sub getTestByID {
  1312. my ($self,$test_id) = @_;
  1313. confess("Object methods must be called by an instance") unless ref($self);
  1314. confess("Test ID must be integer") unless $self->_checkInteger($test_id);
  1315. return $self->_doRequest("index.php?/api/v2/get_test/$test_id");
  1316. }
  1317. =head2 B<getTestResultFields()>
  1318. Gets custom fields that can be set for tests.
  1319. Returns ARRAYREF of result definition HASHREFs.
  1320. =cut
  1321. sub getTestResultFields {
  1322. my $self = shift;
  1323. confess("Object methods must be called by an instance") unless ref($self);
  1324. return $self->{'tr_fields'} if defined($self->{'tr_fields'}); #cache
  1325. $self->{'tr_fields'} = $self->_doRequest('index.php?/api/v2/get_result_fields');
  1326. return $self->{'tr_fields'};
  1327. }
  1328. =head2 B<getTestResultFieldByName(SYSTEM_NAME,PROJECT_ID)>
  1329. Gets a test result field by it's system name. Optionally filter by project ID.
  1330. =over 4
  1331. =item B<SYSTEM NAME> - STRING: system name of a result field.
  1332. =item B<PROJECT ID> - INTEGER (optional): Filter by whether or not the field is enabled for said project
  1333. =back
  1334. =cut
  1335. sub getTestResultFieldByName {
  1336. my ($self,$system_name,$project_id) = @_;
  1337. confess("Object methods must be called by an instance") unless ref($self);
  1338. confess("System name must be string") unless $self->_checkString($system_name);
  1339. my @candidates = grep { $_->{'name'} eq $system_name} @{$self->getTestResultFields()};
  1340. return 0 if !scalar(@candidates); #No such name
  1341. return -1 if ref($candidates[0]) ne 'HASH';
  1342. return -2 if ref($candidates[0]->{'configs'}) ne 'ARRAY' && !scalar(@{$candidates[0]->{'configs'}}); #bogofilter
  1343. #Give it to the user
  1344. my $ret = $candidates[0]; #copy/save for later
  1345. return $ret if !defined($project_id);
  1346. #Filter by project ID
  1347. foreach my $config (@{$candidates[0]->{'configs'}}) {
  1348. return $ret if ( grep { $_ == $project_id} @{ $config->{'context'}->{'project_ids'} } )
  1349. }
  1350. return -3;
  1351. }
  1352. =head2 B<getPossibleTestStatuses()>
  1353. Gets all possible statuses a test can be set to.
  1354. Returns ARRAYREF of status definition HASHREFs.
  1355. =cut
  1356. sub getPossibleTestStatuses {
  1357. my $self = shift;
  1358. confess("Object methods must be called by an instance") unless ref($self);
  1359. return $self->_doRequest('index.php?/api/v2/get_statuses');
  1360. }
  1361. =head2 statusNamesToIds(names)
  1362. Convenience method to translate a list of statuses to TestRail status IDs.
  1363. The names referred to here are 'internal names' rather than the labels shown in TestRail.
  1364. =over 4
  1365. =item ARRAY C<NAMES> - Array of status names to translate to IDs.
  1366. =back
  1367. Returns ARRAY of status IDs.
  1368. Throws an exception in the case of one (or more) of the names not corresponding to a valid test status.
  1369. =cut
  1370. sub statusNamesToIds {
  1371. my ($self,@names) = @_;
  1372. confess("Object methods must be called by an instance") unless ref($self);
  1373. confess("At least one status name must be provided") if !scalar(@names);
  1374. my @ret = grep {defined $_} map {my $status = $_; my @list = grep {$status->{'name'} eq $_} @names; scalar(@list) ? $status->{'id'} : undef} @{$self->getPossibleTestStatuses()};
  1375. confess("One or more status names provided does not exist in TestRail.") unless scalar(@names) == scalar(@ret);
  1376. return @ret;
  1377. };
  1378. =head2 B<createTestResults(test_id,status_id,comment,options,custom_options)>
  1379. Creates a result entry for a test.
  1380. =over 4
  1381. =item INTEGER C<TEST_ID> - ID of desired test
  1382. =item INTEGER C<STATUS_ID> - ID of desired test result status
  1383. =item STRING C<COMMENT> (optional) - Any comments about this result
  1384. =item HASHREF C<OPTIONS> (optional) - Various "Baked-In" options that can be set for test results. See TR docs for more information.
  1385. =item HASHREF C<CUSTOM OPTIONS> (optional) - Options to set for custom fields. See buildStepResults for a simple way to post up custom steps.
  1386. =back
  1387. Returns result definition HASHREF.
  1388. $options = {
  1389. elapsed => '30m 22s',
  1390. defects => ['TSR-3','BOOM-44'],
  1391. version => '6969'
  1392. };
  1393. $custom_options = {
  1394. step_results => [
  1395. {
  1396. content => 'Step 1',
  1397. expected => "Bought Groceries",
  1398. actual => "No Dinero!",
  1399. status_id => 2
  1400. },
  1401. {
  1402. content => 'Step 2',
  1403. expected => 'Ate Dinner',
  1404. actual => 'Went Hungry',
  1405. status_id => 2
  1406. }
  1407. ]
  1408. };
  1409. $res = $tr->createTestResults(1,2,'Test failed because it was all like WAAAAAAA when I poked it',$options,$custom_options);
  1410. =cut
  1411. sub createTestResults {
  1412. my ($self,$test_id,$status_id,$comment,$opts,$custom_fields) = @_;
  1413. confess("Object methods must be called by an instance") unless ref($self);
  1414. confess("Test ID must be integer") unless $self->_checkInteger($test_id);
  1415. confess("Status ID must be integer") unless $self->_checkInteger($status_id);
  1416. confess("Comment must be string") unless !defined($comment) || $self->_checkString($comment);
  1417. confess("Options must be HASHREF") unless !defined($opts) || (reftype($opts) || 'undef') eq 'HASH';
  1418. confess("Custom Options must be HASHREF") unless !defined($custom_fields) || (reftype($custom_fields) || 'undef') eq 'HASH';
  1419. my $stuff = {
  1420. status_id => $status_id,
  1421. comment => $comment
  1422. };
  1423. #Handle options
  1424. if (defined($opts) && reftype($opts) eq 'HASH') {
  1425. $stuff->{'version'} = defined($opts->{'version'}) ? $opts->{'version'} : undef;
  1426. $stuff->{'elapsed'} = defined($opts->{'elapsed'}) ? $opts->{'elapsed'} : undef;
  1427. $stuff->{'defects'} = defined($opts->{'defects'}) ? join(',',@{$opts->{'defects'}}) : undef;
  1428. $stuff->{'assignedto_id'} = defined($opts->{'assignedto_id'}) ? $opts->{'assignedto_id'} : undef;
  1429. }
  1430. #Handle custom fields
  1431. if (defined($custom_fields) && reftype($custom_fields) eq 'HASH') {
  1432. foreach my $field (keys(%$custom_fields)) {
  1433. $stuff->{"custom_$field"} = $custom_fields->{$field};
  1434. }
  1435. }
  1436. return $self->_doRequest("index.php?/api/v2/add_result/$test_id",'POST',$stuff);
  1437. }
  1438. =head2 bulkAddResults(run_id,results)
  1439. Add multiple results to a run, where each result is a HASHREF with keys as outlined in the get_results API call documentation.
  1440. =over 4
  1441. =item INTEGER C<RUN_ID> - ID of desired run to add results to
  1442. =item ARRAYREF C<RESULTS> - Array of result objects to upload.
  1443. =back
  1444. Returns ARRAYREF of result definition HASHREFs.
  1445. =cut
  1446. sub bulkAddResults {
  1447. my ($self, $run_id, $results) = @_;
  1448. confess("Object methods must be called by an instance") unless ref($self);
  1449. confess("Run ID must be integer") unless $self->_checkInteger($run_id);
  1450. confess("results must be arrayref") unless ( reftype($results) || 'undef' ) eq 'ARRAY';
  1451. return $self->_doRequest("index.php?/api/v2/add_results/$run_id", 'POST', { 'results' => $results });
  1452. }
  1453. =head2 B<getTestResults(test_id,limit,offset)>
  1454. Get the recorded results for desired test, limiting output to 'limit' entries.
  1455. =over 4
  1456. =item INTEGER C<TEST_ID> - ID of desired test
  1457. =item POSITIVE INTEGER C<LIMIT> (OPTIONAL) - provide no more than this number of results.
  1458. =item INTEGER C<OFFSET> (OPTIONAL) - Offset to begin viewing result set at.
  1459. =back
  1460. Returns ARRAYREF of result definition HASHREFs.
  1461. =cut
  1462. sub getTestResults {
  1463. my ($self,$test_id,$limit,$offset) = @_;
  1464. confess("Object methods must be called by an instance") unless ref($self);
  1465. confess("Test ID must be positive integer") unless $self->_checkInteger($test_id);
  1466. confess("Result limitation must be positive integer") unless !defined($limit) || ($self->_checkInteger($limit) && $limit > 0);
  1467. confess("Result offset must be integer") unless !defined($offset) || $self->_checkInteger($offset);
  1468. my $url = "index.php?/api/v2/get_results/$test_id";
  1469. $url .= "&limit=$limit" if defined($limit);
  1470. $url .= "&offset=$offset" if defined($offset);
  1471. return $self->_doRequest($url);
  1472. }
  1473. =head1 CONFIGURATION METHODS
  1474. =head2 B<getConfigurationGroups(project_id)>
  1475. Gets the available configuration groups for a project, with their configurations as children.
  1476. Basically a direct wrapper of The 'get_configs' api call, with caching tacked on.
  1477. =over 4
  1478. =item INTEGER C<PROJECT_ID> - ID of relevant project
  1479. =back
  1480. Returns ARRAYREF of configuration group definition HASHREFs.
  1481. =cut
  1482. sub getConfigurationGroups {
  1483. my ($self,$project_id) = @_;
  1484. confess("Object methods must be called by an instance") unless ref($self);
  1485. confess("Project ID must be positive integer") unless $self->_checkInteger($project_id);
  1486. my $url = "index.php?/api/v2/get_configs/$project_id";
  1487. return $self->{'configurations'}->{$project_id} if $self->{'configurations'}->{$project_id}; #cache this since we can't change it with the API
  1488. $self->{'configurations'}->{$project_id} = $self->_doRequest($url);
  1489. return $self->{'configurations'}->{$project_id};
  1490. }
  1491. =head2 B<getConfigurations(project_id)>
  1492. Gets the available configurations for a project.
  1493. Mostly for convenience (no need to write a boilerplate loop over the groups).
  1494. =over 4
  1495. =item INTEGER C<PROJECT_ID> - ID of relevant project
  1496. =back
  1497. Returns ARRAYREF of configuration definition HASHREFs.
  1498. Returns result of getConfigurationGroups (likely -500) in the event that call fails.
  1499. =cut
  1500. sub getConfigurations {
  1501. my ($self,$project_id) = @_;
  1502. confess("Object methods must be called by an instance") unless ref($self);
  1503. confess("Project ID must be positive integer") unless $self->_checkInteger($project_id);
  1504. my $cgroups = $self->getConfigurationGroups($project_id);
  1505. my $configs = [];
  1506. return $cgroups unless (reftype($cgroups) || 'undef') eq 'ARRAY';
  1507. foreach my $cfg (@$cgroups) {
  1508. push(@$configs, @{$cfg->{'configs'}});
  1509. }
  1510. return $configs;
  1511. }
  1512. =head2 B<translateConfigNamesToIds(project_id,configs)>
  1513. Transforms a list of configuration names into a list of config IDs.
  1514. =over 4
  1515. =item INTEGER C<PROJECT_ID> - Relevant project ID for configs.
  1516. =item ARRAYREF C<CONFIGS> - Array ref of config names
  1517. =back
  1518. Returns ARRAYREF of configuration names, with undef values for unknown configuration names.
  1519. =cut
  1520. sub translateConfigNamesToIds {
  1521. my ($self,$project_id,$configs) = @_;
  1522. confess("Object methods must be called by an instance") unless ref($self);
  1523. confess("Project ID must be positive integer") unless $self->_checkInteger($project_id);
  1524. confess("Configs must be arrayref") unless (reftype($configs) || 'undef') eq 'ARRAY';
  1525. return [] if !scalar(@$configs);
  1526. my $existing_configs = $self->getConfigurations($project_id);
  1527. return map {undef} @$configs if (reftype($existing_configs) || 'undef') ne 'ARRAY';
  1528. my @ret = map {my $name = $_; my @candidates = grep { $name eq $_->{'name'} } @$existing_configs; scalar(@candidates) ? $candidates[0]->{'id'} : undef } @$configs;
  1529. return \@ret;
  1530. }
  1531. =head1 STATIC METHODS
  1532. =head2 B<buildStepResults(content,expected,actual,status_id)>
  1533. Convenience method to build the stepResult hashes seen in the custom options for getTestResults.
  1534. =over 4
  1535. =item STRING C<CONTENT> (optional) - The step itself.
  1536. =item STRING C<EXPECTED> (optional) - Expected result of test step.
  1537. =item STRING C<ACTUAL> (optional) - Actual result of test step
  1538. =item INTEGER C<STATUS ID> (optional) - Status ID of result
  1539. =back
  1540. =cut
  1541. #Convenience method for building stepResults
  1542. sub buildStepResults {
  1543. my ($content,$expected,$actual,$status_id) = @_;
  1544. return {
  1545. content => $content,
  1546. expected => $expected,
  1547. actual => $actual,
  1548. status_id => $status_id
  1549. };
  1550. }
  1551. #Type checks
  1552. sub _checkInteger {
  1553. shift;
  1554. my $integer = shift;
  1555. return ( defined $integer && looks_like_number($integer) && int($integer) == $integer );
  1556. }
  1557. sub _checkString {
  1558. shift;
  1559. my $str = shift;
  1560. return ( defined($str) && !ref($str) );
  1561. }
  1562. 1;
  1563. __END__
  1564. =head1 SEE ALSO
  1565. L<HTTP::Request>
  1566. L<LWP::UserAgent>
  1567. L<JSON::MaybeXS>
  1568. L<http://docs.gurock.com/testrail-api2/start>
  1569. =head1 SPECIAL THANKS
  1570. Thanks to cPanel Inc, for graciously funding the creation of this module.