File Coverage

blib/lib/TAP/Harness/JUnit.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1 6     6   27742 use warnings;
  6         17  
  6         272  
2 6     6   46 use strict;
  6         8  
  6         339  
3              
4             =head1 NAME
5              
6             TAP::Harness::JUnit - Generate JUnit compatible output from TAP results
7              
8             =head1 SYNOPSIS
9              
10             use TAP::Harness::JUnit;
11             my $harness = TAP::Harness::JUnit->new({
12             xmlfile => 'output.xml',
13             package => 'database',
14             # ...
15             });
16             $harness->runtests(@tests);
17              
18             =head1 DESCRIPTION
19              
20             The only difference between this module and I is that this module
21             adds the optional arguments 'xmlfile', 'package', and 'namemangle' that cause
22             the output to be formatted into XML in a format similar to the one that is
23             produced by the JUnit testing framework.
24              
25             =head1 METHODS
26              
27             This module inherits all functions from I.
28              
29             =cut
30              
31             package TAP::Harness::JUnit;
32 6     6   31 use base 'TAP::Harness';
  6         13  
  6         6869  
33              
34 6     6   154442 use Benchmark ':hireswallclock';
  6         54006  
  6         48  
35 6     6   13325 use File::Temp;
  6         160014  
  6         595  
36 6     6   7229 use TAP::Parser;
  6         350134  
  6         194  
37 6     6   9574 use XML::Simple;
  0            
  0            
