6 TAP::Harness::JUnit - Generate JUnit compatible output from TAP results
10 use TAP::Harness::JUnit;
11 my $harness = TAP::Harness::JUnit->new({
12 xmlfile => 'output.xml',
15 $harness->runtests(@tests);
19 The only difference between this module and I<TAP::Harness> is that
20 this adds optional 'xmlfile' argument, that causes the output to
21 be formatted into XML in format similar to one that is produced by
22 JUnit testing framework.
26 This modules inherits all functions from I<TAP::Harness>.
30 package TAP
::Harness
::JUnit
;
31 use base
'TAP::Harness';
33 use Benchmark
':hireswallclock';
37 use Scalar
::Util qw
/blessed/;
40 our $VERSION = '0.26';
44 These options are added (compared to I<TAP::Harness>):
50 Name of the file XML output will be saved to. In case this argument
51 is ommited, default of "junit_output.xml" is used and a warning is issued.
55 If provided (and true), test case times will not be recorded.
59 Specify how to mangle testcase names. This is sometimes required to
60 interact with buggy JUnit consumers that lack sufficient validation.
67 Replace anything but alphanumeric characters with underscores.
68 This is default for historic reasons.
72 Replace slashes in directory hierarchy with dots so that the
73 filesystem layout resemble Java class hierarchy.
77 Do not do any transformations.
86 my ($class, $args) = @_;
91 unless ($xmlfile = delete $args->{xmlfile
}) {
92 $xmlfile = 'junit_output.xml';
93 warn 'xmlfile argument not supplied, defaulting to "junit_output.xml"';
95 defined $args->{merge
} or
96 warn 'You should consider using "merge" parameter. See BUGS section of TAP::Harness::JUnit manual';
98 # Get the name of raw perl dump directory
99 my $rawtapdir = $ENV{PERL_TEST_HARNESS_DUMP_TAP
};
100 $rawtapdir = $args->{rawtapdir
} unless $rawtapdir;
101 $rawtapdir = File
::Temp
::tempdir
() unless $rawtapdir;
102 delete $args->{rawtapdir
};
104 my $notimes = delete $args->{notimes
};
106 my $self = $class->SUPER::new
($args);
107 $self->{__xmlfile
} = $xmlfile;
108 $self->{__xml
} = {testsuite
=> []};
109 $self->{__rawtapdir
} = $rawtapdir;
110 $self->{__cleantap
} = not defined $ENV{PERL_TEST_HARNESS_DUMP_TAP
};
111 $self->{__notimes
} = $notimes;
112 if (defined $args->{namemangle
}) {
113 $self->{__namemangle
} = $args->{namemangle
};
115 $self->{__namemangle
} = 'hudson';
121 # Add "(number)" at the end of the test name if the test with
122 # the same name already exists in XML
130 # Beautify a bit -- strip leading "- "
131 # (that is added by Test::More)
132 $name =~ s/^[\s-]*//;
137 $newname .= " ($number)" if $number > 1;
139 $newname = "Unnamed test case $number";
143 foreach my $testcase (@
{$xml->{testcase
}}) {
144 next NAME
if $newname eq $testcase->{name
};
151 # Add a single TAP output file to the XML
167 'system-out' => [''],
170 open (my $tap_handle, $self->{__rawtapdir
}.'/'.$file)
172 my $rawtap = join ('', <$tap_handle>);
175 my $parser = new TAP
::Parser
({'tap' => $rawtap });
178 my $comment = ''; # Comment agreggator
179 while ( my $result = $parser->next ) {
182 if ($result->type eq 'plan') {
183 $xml->{tests
} = $result->tests_planned;
187 if ($result->type eq 'comment') {
189 $badretval = $result if $result->comment =~ /Looks like your test died/;
191 #$comment .= $result->comment."\n";
192 # ->comment has leading whitespace stripped
193 $result->raw =~ /^# (.*)/ and $comment .= $1."\n";
197 if ($result->type eq 'unknown') {
198 $comment .= $result->raw."\n";
202 if ($result->type eq 'test') {
205 # JUnit can't express these -- pretend they do not exist
206 $result->directive eq 'TODO' and next;
207 $result->directive eq 'SKIP' and next;
211 name
=> uniquename
($xml, $result->description),
215 if ($result->ok eq 'not ok') {
216 $test->{failure
} = [{
217 type
=> blessed
($result),
218 message
=> $result->raw,
224 push @
{$xml->{testcase
}}, $test;
229 $xml->{'system-out'}->[0] .= $result->raw."\n";
233 unless (defined $xml->{tests
}) {
234 # Ensure XML will have non-empty value
238 push @
{$xml->{testcase
}}, {
240 name
=> uniquename
($xml, 'Test died too soon, even before plan.'),
244 message
=> 'The test suite died before a plan was produced. You need to have a plan.',
245 content
=> 'No plan',
252 elsif ($xml->{failures
} = $xml->{tests
} - $tests_run) {
254 push @
{$xml->{testcase
}}, {
256 name
=> uniquename
($xml, 'Number of runned tests does not match plan.'),
260 message
=> ($xml->{failures
} > 0
261 ?
'Some test were not executed, The test died prematurely.'
262 : 'Extra tests tun.'),
263 content
=> 'Bad plan',
267 $xml->{failures
} = abs ($xml->{failures
});
270 # Bad return value. See BUGS
271 elsif ($badretval and not $xml->{errors
}) {
273 push @
{$xml->{testcase
}}, {
275 name
=> uniquename
($xml, 'Test returned failure'),
279 message
=> $badretval->comment,
280 content
=> $badretval->raw,
286 # Make up times for sub-tests
288 foreach my $testcase (@
{$xml->{testcase
}}) {
289 $testcase->{time} = $time / @
{$xml->{testcase
}};
293 # Add this suite to XML
294 push @
{$self->{__xml
}->{testsuite
}}, $xml;
298 my ($self, @files) = @_;
300 $ENV{PERL_TEST_HARNESS_DUMP_TAP
} = $self->{__rawtapdir
};
301 my $aggregator = $self->SUPER::runtests
(@files);
303 foreach my $test (@files) {
307 if (ref $test eq 'ARRAY') {
308 ($file, $comment) = @
{$test};
312 $comment = $file unless defined $comment;
314 if ($self->{__namemangle
}) {
315 # Older version of hudson crafted an URL of the test
316 # results using the comment verbatim. Unfortunatelly,
317 # they didn't escape special characters, soo '/'-s
318 # and family would result in incorrect URLs.
319 # See hudson bug #2167
320 $self->{__namemangle
} eq 'hudson'
321 and $comment =~ s/[^a-zA-Z0-9, ]/_/g;
323 # Transform hierarchy of directories into what would
324 # look like hierarchy of classes in Hudson
325 if ($self->{__namemangle
} eq 'perl') {
326 $comment =~ s/^[\.\/]*//;
327 $comment =~ s/\./_/g;
328 $comment =~ s/\//./g
;
332 $self->parsetest ($file, $comment, $self->{__notimes
} ?
0 : $aggregator->elapsed->[0]);
336 my $xs = new XML
::Simple
;
337 my $xml = $xs->XMLout ($self->{__xml
}, RootName
=> 'testsuites');
339 # Ensure it is valid XML. Not very smart though.
340 $xml = encode
('UTF-8', decode
('UTF-8', $xml));
343 open my $xml_fh, '>', $self->{__xmlfile
}
344 or die $self->{__xmlfile
}.': '.$!;
345 print $xml_fh "<?xml version='1.0' encoding='utf-8'?>\n";
349 # If we caused the dumps to be preserved, clean them
350 File
::Path
::rmtree
($self->{__rawtapdir
}) if $self->{__cleantap
};
357 JUnit XML schema was obtained from L<http://jra1mw.cvs.cern.ch:8180/cgi-bin/jra1mw.cgi/org.glite.testing.unit/config/JUnitXSchema.xsd?view=markup>.
359 =head1 ACKNOWLEDGEMENTS
361 This module was partly inspired by Michael Peters' I<TAP::Harness::Archive>.
363 Following people (in no specific order) have reported problems
364 or contributed fixes to I<TAP::Harness::JUnit>:
378 Test return value is ignored. This is actually not a bug, I<TAP::Parser> doesn't present
379 the fact and TAP specification does not require that anyway.
381 Note that this may be a problem when running I<Test::More> tests with C<no_plan>,
382 since it will add a plan matching the number of tests actually run even in case
383 the test dies. Do not do that -- always write a plan! In case it's not possible,
384 pass C<merge> argument when creating a I<TAP::Harness::JUnit> instance, and the
385 harness will detect such failures by matching certain comments.
387 Test durations are not mesaured. Unless the "notimes" parameter is provided (and
388 true), the test duration is recorded as testcase duration divided by number of
389 tests, otherwise it's set to 0 seconds. This could be addressed if the module
390 was reimplmented as a formatter.
392 The comments that are above the C<ok> or C<not ok> are considered the output
393 of the test. This, though being more logical, is against TAP specification.
395 I<XML::Simple> is used to generate the output. It is suboptimal and involves
398 During testing, the resulting files are not tested against the schema, which
399 would be a good thing to do.
403 Lubomir Rintel (Good Data) C<< <lubo.rintel@gooddata.com> >>
405 Source code for I<TAP::Harness::JUnit> is kept in a public GIT repository.
406 Visit L<http://repo.or.cz/w/TAP-Harness-JUnit.git> to get it.
408 =head1 COPYRIGHT & LICENSE
410 Copyright 2008, 2009 Good Data, All rights reserved.
412 This program is free software; you can redistribute it and/or modify it
413 under the same terms as Perl itself.