File Coverage

blib/lib/Test/Rail/Parser.pm
Criterion Covered Total %
statement 357 364 98.0
branch 148 188 78.7
condition 103 133 77.4
subroutine 22 22 100.0
pod 8 8 100.0
total 638 715 89.2


line stmt bran cond sub pod time code
1             # ABSTRACT: Upload your TAP results to TestRail
2             # PODNAME: Test::Rail::Parser
3              
4             package Test::Rail::Parser;
5             $Test::Rail::Parser::VERSION = '0.052';
6 3     3   55105 use strict;
  3         7  
  3         95  
7 3     3   14 use warnings;
  3         7  
  3         93  
8 3     3   13 use utf8;
  3         7  
  3         21  
9              
10 3     3   86 use parent qw/TAP::Parser/;
  3         5  
  3         19  
11 3     3   108836 use Carp qw{cluck confess};
  3         8  
  3         186  
12 3     3   28 use POSIX qw{floor strftime};
  3         8  
  3         24  
13 3     3   2791 use Clone qw{clone};
  3         8  
  3         171  
14              
15 3     3   18 use TestRail::API;
  3         5  
  3         105  
16 3     3   404 use TestRail::Utils;
  3         7  
  3         96  
17 3     3   16 use Scalar::Util qw{reftype};
  3         5  
  3         147  
18              
19 3     3   18 use File::Basename qw{basename};
  3         5  
  3         12856  
