Parser.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. # ABSTRACT: Upload your TAP results to TestRail
  2. # PODNAME: Test::Rail::Parser
  3. package Test::Rail::Parser;
  4. use strict;
  5. use warnings;
  6. use utf8;
  7. use parent qw/TAP::Parser/;
  8. use Carp qw{cluck confess};
  9. use TestRail::API;
  10. use Scalar::Util qw{reftype};
  11. use File::Basename qw{basename};
  12. our $self;
  13. =head1 DESCRIPTION
  14. A TAP parser which will upload your test results to a TestRail install.
  15. Has several options as to how you might want to upload said results.
  16. Subclass of L<TAP::Parser>, see that for usage past the constructor.
  17. You should probably use L<App::Prove::Plugin::TestRail> or the bundled program testrail-report for day-to-day usage...
  18. unless you need to subclass this. In that case a couple of options have been exposed for your convenience.
  19. =cut
  20. =head1 CONSTRUCTOR
  21. =head2 B<new(OPTIONS)>
  22. Get the TAP Parser ready to talk to TestRail, and register a bunch of callbacks to upload test results.
  23. =over 4
  24. =item B<OPTIONS> - HASHREF -- Keys are as follows:
  25. =over 4
  26. =item B<apiurl> - STRING: Full URI to your TestRail installation.
  27. =item B<user> - STRING: Name of your TestRail user.
  28. =item B<pass> - STRING: Said user's password.
  29. =item B<debug> - BOOLEAN: Print a bunch of extra messages
  30. =item B<browser> - OBJECT: Something like an LWP::UserAgent. Useful for mocking with L<Test::LWP::UserAgent::TestRailMock>.
  31. =item B<run> - STRING (optional): name of desired run. Required if run_id not passed.
  32. =item B<run_id> - INTEGER (optional): ID of desired run. Required if run not passed.
  33. =item B<project> - STRING (optional): name of project containing your desired run. Required if project_id not passed.
  34. =item B<project_id> - INTEGER (optional): ID of project containing your desired run. Required if project not passed.
  35. =item B<step_results> - STRING (optional): 'internal name' of the 'step_results' type field available for your project. Mutually exclusive with case_per_ok
  36. =item B<case_per_ok> - BOOLEAN (optional): Consider test files to correspond to section names, and test steps (OKs) to correspond to tests in TestRail. Mutually exclusive with step_results.
  37. =item B<result_options> - HASHREF (optional): Extra options to set with your result. See L<TestRail::API>'s createTestResults function for more information.
  38. =item B<custom_options> - HASHREF (optional): Custom options to set with your result. See L<TestRail::API>'s createTestResults function for more information. step_results will be set here, if the option is passed.
  39. =back
  40. =back
  41. It is worth noting that if neither step_results or case_per_ok is passed, that the test will be passed if it has no problems of any sort, failed otherwise.
  42. In both this mode and step_results, the file name of the test is expected to correspond to the test name in TestRail.
  43. =cut
  44. sub new {
  45. my ($class,$opts) = @_;
  46. our $self;
  47. #Load our callbacks
  48. $opts->{'callbacks'} = {
  49. 'test' => \&testCallback,
  50. 'comment' => \&commentCallback,
  51. 'unknown' => \&unknownCallback,
  52. 'EOF' => \&EOFCallback
  53. };
  54. my $tropts = {
  55. 'apiurl' => delete $opts->{'apiurl'},
  56. 'user' => delete $opts->{'user'},
  57. 'pass' => delete $opts->{'pass'},
  58. 'debug' => delete $opts->{'debug'},
  59. 'browser' => delete $opts->{'browser'},
  60. 'run' => delete $opts->{'run'},
  61. 'run_id' => delete $opts->{'run_id'},
  62. 'project' => delete $opts->{'project'},
  63. 'project_id' => delete $opts->{'project_id'},
  64. 'step_results' => delete $opts->{'step_results'},
  65. 'case_per_ok' => delete $opts->{'case_per_ok'},
  66. #Stubs for extension by subclassers
  67. 'result_options' => delete $opts->{'result_options'},
  68. 'result_custom_options' => delete $opts->{'result_custom_options'}
  69. };
  70. #Allow natural confessing from constructor
  71. my $tr = TestRail::API->new($tropts->{'apiurl'},$tropts->{'user'},$tropts->{'pass'},$tropts->{'debug'});
  72. $tropts->{'testrail'} = $tr;
  73. $tr->{'browser'} = $tropts->{'browser'} if defined($tropts->{'browser'}); #allow mocks
  74. $tr->{'debug'} = 0; #Always suppress in production
  75. #Get project ID from name, if not provided
  76. if (!defined($tropts->{'project_id'})) {
  77. my $pname = $tropts->{'project'};
  78. $tropts->{'project'} = $tr->getProjectByName($pname);
  79. confess("Could not list projects! Shutting down.") if ($tropts->{'project'} == -500);
  80. if (!$tropts->{'project'}) {
  81. confess("No project (or project_id) provided, or that which was provided was invalid!");
  82. }
  83. } else {
  84. $tropts->{'project'} = $tr->getProjectByID($tropts->{'project_id'});
  85. confess("No such project with ID $tropts->{project_id}!") if !$tropts->{'project'};
  86. }
  87. $tropts->{'project_id'} = $tropts->{'project'}->{'id'};
  88. #Discover possible test statuses
  89. $tropts->{'statuses'} = $tr->getPossibleTestStatuses();
  90. my @ok = grep {$_->{'name'} eq 'passed'} @{$tropts->{'statuses'}};
  91. my @not_ok = grep {$_->{'name'} eq 'failed'} @{$tropts->{'statuses'}};
  92. my @skip = grep {$_->{'name'} eq 'skip'} @{$tropts->{'statuses'}};
  93. my @todof = grep {$_->{'name'} eq 'todo_fail'} @{$tropts->{'statuses'}};
  94. my @todop = grep {$_->{'name'} eq 'todo_pass'} @{$tropts->{'statuses'}};
  95. confess("No status with internal name 'passed' in TestRail!") unless scalar(@ok);
  96. confess("No status with internal name 'failed' in TestRail!") unless scalar(@not_ok);
  97. confess("No status with internal name 'skip' in TestRail!") unless scalar(@skip);
  98. confess("No status with internal name 'todo_fail' in TestRail!") unless scalar(@todof);
  99. confess("No status with internal name 'todo_pass' in TestRail!") unless scalar(@todop);
  100. $tropts->{'ok'} = $ok[0];
  101. $tropts->{'not_ok'} = $not_ok[0];
  102. $tropts->{'skip'} = $skip[0];
  103. $tropts->{'todo_fail'} = $todof[0];
  104. $tropts->{'todo_pass'} = $todop[0];
  105. #Grab suite from run
  106. my $run_id = $tropts->{'run_id'};
  107. if ($tropts->{'run'}) {
  108. my $run = $tr->getRunByName($tropts->{'project_id'},$tropts->{'run'});
  109. if (defined($run) && (reftype($run) || 'undef') eq 'HASH') {
  110. $tropts->{'run'} = $run;
  111. $tropts->{'run_id'} = $run->{'id'};
  112. }
  113. } else {
  114. $tropts->{'run'} = $tr->getRunByID($run_id);
  115. }
  116. confess("No run ID provided, and no run with specified name exists!") if !$tropts->{'run_id'};
  117. $self = $class->SUPER::new($opts);
  118. if (defined($self->{'_iterator'}->{'command'}) && reftype($self->{'_iterator'}->{'command'}) eq 'ARRAY' ) {
  119. $self->{'file'} = $self->{'_iterator'}->{'command'}->[-1];
  120. print "PROCESSING RESULTS FROM TEST FILE: $self->{'file'}\n";
  121. }
  122. #Make sure the step results field passed exists on the system
  123. $tropts->{'step_results'} = $tr->getTestResultFieldByName($tropts->{'step_results'},$tropts->{'project_id'}) if defined $tropts->{'step_results'};
  124. $self->{'tr_opts'} = $tropts;
  125. $self->{'errors'} = 0;
  126. return $self;
  127. }
  128. =head1 PARSER CALLBACKS
  129. =head2 unknownCallback
  130. Called whenever we encounter an unknown line in TAP. Only useful for prove output, as we might pick a filename out of there.
  131. Stores said filename for future use if encountered.
  132. =cut
  133. # Look for file boundaries, etc.
  134. sub unknownCallback {
  135. my (@args) = @_;
  136. our $self;
  137. my $line = $args[0]->as_string;
  138. #try to pick out the filename if we are running this on TAP in files
  139. #old prove
  140. if ($line =~ /^Running\s(.*)/) {
  141. #TODO figure out which testsuite this implies
  142. $self->{'file'} = $1;
  143. print "PROCESSING RESULTS FROM TEST FILE: $self->{'file'}\n";
  144. }
  145. #RAW tap #XXX this regex could be improved
  146. if ($line =~ /(.*)\s\.\.$/) {
  147. $self->{'file'} = $1 unless $line =~ /^[ok|not ok] - /; #a little more careful
  148. }
  149. print "$line\n" if ($line =~ /^error/i);
  150. }
  151. =head2 commentCallback
  152. Grabs comments preceding a test so that we can include that as the test's notes.
  153. Especially useful when merge=1 is passed to the constructor.
  154. =cut
  155. # Register the current suite or test desc for use by test callback, if the line begins with the special magic words
  156. sub commentCallback {
  157. my (@args) = @_;
  158. our $self;
  159. my $line = $args[0]->as_string;
  160. if ($line =~ m/^#TESTDESC:\s*/) {
  161. $self->{'tr_opts'}->{'test_desc'} = $line;
  162. $self->{'tr_opts'}->{'test_desc'} =~ s/^#TESTDESC:\s*//g;
  163. return;
  164. }
  165. #keep all comments before a test that aren't these special directives to save in NOTES field of reportTCResult
  166. $self->{'tr_opts'}->{'test_notes'} .= "$line\n";
  167. }
  168. =head2 testCallback
  169. If we are using step_results, append it to the step results array for use at EOF.
  170. If we are using case_per_ok, update TestRail per case.
  171. Otherwise, do nothing.
  172. =cut
  173. sub testCallback {
  174. my (@args) = @_;
  175. my $test = $args[0];
  176. our $self;
  177. #Don't do anything if we don't want to map TR case => ok or use step-by-step results
  178. if ( !($self->{'tr_opts'}->{'step_results'} || $self->{'tr_opts'}->{'case_per_ok'}) ) {
  179. print "Neither step_results of case_per_ok set. No action to be taken, except on a whole test basis.\n" if $self->{'tr_opts'}->{'debug'};
  180. return 1;
  181. }
  182. if ($self->{'tr_opts'}->{'step_results'} && $self->{'tr_opts'}->{'case_per_ok'}) {
  183. cluck("ERROR: step_options and case_per_ok options are mutually exclusive!");
  184. $self->{'errors'}++;
  185. return 0;
  186. }
  187. #Fail on unplanned tests
  188. if ($test->is_unplanned()) {
  189. cluck("ERROR: Unplanned test detected. Will not attempt to upload results.");
  190. $self->{'errors'}++;
  191. return 0;
  192. }
  193. #Default assumption is that case name is step text (case_per_ok), unless...
  194. my $line = $test->as_string;
  195. $line =~ s/^(ok|not ok)\s[0-9]*\s-\s//g;
  196. my $test_name = $line;
  197. my $run_id = $self->{'tr_opts'}->{'run_id'};
  198. print "Assuming test name is '$test_name'...\n" if $self->{'tr_opts'}->{'debug'} && !$self->{'tr_opts'}->{'step_results'};
  199. my $todo_reason;
  200. #Setup args to pass to function
  201. my $status = $self->{'tr_opts'}->{'not_ok'}->{'id'};
  202. if ($test->is_actual_ok()) {
  203. $status = $self->{'tr_opts'}->{'ok'}->{'id'};
  204. if ($test->has_skip()) {
  205. $status = $self->{'tr_opts'}->{'skip'}->{'id'};
  206. $test_name =~ s/^(ok|not ok)\s[0-9]*\s//g;
  207. $test_name =~ s/^# skip //gi;
  208. }
  209. if ($test->has_todo()) {
  210. $status = $self->{'tr_opts'}->{'todo_pass'}->{'id'};
  211. $test_name =~ s/^(ok|not ok)\s[0-9]*\s//g;
  212. $test_name =~ s/(^# todo & skip )//gi; #handle todo_skip
  213. $test_name =~ s/ # todo\s(.*)$//gi;
  214. $todo_reason = $1;
  215. }
  216. } else {
  217. if ($test->has_todo()) {
  218. $status = $self->{'tr_opts'}->{'todo_pass'}->{'id'};
  219. $test_name =~ s/^(ok|not ok)\s[0-9]*\s//g;
  220. $test_name =~ s/^# todo & skip //gi; #handle todo_skip
  221. $test_name =~ s/# todo\s(.*)$//gi;
  222. $todo_reason = $1;
  223. }
  224. }
  225. #If this is a TODO, set the reason in the notes
  226. $self->{'tr_opts'}->{'test_notes'} .= "\nTODO reason: $todo_reason\n" if $todo_reason;
  227. #Setup step options and exit if that's the mode we be rollin'
  228. if ($self->{'tr_opts'}->{'step_results'}) {
  229. $self->{'tr_opts'}->{'result_custom_options'} = {} if !defined $self->{'tr_opts'}->{'result_custom_options'};
  230. $self->{'tr_opts'}->{'result_custom_options'}->{'step_results'} = [] if !defined $self->{'tr_opts'}->{'result_custom_options'}->{'step_results'};
  231. #XXX Obviously getting the 'expected' and 'actual' from the tap DIAGs would be ideal
  232. push(
  233. @{$self->{'tr_opts'}->{'result_custom_options'}->{'step_results'}},
  234. TestRail::API::buildStepResults($line,"Good result","Bad Result",$status)
  235. );
  236. print "Appended step results.\n" if $self->{'tr_opts'}->{'debug'};
  237. return 1;
  238. }
  239. #Optional args
  240. my $notes = $self->{'tr_opts'}->{'test_notes'};
  241. my $options = $self->{'tr_opts'}->{'result_options'};
  242. my $custom_options = $self->{'tr_opts'}->{'result_custom_options'};
  243. _set_result($run_id,$test_name,$status,$notes,$options,$custom_options);
  244. #Blank out test description in anticipation of next test
  245. # also blank out notes
  246. $self->{'tr_opts'}->{'test_notes'} = undef;
  247. $self->{'tr_opts'}->{'test_desc'} = undef;
  248. }
  249. =head2 EOFCallback
  250. If we are running in step_results mode, send over all the step results to TestRail.
  251. If we are running in case_per_ok mode, do nothing.
  252. Otherwise, upload the overall results of the test to TestRail.
  253. =cut
  254. sub EOFCallback {
  255. our $self;
  256. if (!(!$self->{'tr_opts'}->{'step_results'} xor $self->{'tr_opts'}->{'case_per_ok'})) {
  257. print "Nothing left to do.\n";
  258. undef $self->{'tr_opts'};
  259. return 1;
  260. }
  261. #Fail if the file is not set
  262. if (!defined($self->{'file'})) {
  263. cluck("ERROR: Cannot detect filename, will not be able to find a Test Case with that name");
  264. $self->{'errors'}++;
  265. return 0;
  266. }
  267. my $run_id = $self->{'tr_opts'}->{'run_id'};
  268. my $test_name = basename($self->{'file'});
  269. my $status = $self->{'tr_opts'}->{'ok'}->{'id'};
  270. $status = $self->{'tr_opts'}->{'not_ok'}->{'id'} if $self->has_problems();
  271. $status = $self->{'tr_opts'}->{'skip'}->{'id'} if $self->skip_all();
  272. #Optional args
  273. my $notes = $self->{'tr_opts'}->{'test_notes'};
  274. my $options = $self->{'tr_opts'}->{'result_options'};
  275. my $custom_options = $self->{'tr_opts'}->{'result_custom_options'};
  276. print "Setting results...\n";
  277. my $cres = _set_result($run_id,$test_name,$status,$notes,$options,$custom_options);
  278. undef $self->{'tr_opts'};
  279. return $cres;
  280. }
  281. sub _set_result {
  282. my ($run_id,$test_name,$status,$notes,$options,$custom_options) = @_;
  283. our $self;
  284. my $tc;
  285. print "Attempting to find case by title '".$test_name."'...\n";
  286. $tc = $self->{'tr_opts'}->{'testrail'}->getTestByName($run_id,$test_name);
  287. if (!defined($tc) || (reftype($tc) || 'undef') ne 'HASH') {
  288. cluck("ERROR: Could not find test case: $tc");
  289. $self->{'errors'}++;
  290. return 0;
  291. }
  292. my $xid = $tc ? $tc->{'id'} : '???';
  293. my $cres;
  294. #Set test result
  295. if ($tc) {
  296. print "Reporting result of case $xid in run $self->{'tr_opts'}->{'run_id'} as status '$status'...";
  297. # createTestResults(test_id,status_id,comment,options,custom_options)
  298. $cres = $self->{'tr_opts'}->{'testrail'}->createTestResults($tc->{'id'},$status, $notes, $options, $custom_options);
  299. print "OK! (set to $status)\n" if (reftype($cres) || 'undef') eq 'HASH';
  300. }
  301. if (!$tc || ((reftype($cres) || 'undef') ne 'HASH') ) {
  302. print "Failed!\n";
  303. print "No Such test case in TestRail ($xid).\n";
  304. $self->{'errors'}++;
  305. }
  306. }
  307. 1;
  308. __END__
  309. =head1 NOTES
  310. When using SKIP: {} (or TODO skip) blocks, you may want to consider naming your skip reasons the same as your test names when running in test_per_ok mode.
  311. =head1 SEE ALSO
  312. L<TestRail::API>
  313. L<TAP::Parser>
  314. =head1 SPECIAL THANKS
  315. Thanks to cPanel Inc, for graciously funding the creation of this module.