38             use Scalar::Util qw/blessed/;
39             use Encode;
40              
41             our $VERSION = '0.41';
42              
43             =head2 new
44              
45             These options are added (compared to I):
46              
47             =over
48              
49             =item xmlfile
50              
51             Name of the file XML output will be saved to. If this argument is omitted, the
52             default of "junit_output.xml" is used and a warning is issued.
53              
54             Alternatively, the name of the output file can be specified in the
55             $JUNIT_OUTPUT_FILE environment variable
56              
57             =item package
58              
59             The Hudson/Jenkins continuous-integration systems support separating test
60             results into "packages". By default any number of output xml files will be
61             merged into the default package "(root)".
62              
63             Setting a package name will place all test results from the current run into
64             that package. You can also set the environment variable $JUNIT_PACKAGE to do
65             the same.
66              
67             =item notimes (DEPRECATED)
68              
69             If provided (and true), test case times will not be recorded.
70              
71             =item namemangle
72              
73             Specify how to mangle testcase names. This is sometimes required to interact
74             with buggy JUnit consumers that lack sufficient validation.
75              
76             Alternatively, this value can be set in the environment variable
77             $JUNIT_NAME_MANGLE.
78              
79             Available values are:
80              
81             =over
82              
83             =item hudson
84              
85             Replace anything but alphanumeric characters with underscores. This is the
86             default for historic reasons.
87              
88             =item perl (RECOMMENDED)
89              
90             Replace slashes in the directory hierarchy with dots so that the filesystem
91             layout resembles a Java class hierarchy.
92              
93             This is the recommended setting and may become the default in future.
94              
95             =item none
96              
97             Do not perform any transformations.
98              
99             =back
100              
101             =back
102              
103             =head1 ENVIRONMENT VARIABLES
104              
105             The name of the output file can be specified in the $JUNIT_OUTPUT_FILE
106             environment variable
107              
108             The package name that Hudson/Jenkins use to categorise test results can be
109             specified in $JUNIT_PACKAGE.
110              
111             The name mangling mechanism used to rewrite test names can be specified in
112             $JUNIT_NAME_MANGLE. (See namemangle documentation for available values.)
113              
114             =cut
115              
116             sub new {
117             my ($class, $args) = @_;
118             $args ||= {};
119              
120             # Process arguments
121             my $xmlfile = delete $args->{xmlfile};
122             $xmlfile = $ENV{JUNIT_OUTPUT_FILE} unless defined $xmlfile;
123             unless ($xmlfile) {
124             $xmlfile = 'junit_output.xml';
125             warn 'xmlfile argument not supplied, defaulting to "junit_output.xml"';
126             }
127              
128             my $xmlpackage = delete $args->{package};
129             $xmlpackage = $ENV{JUNIT_PACKAGE} unless defined $xmlpackage;
130              
131             # Get the name of raw perl dump directory
132             my $rawtapdir = $ENV{PERL_TEST_HARNESS_DUMP_TAP};
133             $rawtapdir = $args->{rawtapdir} unless $rawtapdir;
134             $rawtapdir = File::Temp::tempdir() unless $rawtapdir;
135             delete $args->{rawtapdir};
136              
137             my $notimes = delete $args->{notimes};
138              
139             my $namemangle = delete $args->{namemangle};
140             $namemangle = $ENV{JUNIT_NAME_MANGLE} unless defined $namemangle;
141             unless ($namemangle) {
142             $namemangle = 'hudson';
143             }
144              
145             my $self = $class->SUPER::new($args);
146             $self->{__xmlfile} = $xmlfile;
147             $self->{__xml} = {testsuite => []};
148             $self->{__xmlpackage} = $xmlpackage;
149             $self->{__rawtapdir} = $rawtapdir;
150             $self->{__cleantap} = not defined $ENV{PERL_TEST_HARNESS_DUMP_TAP};
151             $self->{__notimes} = $notimes;
152             $self->{__namemangle} = $namemangle;
153             $self->{__auto_number} = 1;
154              
155             # Inject our parser, that persists results for later
156             # consumption and adds timing information
157             @TAP::Harness::JUnit::Parser::ISA = ($self->parser_class);
158             $self->parser_class ('TAP::Harness::JUnit::Parser');
159              
160             return $self;
161             }
162              
163             # Add "(number)" at the end of the test name if the test with
164             # the same name already exists in XML
165             sub uniquename {
166             my $self = shift;
167             my $xml = shift;
168             my $name = shift;
169              
170             my $newname;
171              
172             # Beautify a bit -- strip leading "- "
173             # (that is added by Test::More)
174             $name =~ s/^[\s-]*//;
175              
176             $self->{__test_names} = { map { $_->{name} => 1 } @{ $xml->{testcase} } }
177             unless $self->{__test_names};
178              
179             while(1) {
180             my $number = $self->{__auto_number};
181             $newname = $name
182             ? $name.($number > 1 ? " ($number)" : '')
183             : "Unnamed test case $number"
184             ;
185             last unless exists $self->{__test_names}->{$newname};
186             $self->{__auto_number}++;
187             };
188              
189             $self->{__test_names}->{$newname}++;
190              
191             return xmlsafe($newname);
192             }
193              
194             # Add result of a single TAP parse to the XML
195             sub parsetest {
196             my $self = shift;
197             my $name = shift;
198             my $parser = shift;
199              
200             my $time = $parser->end_time - $parser->start_time;
201             $time = 0 if $self->{__notimes};
202              
203             # Get the return code of test script before re-parsing the TAP output
204             my $badretval = $parser->exit;
205              
206             if ($self->{__namemangle}) {
207             # Older version of hudson crafted an URL of the test
208             # results using the name verbatim. Unfortunatelly,
209             # they didn't escape special characters, soo '/'-s
210             # and family would result in incorrect URLs.
211             # See hudson bug #2167
212             $self->{__namemangle} eq 'hudson'
213             and $name =~ s/[^a-zA-Z0-9, ]/_/g;
214              
215             # Transform hierarchy of directories into what would
216             # look like hierarchy of classes in Hudson
217             if ($self->{__namemangle} eq 'perl') {
218             $name =~ s/^[\.\/]*//;
219             $name =~ s/\./_/g;
220             $name =~ s/\//./g;
221             }
222             }
223              
224             # Hudson/Jenkins strip the prefix from a classname to figure out the package
225             my $prefixname = $self->{__xmlpackage}
226             ? $self->{__xmlpackage}.'.'.$name
227             : $name;
228              
229             my $xml = {
230             name => $prefixname,
231             failures => 0,
232             errors => 0,
233             tests => undef,
234             'time' => $time,
235             testcase => [],
236             'system-out' => [''],
237             };
238              
239             my $tests_run = 0;
240             my $comment = ''; # Comment agreggator
241             foreach my $result (@{$parser->{__results}}) {
242              
243             my $time = $result->{__end_time} - $result->{__start_time};
244             $time = 0 if $self->{__notimes};
245              
246             # Counters
247             if ($result->type eq 'plan') {
248             $xml->{tests} = $result->tests_planned;
249             }
250              
251             # Comments
252             if ($result->type eq 'comment') {
253             $result->raw =~ /^# (.*)/ and $comment .= xmlsafe($1)."\n";
254             }
255              
256             # Errors
257             if ($result->type eq 'unknown') {
258             $comment .= xmlsafe($result->raw)."\n";
259             }
260              
261             # Test case
262             if ($result->type eq 'test') {
263             $tests_run++;
264              
265             # JUnit can't express these -- pretend they do not exist
266             $result->directive eq 'TODO' and next;
267             $result->directive eq 'SKIP' and next;
268              
269             my $test = {
270             'time' => $time,
271             name => $self->uniquename($xml, $result->description),
272             classname => $prefixname,
273             };
274              
275             if ($result->ok eq 'not ok') {
276             $test->{failure} = [{
277             type => blessed ($result),
278             message => xmlsafe($result->raw),
279             content => $comment,
280             }];
281             $xml->{failures}++;
282             };
283              
284             push @{$xml->{testcase}}, $test;
285             $comment = '';
286             }
287              
288             # Log
289             $xml->{'system-out'}->[0] .= xmlsafe($result->raw)."\n";
290             }
291              
292             # Detect no plan
293             unless (defined $xml->{tests}) {
294             # Ensure XML will have non-empty value
295             $xml->{tests} = 0;
296              
297             # Fake a failed test
298             push @{$xml->{testcase}}, {
299             'time' => $time,
300             name => $self->uniquename($xml, 'Test died too soon, even before plan.'),
301             classname => $prefixname,
302             failure => {
303             type => 'Plan',
304             message => 'The test suite died before a plan was produced. You need to have a plan.',
305             content => 'No plan',
306             },
307             };
308             $xml->{errors}++;
309             }
310              
311             # Detect bad plan
312             elsif ($xml->{errors} = $xml->{tests} - $tests_run) {
313             # Fake an error
314             push @{$xml->{testcase}}, {
315             'time' => $time,
316             name => $self->uniquename($xml, 'Number of runned tests does not match plan.'),
317             classname => $prefixname,
318             failure => {
319             type => 'Plan',
320             message => ($xml->{errors} > 0
321             ? 'Some test were not executed, The test died prematurely.'
322             : 'Extra tests tun.'),
323             content => 'Bad plan',
324             },
325             };
326             $xml->{failures}++;
327             $xml->{errors} = abs ($xml->{errors});
328             }
329              
330             # Bad return value. See BUGS
331             elsif ($badretval and not $xml->{failures}) {
332             # Fake an error
333             push @{$xml->{testcase}}, {
334             'time' => $time,
335             name => $self->uniquename($xml, 'Test returned failure'),
336             classname => $prefixname,
337             failure => {
338             type => 'Died',
339             message => "Test died with return code $badretval",
340             content => "Test died with return code $badretval",
341             },
342             };
343             $xml->{errors}++;
344             $xml->{tests}++;
345             }
346              
347             # Add this suite to XML
348             push @{$self->{__xml}->{testsuite}}, $xml;
349             }
350              
351             sub runtests {
352             my ($self, @files) = @_;
353              
354             my $aggregator = $self->SUPER::runtests(@files);
355              
356             foreach my $test (keys %{$aggregator->{parser_for}}) {
357             $self->parsetest ($test => $aggregator->{parser_for}->{$test});
358             }
359              
360             # Format XML output
361             my $xs = new XML::Simple;
362             my $xml = $xs->XMLout ($self->{__xml}, RootName => 'testsuites');
363              
364             # Ensure it is valid XML. Not very smart though.
365             $xml = encode ('UTF-8', decode ('UTF-8', $xml));
366              
367             # Dump output
368             open my $xml_fh, '>', $self->{__xmlfile}
369             or die $self->{__xmlfile}.': '.$!;
370             print $xml_fh "\n";
371             print $xml_fh $xml;
372             close $xml_fh;
373              
374             # If we caused the dumps to be preserved, clean them
375             File::Path::rmtree($self->{__rawtapdir}) if $self->{__cleantap};
376              
377             return $aggregator;
378             }
379              
380             # Because not all utf8 characters are allowed in xml, only these
381             # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
382             # http://www.w3.org/TR/REC-xml/#NT-Char
383             sub xmlsafe {
384             my $s = shift;
385              
386             return '' unless defined $s && length($s) > 0;
387              
388             $s =~ s/([\x01|\x02|\x03|\x04|\x05|\x06|\x07|\x08|\x0B|\x0C|\x0E|\x0F|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1A|\x1B|\x1C|\x1D|\x1E|\x1F])/ sprintf("<%0.2x>", ord($1)) /gex;
389              
390              
391             return $s;
392             }
393              
394             # This is meant to transparently extend the parser chosen by user.
395             # Dynamically superubclassed to the chosen parser upon harnsess construction.
396             package TAP::Harness::JUnit::Parser;
397              
398             use Time::HiRes qw/time/;
399              
400             # Upon each line taken, account for time and remember the exact
401             # result. A harness should then collect the results from the aggregator.
402             sub next
403             {
404             my $self = shift;
405             my $result = $self->SUPER::next (@_);
406             return $result unless $result; # last call
407              
408             # First assert
409             unless ($self->{__results}) {
410             $self->{__last_assert} = $self->start_time;
411             $self->{__results} = []
412             }
413              
414             # Account for time taken
415             $result->{__start_time} = $self->{__last_assert};
416             $result->{__end_time} = $self->{__last_assert} = time;
417              
418             # Remember for the aggregator
419             push @{$self->{__results}}, $result;
420              
421             return $result;
422             }
423              
424             =head1 SEE ALSO
425              
426             The JUnit XML schema was obtained from
427             L.
428              
429             =head1 ACKNOWLEDGEMENTS
430              
431             This module was partly inspired by Michael Peters's I.
432             It was originally written by Lubomir Rintel (GoodData)
433             C<< >> and includes code from several contributors.
434              
435             The following people (in no specific order) have reported problems or
436             contributed code to I:
437              
438             =over
439              
440             =item David Ritter
441              
442             =item Jeff Lavallee
443              
444             =item Andreas Pohl
445              
446             =item Ton Voon
447              
448             =item Kevin Goess
449              
450             =item Richard Huxton
451              
452             =item David E. Wheeler
453              
454             =item Malcolm Parsons
455              
456             =item Finn Smith
457              
458             =back
459              
460             =head1 BUGS
461              
462             The comments that are above the C or C are considered the output of
463             the test. This, though being more logical, is against TAP specification.
464              
465             I is used to generate the output. This is suboptimal and involves
466             some hacks.
467              
468             During testing the resulting files are not tested against the schema. This
469             would be a good thing to do.
470              
471             =head1 CONTRIBUTING
472              
473             Source code for I is kept in a public Git repository.
474             Visit L.
475              
476             Bug reports and feature enhancement requests are tracked at
477             L.
478              
479             =head1 COPYRIGHT & LICENSE
480              
481             Copyright 2008, 2009, 2010, 2011, 2012, 2013 I
482             contributors. All rights reserved.
483              
484             This program is free software; you can redistribute it and/or modify it under
485             the same terms as Perl itself.
486              
487             =cut
488              
489             1;