20              
21             sub new {
22 57     57 1 90557 my ( $class, $opts ) = @_;
23 57         251958 $opts = clone $opts; #Convenience, if we are passing over and over again...
24              
25             #Load our callbacks
26 57         8370 $opts->{'callbacks'} = {
27             'test' => \&testCallback,
28             'comment' => \&commentCallback,
29             'unknown' => \&unknownCallback,
30             'bailout' => \&bailoutCallback,
31             'EOF' => \&EOFCallback,
32             'plan' => \&planCallback,
33             };
34              
35             my $tropts = {
36             'apiurl' => delete $opts->{'apiurl'},
37             'user' => delete $opts->{'user'},
38             'pass' => delete $opts->{'pass'},
39             'debug' => delete $opts->{'debug'},
40             'browser' => delete $opts->{'browser'},
41             'run' => delete $opts->{'run'},
42             'project' => delete $opts->{'project'},
43             'project_id' => delete $opts->{'project_id'},
44             'step_results' => delete $opts->{'step_results'},
45             'plan' => delete $opts->{'plan'},
46             'plan_id' => delete $opts->{'plan_id'},
47             'configs' => delete $opts->{'configs'} // [],
48             'testsuite_id' => delete $opts->{'testsuite_id'},
49             'testsuite' => delete $opts->{'testsuite'},
50             'encoding' => delete $opts->{'encoding'},
51             'sections' => delete $opts->{'sections'},
52             'autoclose' => delete $opts->{'autoclose'},
53             'config_group' => delete $opts->{'config_group'},
54              
55             #Stubs for extension by subclassers
56             'result_options' => delete $opts->{'result_options'},
57             'result_custom_options' => delete $opts->{'result_custom_options'},
58             'test_bad_status' => delete $opts->{'test_bad_status'},
59 57   100     2185 'max_tries' => delete $opts->{'max_tries'} || 1,
      100        
60             };
61              
62             confess("plan passed, but no run passed!")
63 57 50 66     930 if !$tropts->{'run'} && $tropts->{'plan'};
64              
65             #Allow natural confessing from constructor
66             #Force-on POST redirects for maximum compatibility
67             #Also ensure all opts that need string type have it when undef
68             my $tr = TestRail::API->new(
69             $tropts->{'apiurl'} // '',
70             $tropts->{'user'} // '',
71             $tropts->{'pass'} // '',
72             $tropts->{'encoding'} // '',
73             $tropts->{'debug'},
74             1,
75             $tropts->{max_tries},
76 56   50     1737 { 'skip_usercache' => 1 },
      50        
      50        
      50        
77             );
78 56         230 $tropts->{'testrail'} = $tr;
79             $tr->{'browser'} = $tropts->{'browser'}
80 56 50       1752 if defined( $tropts->{'browser'} ); #allow mocks
81 56         169 $tr->{'debug'} = 0; #Always suppress in production
82              
83             #Get project ID from name, if not provided
84 56 100       264 if ( !defined( $tropts->{'project_id'} ) ) {
85 52         142 my $pname = $tropts->{'project'};
86 52         469 $tropts->{'project'} = $tr->getProjectByName($pname);
87             confess("Could not list projects! Shutting down.")
88 52 50       240 if ( $tropts->{'project'} == -500 );
89 52 50       199 if ( !$tropts->{'project'} ) {
90 0         0 confess(
91             "No project (or project_id) provided, or that which was provided was invalid!"
92             );
93             }
94             }
95             else {
96 4         33 $tropts->{'project'} = $tr->getProjectByID( $tropts->{'project_id'} );
97             confess("No such project with ID $tropts->{project_id}!")
98 4 50       29 if !$tropts->{'project'};
99             }
100 56         178 $tropts->{'project_id'} = $tropts->{'project'}->{'id'};
101              
102             # Ok, let's cache the users since we have the project ID now
103 56         436 $tr->getUsers( $tropts->{'project_id'} );
104              
105             #Discover possible test statuses
106 56         547 $tropts->{'statuses'} = $tr->getPossibleTestStatuses();
107 56         187 my @ok = grep { $_->{'name'} eq 'passed' } @{ $tropts->{'statuses'} };
  504         938  
  56         195  
108 56         128 my @not_ok = grep { $_->{'name'} eq 'failed' } @{ $tropts->{'statuses'} };
  504         793  
  56         145  
109 56         115 my @skip = grep { $_->{'name'} eq 'skip' } @{ $tropts->{'statuses'} };
  504         849  
  56         152  
110 56         158 my @todof = grep { $_->{'name'} eq 'todo_fail' } @{ $tropts->{'statuses'} };
  504         784  
  56         137  
111 56         115 my @todop = grep { $_->{'name'} eq 'todo_pass' } @{ $tropts->{'statuses'} };
  504         755  
  56         123  
112 56         106 my @retest = grep { $_->{'name'} eq 'retest' } @{ $tropts->{'statuses'} };
  504         753  
  56         113  
113 56         116 my @tbad;
114             @tbad =
115 162         263 grep { $_->{'name'} eq $tropts->{test_bad_status} }
116 18         65 @{ $tropts->{'statuses'} }
117 56 100       214 if $tropts->{test_bad_status};
118 56 50       185 confess("No status with internal name 'passed' in TestRail!")
119             unless scalar(@ok);
120 56 50       180 confess("No status with internal name 'failed' in TestRail!")
121             unless scalar(@not_ok);
122 56 50       206 confess("No status with internal name 'skip' in TestRail!")
123             unless scalar(@skip);
124 56 50       157 confess("No status with internal name 'todo_fail' in TestRail!")
125             unless scalar(@todof);
126 56 50       175 confess("No status with internal name 'todo_pass' in TestRail!")
127             unless scalar(@todop);
128 56 50       174 confess("No status with internal name 'retest' in TestRail!")
129             unless scalar(@retest);
130             confess(
131             "No status with internal name '$tropts->{test_bad_status}' in TestRail!"
132 56 100 100     958 ) unless scalar(@tbad) || !$tropts->{test_bad_status};
133              
134             #Map in all the statuses
135 55         148 foreach my $status ( @{ $tropts->{'statuses'} } ) {
  55         204  
136 495         1202 $tropts->{ $status->{'name'} } = $status;
137             }
138              
139             #Special aliases
140 55         163 $tropts->{'ok'} = $ok[0];
141 55         142 $tropts->{'not_ok'} = $not_ok[0];
142              
143             confess "testsuite and testsuite_id are mutually exclusive"
144 55 50 66     478 if ( $tropts->{'testsuite_id'} && $tropts->{'testsuite'} );
145              
146             #Grab testsuite by name if needed
147 55 100       193 if ( $tropts->{'testsuite'} ) {
148             my $ts = $tr->getTestSuiteByName( $tropts->{'project_id'},
149 3         40 $tropts->{'testsuite'} );
150 3 50       15 confess( "No such testsuite '" . $tropts->{'testsuite'} . "' found!" )
151             unless $ts;
152 3         16 $tropts->{'testsuite_id'} = $ts->{'id'};
153             }
154              
155             #Grab run
156 55         139 my ( $run, $plan, $config_ids );
157              
158             # See if we have to create a configuration
159 55         520 my $configz2create = $tr->getConfigurations( $tropts->{'project_id'} );
160             @$configz2create = grep {
161 55         162 my $c = $_;
  231         291  
162 231         268 ( grep { $_ eq $c->{'name'} } @{ $tropts->{'configs'} } )
  73         219  
  231         691  
163             } @$configz2create;
164 55 100 100     307 if ( scalar(@$configz2create) && $tropts->{'config_group'} ) {
165             my $cgroup = $tr->getConfigurationGroupByName( $tropts->{project_id},
166 3         34 $tropts->{'config_group'} );
167 3 50       13 unless ( ref($cgroup) eq 'HASH' ) {
168 3         144 print "# Adding Configuration Group $tropts->{config_group}...\n";
169             $cgroup = $tr->addConfigurationGroup( $tropts->{project_id},
170 3         63 $tropts->{'config_group'} );
171             }
172             confess(
173 3 50       106 "Could neither find nor create the provided configuration group '$tropts->{config_group}'"
174             ) unless ref($cgroup) eq 'HASH';
175 3         12 foreach my $cc (@$configz2create) {
176 3         112 print "# Adding Configuration $cc->{name}...\n";
177 3         27 $tr->addConfiguration( $cgroup->{'id'}, $cc->{'name'} );
178             }
179             }
180              
181             #check if configs passed are defined for project. If we can't get all the IDs, something's hinky
182             @$config_ids = $tr->translateConfigNamesToIds( $tropts->{'project_id'},
183 55         286 @{ $tropts->{'configs'} } );
  55         287  
184 55 50 50     465 confess("Could not retrieve list of valid configurations for your project.")
185             unless ( reftype($config_ids) || 'undef' ) eq 'ARRAY';
186 55         158 my @bogus_configs = grep { !defined($_) } @$config_ids;
  16         64  
187 55         105 my $num_bogus = scalar(@bogus_configs);
188 55 50       332 confess(
189             "Detected $num_bogus bad config names passed. Check available configurations for your project."
190             ) if $num_bogus;
191              
192 55 100       302 if ( $tropts->{'plan'} ) {
193              
194             #Attempt to find run, filtered by configurations
195 21 50       79 if ( $tropts->{'plan_id'} ) {
196 0         0 $plan = $tr->getPlanByID( $tropts->{'plan_id'} );
197             }
198             else {
199             $plan =
200 21         182 $tr->getPlanByName( $tropts->{'project_id'}, $tropts->{'plan'} );
201             }
202             confess(
203             "Test plan provided is completed, and spawning was not indicated")
204             if ( ref $plan eq 'HASH' )
205             && $plan->{'is_completed'}
206 21 100 100     1487 && ( !$tropts->{'testsuite_id'} );
      100        
207 20 100 100     338 if ( $plan && !$plan->{'is_completed'} ) {
208 12         104 $tropts->{'plan'} = $plan;
209             $run = $tr->getChildRunByName( $plan, $tropts->{'run'},
210 12         134 $tropts->{'configs'} ); #Find plan filtered by configs
211              
212 12 100 100     132 if ( defined($run) && ( reftype($run) || 'undef' ) eq 'HASH' ) {
      66        
213 10         31 $tropts->{'run'} = $run;
214 10         28 $tropts->{'run_id'} = $run->{'id'};
215             }
216             }
217             else {
218             #Try to make it if spawn is passed
219             $tropts->{'plan'} = $tr->createPlan( $tropts->{'project_id'},
220             $tropts->{'plan'}, "Test plan created by TestRail::API" )
221 8 50       95 if $tropts->{'testsuite_id'};
222             confess("Could not find plan "
223             . $tropts->{'plan'}
224             . " in provided project, and spawning failed (or was not indicated)!"
225 8 50       576 ) if !$tropts->{'plan'};
226             }
227             }
228             else {
229 34         331 $run = $tr->getRunByName( $tropts->{'project_id'}, $tropts->{'run'} );
230             confess(
231             "Test run provided is completed, and spawning was not indicated")
232             if ( ref $run eq 'HASH' )
233             && $run->{'is_completed'}
234 34 100 100     375 && ( !$tropts->{'testsuite_id'} );
      100        
235 33 100 100     595 if ( defined($run)
      66        
      100        
236             && ( reftype($run) || 'undef' ) eq 'HASH'
237             && !$run->{'is_completed'} )
238             {
239 12         76 $tropts->{'run'} = $run;
240 12         35 $tropts->{'run_id'} = $run->{'id'};
241             }
242             }
243              
244             #If spawn was passed and we don't have a Run ID yet, go ahead and make it
245 53 100 100     497 if ( $tropts->{'testsuite_id'} && !$tropts->{'run_id'} ) {
246 30         1833 print "# Spawning run\n";
247 30         164 my $cases = [];
248 30 100       160 if ( $tropts->{'sections'} ) {
249 9         95 print "# with specified sections\n";
250              
251             #Then translate the sections into an array of case IDs.
252             confess("Sections passed to spawn must be ARRAYREF")
253 9 50 50     154 unless ( reftype( $tropts->{'sections'} ) || 'undef' ) eq 'ARRAY';
254 8         34 @{ $tropts->{'sections'} } = $tr->sectionNamesToIds(
255             $tropts->{'project_id'},
256             $tropts->{'testsuite_id'},
257 9         44 @{ $tropts->{'sections'} }
  9         111  
258             );
259 8         20 foreach my $section ( @{ $tropts->{'sections'} } ) {
  8         25  
260              
261             #Get the child sections, and append them to our section list so we get their cases too.
262             my $append_sections = $tr->getChildSections(
263             $tropts->{'project_id'},
264             {
265             'id' => $section,
266 9         75 'suite_id' => $tropts->{'testsuite_id'}
267             }
268             );
269             @$append_sections = grep {
270 9         32 my $sc = $_;
  6         8  
271 18         32 !scalar( grep { $_ == $sc->{'id'} }
272 6         7 @{ $tropts->{'sections'} } )
  6         9  
273             } @$append_sections
274             ; #de-dup in case the user added children to the list
275 9         22 @$append_sections = map { $_->{'id'} } @$append_sections;
  2         5  
276 9         15 push( @{ $tropts->{'sections'} }, @$append_sections );
  9         26  
277              
278             my $section_cases = $tr->getCases(
279             $tropts->{'project_id'},
280 9         68 $tropts->{'testsuite_id'},
281             { 'section_id' => $section }
282             );
283 9 50 50     75 push( @$cases, @$section_cases )
284             if ( reftype($section_cases) || 'undef' ) eq 'ARRAY';
285             }
286             }
287              
288 29 100       133 if ( scalar(@$cases) ) {
289 5         16 @$cases = map { $_->{'id'} } @$cases;
  11         50  
290             }
291             else {
292 24         67 $cases = undef;
293             }
294              
295 29 100       112 if ( $tropts->{'plan'} ) {
296 9         222 print "# inside of plan\n";
297             $plan = $tr->createRunInPlan(
298             $tropts->{'plan'}->{'id'},
299             $tropts->{'testsuite_id'},
300 9         128 $tropts->{'run'}, undef, $config_ids, $cases
301             );
302             $run = $plan->{'runs'}->[0]
303             if exists( $plan->{'runs'} )
304             && ( reftype( $plan->{'runs'} ) || 'undef' ) eq 'ARRAY'
305 9 50 50     705 && scalar( @{ $plan->{'runs'} } );
  9   33     65  
      50        
306 9 50 50     90 if ( defined($run) && ( reftype($run) || 'undef' ) eq 'HASH' ) {
      33        
307 9         32 $tropts->{'run'} = $run;
308 9         26 $tropts->{'run_id'} = $run->{'id'};
309             }
310             }
311             else {
312             $run = $tr->createRun(
313             $tropts->{'project_id'},
314             $tropts->{'testsuite_id'},
315 20         280 $tropts->{'run'},
316             "Automatically created Run from TestRail::API",
317             undef,
318             undef,
319             $cases
320             );
321 20 50 50     1708 if ( defined($run) && ( reftype($run) || 'undef' ) eq 'HASH' ) {
      33        
322 20         89 $tropts->{'run'} = $run;
323 20         74 $tropts->{'run_id'} = $run->{'id'};
324             }
325             }
326             confess("Could not spawn run with requested parameters!")
327 29 50       124 if !$tropts->{'run_id'};
328 29         1443 print "# Success!\n";
329             }
330             confess(
331             "No run ID provided, and no run with specified name exists in provided project/plan!"
332 52 100       679 ) if !$tropts->{'run_id'};
333              
334 51         531 my $self = $class->SUPER::new($opts);
335 51 100 66     358288 if ( defined( $self->{'_iterator'}->{'command'} )
336             && reftype( $self->{'_iterator'}->{'command'} ) eq 'ARRAY' )
337             {
338 33         217 $self->{'file'} = $self->{'_iterator'}->{'command'}->[-1];
339 33         1860 print "# PROCESSING RESULTS FROM TEST FILE: $self->{'file'}\n";
340 33         330 $self->{'track_time'} = 1;
341             }
342             else {
343             #Not running inside of prove in real-time, don't bother with tracking elapsed times.
344 18         51 $self->{'track_time'} = 0;
345             }
346              
347             #Make sure the step results field passed exists on the system
348 51         211 my $sr_name = $tropts->{'step_results'};
349             $tropts->{'step_results'} =
350             $tr->getTestResultFieldByName( $tropts->{'step_results'},
351             $tropts->{'project_id'} )
352 51 100       383 if defined $tropts->{'step_results'};
353             confess(
354             "Invalid step results value '$sr_name' passed. Check the spelling and confirm that your project can use the '$sr_name' custom result field."
355 51 100 100     1826 ) if ref $tropts->{'step_results'} ne 'HASH' && $sr_name;
356              
357 50         299 $self->{'tr_opts'} = $tropts;
358 50         276 $self->{'errors'} = 0;
359              
360             #Start the shot clock
361 50         229 $self->{'starttime'} = time();
362              
363             #Make sure we get the time it took to get to each step from the last correctly
364 50         137 $self->{'lasttime'} = $self->{'starttime'};
365 50         250 $self->{'raw_output'} = "";
366              
367 50         15641 return $self;
368             }
369              
370             # Look for file boundaries, etc.
371             sub unknownCallback {
372 87     87 1 3165 my ($test) = @_;
373 87         171 my $self = $test->{'parser'};
374 87         249 my $line = $test->as_string;
375 87         409 $self->{'raw_output'} .= "$line\n";
376              
377             #Unofficial "Extensions" to TAP
378 87         194 my ($status_override) = $line =~ m/^% mark_status=([a-z|_]*)/;
379 87 100       211 if ($status_override) {
380             cluck "Unknown status override"
381 1 50       6 unless defined $self->{'tr_opts'}->{$status_override}->{'id'};
382             $self->{'global_status'} =
383             $self->{'tr_opts'}->{$status_override}->{'id'}
384 1 50       6 if $self->{'tr_opts'}->{$status_override};
385             print "# Overriding status to $status_override ("
386             . $self->{'global_status'}
387             . ")...\n"
388 1 50       36 if $self->{'global_status'};
389             }
390              
391             #XXX I'd love to just rely on the 'name' attr in App::Prove::State::Result::Test, but...
392             #try to pick out the filename if we are running this on TAP in files, where App::Prove is uninvolved
393 87         466 my $file = TestRail::Utils::getFilenameFromTapLine($line);
394 87 100 100     381 $self->{'file'} = $file if !$self->{'file'} && $file;
395 87         183 return;
396             }
397              
398             # Register the current suite or test desc for use by test callback, if the line begins with the special magic words
399             sub commentCallback {
400 77     77 1 1959 my ($test) = @_;
401 77         141 my $self = $test->{'parser'};
402 77         302 my $line = $test->as_string;
403 77         484 $self->{'raw_output'} .= "$line\n";
404              
405 77 50       287 if ( $line =~ m/^#TESTDESC:\s*/ ) {
406 0         0 $self->{'tr_opts'}->{'test_desc'} = $line;
407 0         0 $self->{'tr_opts'}->{'test_desc'} =~ s/^#TESTDESC:\s*//g;
408             }
409 77         179 return;
410             }
411              
412             sub testCallback {
413 69     69 1 9264 my ($test) = @_;
414 69         230 my $self = $test->{'parser'};
415              
416 69 100       425 if ( $self->{'track_time'} ) {
417              
418             #Test done. Record elapsed time.
419 45         128 my $tm = time();
420             $self->{'tr_opts'}->{'result_options'}->{'elapsed'} =
421 45         207 _compute_elapsed( $self->{'lasttime'}, $tm );
422             $self->{'elapse_display'} =
423             defined( $self->{'tr_opts'}->{'result_options'}->{'elapsed'} )
424 45 100       390 ? $self->{'tr_opts'}->{'result_options'}->{'elapsed'}
425             : "0s";
426 45         156 $self->{'lasttime'} = $tm;
427             }
428              
429 69         249 my $line = $test->as_string;
430 69         1724 my $tline = $line;
431             $tline = "["
432             . strftime( "%H:%M:%S %b %e %Y", localtime( $self->{'lasttime'} ) )
433             . " ($self->{elapse_display})] $line"
434 69 100       3589 if $self->{'track_time'};
435 69         445 $self->{'raw_output'} .= "$tline\n";
436              
437             #Don't do anything if we don't want to map TR case => ok or use step-by-step results
438 69 100       336 if ( !$self->{'tr_opts'}->{'step_results'} ) {
439             print
440             "# step_results not set. No action to be taken, except on a whole test basis.\n"
441 57 50       1913 if $self->{'tr_opts'}->{'debug'};
442 57         325 return 1;
443             }
444              
445 12         145 $line =~ s/^(ok|not ok)\s[0-9]*\s-\s//g;
446 12         44 my $test_name = $line;
447              
448             print "# Assuming test name is '$test_name'...\n"
449 12 50 33     135 if $self->{'tr_opts'}->{'debug'} && !$self->{'tr_opts'}->{'step_results'};
450              
451 12         26 my $todo_reason;
452              
453             #Setup args to pass to function
454 12         45 my $status = $self->{'tr_opts'}->{'not_ok'}->{'id'};
455 12         53 my $status_name = 'NOT OK';
456 12 100       60 if ( $test->is_actual_ok() ) {
457 9         90 $status = $self->{'tr_opts'}->{'ok'}->{'id'};
458 9         31 $status_name = 'OK';
459 9 100       35 if ( $test->has_skip() ) {
460 1         7 $status = $self->{'tr_opts'}->{'skip'}->{'id'};
461 1         6 $status_name = 'SKIP';
462 1         9 $test_name =~ s/^(ok|not ok)\s[0-9]*\s//g;
463 1         4 $test_name =~ s/^# skip //gi;
464 1         32 print "# '$test_name'\n";
465             }
466 9 100       66 if ( $test->has_todo() ) {
467 4         40 $status = $self->{'tr_opts'}->{'todo_pass'}->{'id'};
468 4         25 $status_name = 'TODO PASS';
469 4         30 $test_name =~ s/^(ok|not ok)\s[0-9]*\s//g;
470 4         15 $test_name =~ s/^# todo & skip //gi; #handle todo_skip
471 4         35 $test_name =~ s/# todo\s(.*)$//gi;
472 4         21 $todo_reason = $test->explanation();
473             }
474             }
475             else {
476 3 100       33 if ( $test->has_todo() ) {
477 1         12 $status = $self->{'tr_opts'}->{'todo_fail'}->{'id'};
478 1         8 $status_name = 'TODO FAIL';
479 1         12 $test_name =~ s/^(ok|not ok)\s[0-9]*\s//g;
480 1         10 $test_name =~ s/^# todo & skip //gi; #handle todo_skip
481 1         14 $test_name =~ s/# todo\s(.*)$//gi;
482 1         10 $todo_reason = $test->explanation();
483             }
484             }
485              
486             #XXX much of the above code would be unneeded if $test->description wasn't garbage
487 12         150 $test_name =~ s/\s+$//g;
488              
489             #If this is a TODO, set the reason in the notes
490 12 100       68 $self->{'tr_opts'}->{'test_notes'} .= "\nTODO reason: $todo_reason\n"
491             if $todo_reason;
492              
493 12         49 my $sr_sys_name = $self->{'tr_opts'}->{'step_results'}->{'name'};
494             $self->{'tr_opts'}->{'result_custom_options'} = {}
495 12 100       57 if !defined $self->{'tr_opts'}->{'result_custom_options'};
496             $self->{'tr_opts'}->{'result_custom_options'}->{$sr_sys_name} = []
497 12 100       71 if !defined $self->{'tr_opts'}->{'result_custom_options'}->{$sr_sys_name};
498              
499             #TimeStamp every particular step
500              
501             $line = "["
502             . strftime( "%H:%M:%S %b %e %Y", localtime( $self->{'lasttime'} ) )
503             . " ($self->{elapse_display})] $line"
504 12 100       509 if $self->{'track_time'};
505              
506             #XXX Obviously getting the 'expected' and 'actual' from the tap DIAGs would be ideal
507             push(
508 12         42 @{ $self->{'tr_opts'}->{'result_custom_options'}->{$sr_sys_name} },
  12         113  
509             TestRail::API::buildStepResults( $line, "OK", $status_name, $status )
510             );
511 12 50       3692 print "# Appended step results.\n" if $self->{'tr_opts'}->{'debug'};
512 12         72 return 1;
513             }
514              
515             sub bailoutCallback {
516 1     1 1 37 my ($test) = @_;
517 1         5 my $self = $test->{'parser'};
518 1         13 my $line = $test->as_string;
519 1         11 $self->{'raw_output'} .= "$line\n";
520              
521 1 50       9 if ( $self->{'tr_opts'}->{'step_results'} ) {
522 1         4 my $sr_sys_name = $self->{'tr_opts'}->{'step_results'}->{'name'};
523              
524             #Handle the case where we die right off
525 1   50     4 $self->{'tr_opts'}->{'result_custom_options'}->{$sr_sys_name} //= [];
526             push(
527 1         7 @{ $self->{'tr_opts'}->{'result_custom_options'}->{$sr_sys_name} },
528             TestRail::API::buildStepResults(
529             "Bail Out!.", "Continued testing",
530 1         2 $test->explanation, $self->{'tr_opts'}->{'not_ok'}->{'id'}
531             )
532             );
533             }
534 1         7 $self->{'is_bailout'} = 1;
535 1         4 return;
536             }
537              
538             sub EOFCallback {
539 45     45 1 1084102 my ($self) = @_;
540              
541 45 100       305 if ( $self->{'track_time'} ) {
542              
543             #Test done. Record elapsed time.
544             $self->{'tr_opts'}->{'result_options'}->{'elapsed'} =
545 29         213 _compute_elapsed( $self->{'starttime'}, time() );
546             }
547              
548             #Fail if the file is not set
549 45 50       241 if ( !defined( $self->{'file'} ) ) {
550 0         0 cluck(
551             "ERROR: Cannot detect filename, will not be able to find a Test Case with that name"
552             );
553 0         0 $self->{'errors'}++;
554 0         0 return 0;
555             }
556              
557 45         144 my $run_id = $self->{'tr_opts'}->{'run_id'};
558 45         4413 my $test_name = basename( $self->{'file'} );
559              
560 45         236 my $status = $self->{'tr_opts'}->{'ok'}->{'id'};
561 45         270 my $todo_failed = $self->todo() - $self->todo_passed();
562 45 100       713 $status = $self->{'tr_opts'}->{'not_ok'}->{'id'} if $self->has_problems();
563 45 100 100     974 if ( !$self->tests_run()
      100        
564             && !$self->is_good_plan()
565             && $self->{'tr_opts'}->{test_bad_status} )
566             { #No tests were run, no plan, code is probably bad so allow custom marking
567             $status =
568 1         23 $self->{'tr_opts'}->{ $self->{'tr_opts'}->{test_bad_status} }->{'id'};
569             }
570 45 100 100     722 $status = $self->{'tr_opts'}->{'todo_pass'}->{'id'}
      100        
571             if $self->todo_passed()
572             && !$self->failed()
573             && $self->is_good_plan(); #If no fails, but a TODO pass, mark as TODOP
574 45 50 66     679 $status = $self->{'tr_opts'}->{'todo_fail'}->{'id'}
      66        
575             if $todo_failed
576             && !$self->failed()
577             && $self->is_good_plan()
578             ; #If no fails, but a TODO fail, prefer TODOF to TODOP
579 45 100       273 $status = $self->{'tr_opts'}->{'skip'}->{'id'}
580             if $self->skip_all(); #Skip all, whee
581              
582             #Global status override
583 45 100       424 $status = $self->{'global_status'} if $self->{'global_status'};
584              
585             #Notify user about bad plan a bit better, supposing we haven't bailed
586 45 100 100     143 if ( !$self->is_good_plan()
      66        
      100        
587             && !$self->{'is_bailout'}
588             && defined $self->tests_run
589             && defined $self->tests_planned )
590             {
591 6         232 $self->{'raw_output'} .=
592             "\n# ERROR: Bad plan. You ran "
593             . $self->tests_run
594             . " tests, but planned "
595             . $self->tests_planned . ".";
596 6 100       98 if ( $self->{'tr_opts'}->{'step_results'} ) {
597 3         13 my $sr_sys_name = $self->{'tr_opts'}->{'step_results'}->{'name'};
598              
599             #Handle the case where we die right off
600 3   100     29 $self->{'tr_opts'}->{'result_custom_options'}->{$sr_sys_name} //=
601             [];
602             push(
603             @{
604 3         8 $self->{'tr_opts'}->{'result_custom_options'}
605 3         14 ->{$sr_sys_name}
606             },
607             TestRail::API::buildStepResults(
608             "Bad Plan.",
609             $self->tests_planned . " Tests",
610             $self->tests_run . " Tests",
611             $status
612             )
613             );
614             }
615             }
616              
617             #Optional args
618 45         472 my $notes = $self->{'raw_output'};
619 45         142 my $options = $self->{'tr_opts'}->{'result_options'};
620 45         131 my $custom_options = $self->{'tr_opts'}->{'result_custom_options'};
621              
622 45         1537 print "# Setting results...\n";
623 45         398 my $cres =
624             $self->_set_result( $run_id, $test_name, $status, $notes, $options,
625             $custom_options );
626 45         312 $self->_test_closure();
627 45         681 $self->{'global_status'} = $status;
628              
629 45 50       223 undef $self->{'tr_opts'} unless $self->{'tr_opts'}->{'debug'};
630              
631 45         428 return $cres;
632             }
633              
634             sub planCallback {
635 43     43 1 5322 my ($plan) = @_;
636 43         146 my $self = $plan->{'parser'};
637 43 50       222 $self->{raw_output} .= $plan->as_string if $plan->as_string;
638             }
639              
640             sub _set_result {
641 45     45   270 my ( $self, $run_id, $test_name, $status, $notes, $options,
642             $custom_options )
643             = @_;
644 45         91 my $tc;
645              
646             print "# Test elapsed: " . $options->{'elapsed'} . "\n"
647 45 100       326 if $options->{'elapsed'};
648              
649 45         578 print "# Attempting to find case by title '"
650             . $test_name
651             . "' in run $run_id...\n";
652             $tc =
653 45         738 $self->{'tr_opts'}->{'testrail'}->getTestByName( $run_id, $test_name );
654 45 100 100     598 if ( !defined($tc) || ( reftype($tc) || 'undef' ) ne 'HASH' ) {
      66        
655 1         371 cluck("ERROR: Could not find test case: $tc");
656 1         667 $self->{'errors'}++;
657 1         8 return 0;
658             }
659              
660 44 50       202 my $xid = $tc ? $tc->{'id'} : '???';
661              
662 44         82 my $cres;
663              
664             #Set test result
665 44 50       167 if ($tc) {
666 44         2036 print
667             "# Reporting result of case $xid in run $self->{'tr_opts'}->{'run_id'} as status '$status'...";
668              
669             # createTestResults(test_id,status_id,comment,options,custom_options)
670             $cres =
671             $self->{'tr_opts'}->{'testrail'}
672 44         533 ->createTestResults( $tc->{'id'}, $status, $notes, $options,
673             $custom_options );
674 44 100 100     8547 print "# OK! (set to $status)\n"
675             if ( reftype($cres) || 'undef' ) eq 'HASH';
676             }
677 44 100 100     1255 if ( !$tc || ( ( reftype($cres) || 'undef' ) ne 'HASH' ) ) {
      66        
678 1         16 print "# Failed!\n";
679 1         13 print "# No Such test case in TestRail ($xid).\n";
680 1         12 $self->{'errors'}++;
681             }
682              
683             }
684              
685             #Compute the expected testrail date interval from 2 unix timestamps.
686             sub _compute_elapsed {
687 78     78   934 my ( $begin, $end ) = @_;
688 78         176 my $secs_elapsed = $end - $begin;
689 78         526 my $mins_elapsed = floor( $secs_elapsed / 60 );
690 78         255 my $secs_remain = $secs_elapsed % 60;
691 78         241 my $hours_elapsed = floor( $mins_elapsed / 60 );
692 78         192 my $mins_remain = $mins_elapsed % 60;
693              
694 78         299 my $datestr = "";
695              
696             #You have bigger problems if your test takes days
697 78 100       281 if ($hours_elapsed) {
698 2         20 $datestr .= "$hours_elapsed" . "h $mins_remain" . "m";
699             }
700             else {
701 76         377 $datestr .= "$mins_elapsed" . "m";
702             }
703 78 100       210 if ($mins_elapsed) {
704 3         6 $datestr .= " $secs_remain" . "s";
705             }
706             else {
707 75         280 $datestr .= " $secs_elapsed" . "s";
708             }
709 78 100       316 undef $datestr if $datestr eq "0m 0s";
710 78         521 return $datestr;
711             }
712              
713             sub _test_closure {
714 45     45   214 my ($self) = @_;
715 45 100       297 return unless $self->{'tr_opts'}->{'autoclose'};
716 8 100       75 my $is_plan = $self->{'tr_opts'}->{'plan'} ? 1 : 0;
717             my $id =
718             $self->{'tr_opts'}->{'plan'}
719             ? $self->{'tr_opts'}->{'plan'}->{'id'}
720 8 100       61 : $self->{'tr_opts'}->{'run'};
721              
722 8 100       58 if ($is_plan) {
723             my $plan_summary =
724 4         54 $self->{'tr_opts'}->{'testrail'}->getPlanSummary($id);
725              
726             return
727             if ( $plan_summary->{'totals'}->{'Untested'} +
728 4 100       28 $plan_summary->{'totals'}->{'Retest'} );
729 3         114 print "# No more outstanding cases detected. Closing Plan.\n";
730 3         28 $self->{'plan_closed'} = 1;
731 3         31 return $self->{'tr_opts'}->{'testrail'}->closePlan($id);
732             }
733              
734 4         62 my ($run_summary) = $self->{'tr_opts'}->{'testrail'}->getRunSummary($id);
735             return
736             if ( $run_summary->{'run_status'}->{'Untested'} +
737 4 100       39 $run_summary->{'run_status'}->{'Retest'} );
738 1         53 print "# No more outstanding cases detected. Closing Run.\n";
739 1         10 $self->{'run_closed'} = 1;
740             return $self->{'tr_opts'}->{'testrail'}
741 1         17 ->closeRun( $self->{'tr_opts'}->{'run_id'} );
742             }
743              
744             sub make_result {
745 277     277 1 26326182 my ( $self, @args ) = @_;
746 277         1133 my $res = $self->SUPER::make_result(@args);
747 277         13482 $res->{'parser'} = $self;
748 277         1081 return $res;
749             }
750              
751             1;
752              
753             __END__