Test2-Harness-Renderer-JUnit
view release on metacpan or search on metacpan
lib/Test2/Harness/Renderer/JUnit.pm view on Meta::CPAN
if ( $f->{'plan'} ) {
if ( $f->{'plan'}->{'skip'} ) {
my $skip = $f->{'plan'}->{'details'};
$test->{'system-out'} .= "# SKIP $skip\n";
}
if ( $f->{'plan'}->{'count'} ) {
$test->{'plan'} = $f->{'plan'}->{'count'};
}
return;
}
if ( $f->{'harness_job_exit'} ) {
return unless $f->{'harness_job_exit'}->{'exit'};
# If we don't see
$test->{'testsuite'}->{'errors'}++;
$test->{'error-msg'} //= $f->{'harness_job_exit'}->{'details'} . "\n";
return;
}
# We just hit an ok/not ok line.
if ( $f->{'assert'} ) {
# Ignore subtests
return if ( $f->{'hubs'} && $f->{'hubs'}->[0]->{'nested'} );
my $test_num = $event->{'assert_count'} || $f->{'assert'}->{'number'};
$test_num = sprintf "%04d", $test_num if defined $test_num;
my $test_name = _squeaky_clean( $f->{'assert'}->{'details'} // 'UNKNOWN_TEST?' );
$test_name = join " - ", grep { defined } $test_num, $test_name;
$test->{'testsuite'}->{'tests'}++;
$self->close_open_failure_testcase( $test, $test_num );
warn Dumper $event unless $stamp;
my $run_time = $stamp - $test->{'last_job_start'};
$test->{'last_job_start'} = $stamp;
if ( $f->{'amnesty'} && grep { ( $_->{'tag'} // '' ) eq 'TODO' } @{ $f->{'amnesty'} } ) { # All TODO Tests
if ( !$f->{'assert'}->{'pass'} ) { # Failing TODO
push @{ $test->{'testcase'} }, $self->xml->testcase( { 'name' => "$test_name (TODO)", 'time' => $run_time, 'classname' => $test->{'testsuite'}->{'name'} }, "" );
}
elsif ( $self->{'allow_passing_todos'} ) { # junit parsers don't like passing TODO tests. Let's just not tell them about it if $ENV{ALLOW_PASSING_TODOS} is set.
push @{ $test->{'testcase'} }, $self->xml->testcase( { 'name' => "$test_name (PASSING TODO)", 'time' => $run_time, 'classname' => $test->{'testsuite'}->{'name'} }, "" );
}
else { # Passing TODO (Failure) when not allowed.
$test->{'testsuite'}->{'failures'}++;
$test->{'testsuite'}->{'errors'}++;
# Grab the first amnesty description that's a TODO message.
my ($todo_message) = map { $_->{'details'} } grep { $_->{'tag'} // '' eq 'TODO' } @{ $f->{'amnesty'} };
push @{ $test->{'testcase'} }, $self->xml->testcase(
{ 'name' => "$test_name (TODO)", 'time' => $run_time, 'classname' => $test->{'testsuite'}->{'name'} },
$self->xml->error(
{ 'message' => $todo_message, 'type' => "TodoTestSucceeded" },
$self->_cdata("ok $test_name")
)
);
}
}
elsif ( $f->{'assert'}->{'pass'} ) { # Passing test
push @{ $test->{'testcase'} }, $self->xml->testcase(
{ 'name' => $test_name, 'time' => $run_time, 'classname' => $test->{'testsuite'}->{'name'} },
""
);
}
else { # Failing Test.
$test->{'testsuite'}->{'failures'}++;
$test->{'testsuite'}->{'errors'}++;
my $message = "not ok" . ( $test_name ? " $test_name" : "" );
# Trap the test information. We can't generate the XML for this test until we get all the diag information.
$test->{'last_failure'} = {
'test_num' => $test_num,
'test_name' => $test_name,
'time' => $run_time,
'message' => $message,
'full_message' => "$message\n",
};
}
return;
}
# This is diag information. Append it to the last failure.
if ( $f->{'info'} && $test->{'last_failure'} ) {
foreach my $line ( @{ $f->{'info'} } ) {
next unless $line->{'details'};
chomp $line->{'details'};
$test->{'last_failure'}->{'full_message'} .= "# $line->{details}\n";
}
return;
}
}
# This is called when the last run is complete and we're ready to emit the junit file.
sub finish {
my $self = shift;
open( my $fh, '>:encoding(UTF-8)', $self->{'junit_file'} ) or die("Can't open '$self->{junit_file}' ($!)");
my $xml = $self->xml;
# These are method calls but you can't do methods with a dash in them so we have to store them as a SV and call it.
my $out_method = 'system-out';
my $err_method = 'system-err';
print {$fh} "<testsuites>\n";
my @jobs = sort { $a->{'job_name'} <=> $b->{'job_name'} } values %{ $self->{'tests'} };
foreach my $job (@jobs) {
print {$fh} $xml->testsuite(
$job->{'testsuite'},
@{ $job->{'testcase'} },
$xml->$out_method( $self->_cdata( $job->{$out_method} ) ),
$xml->$err_method( $self->_cdata( $job->{$err_method} ) ),
) . "\n";
}
print {$fh} "</testsuites>\n";
close $fh;
return;
}
# Because we want to test diag messages after a failed test, we delay closing failures
# until we see the end of the testcase or until we see a new test number.
sub close_open_failure_testcase {
my ( $self, $test, $new_test_number ) = @_;
# Need to handle failed TODOs
# The last test wasn't a fail.
return unless $test->{'last_failure'};
my $fail = $test->{'last_failure'};
# This causes the entire suite to choke. We don't want this.
# If we're here already, we've already failed the test. let's just make sure the person reviewing
# it knows the test count was messed up.
if ( defined $fail->{'test_num'}
&& defined $new_test_number
&& $fail->{'test_num'} == $new_test_number )
{
$fail->{'message'}
.= "# WARNING This test number has already been seen. Duplicate TEST # in output!\n";
}
my $xml = $self->xml;
push @{ $test->{'testcase'} }, $xml->testcase(
{ 'name' => $fail->{'test_name'}, 'time' => $fail->{'time'}, 'classname' => $test->{'testsuite'}->{'name'} },
$xml->failure(
{ 'message' => $fail->{message}, 'type' => 'TestFailed' },
$self->_cdata( $fail->{'full_message'} ) )
);
delete $test->{'last_failure'};
return;
}
sub xml {
my $self = shift;
return $self->{'xml'};
}
# These helpers were borrowed from https://metacpan.org/pod/TAP::Formatter::JUnit. Thanks!
###############################################################################
# Generates the name for the entire test suite.
sub _get_testsuite_name {
my $name = shift;
$name =~ s{^\./}{};
$name =~ s{^t/}{};
return _clean_to_java_class_name($name);
}
###############################################################################
# Cleans up the given string, removing any characters that aren't suitable for
# use in a Java class name.
sub _clean_to_java_class_name {
my $str = shift;
$str =~ s/[^-:_A-Za-z0-9]+/_/gs;
return $str;
}
###############################################################################
# Creates a CDATA block for the given data (which is made squeaky clean first,
# so that JUnit parsers like Hudson's don't choke).
sub _cdata {
my ( $self, $data ) = @_;
# When I first added this conditional, I returned $data and at one point it was returning ^A and breaking the xml parser.
return '' if ( !$data or $data !~ m/\S/ms );
return $self->xml->xmlcdata( _squeaky_clean($data) );
}
###############################################################################
# Clean a string to the point that JUnit can't possibly have a problem with it.
sub _squeaky_clean {
my $string = shift;
# control characters (except CR and LF)
$string =~ s/([\x00-\x09\x0b\x0c\x0e-\x1f])/"^".chr(ord($1)+64)/ge;
# high-byte characters
$string =~ s/([\x7f-\xff])/'[\\x'.sprintf('%02x',ord($1)).']'/ge;
return $string;
}
sub _timestamp {
my $time = shift;
return POSIX::strftime( '%Y-%m-%dT%H:%M:%S', localtime( int($time) ) );
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test2::Harness::Renderer::JUnit - Captures Test2::Harness results and emits a junit xml file.
=head1 SYNOPSIS
On the command line, with F<yath>:
JUNIT_TEST_FILE="/tmp/test-output.xml" ALLOW_PASSING_TODOS=1 yath test --renderer=Formatter --renderer=JUnit -j4 t/*.t
=head1 DESCRIPTION
C<Test2::Harness::Renderer::JUnit> provides JUnit output formatting sufficient
to be parsed by Jenkins and hopefully other junit parsers.
This code borrows many ideas from C<TAP::Formatter::JUnit> but unlike that module
does not provide a method to emit a different xml file for every testcase.
Instead, it defaults to emitting to a single B<junit.xml> to whatever the directory
was you were in when you ran yath. This can be overridden by setting the
C<JUNIT_TEST_FILE> environment variable
Timing information is included in the JUnit XML since this is native to C<Test2::Harness>
In standard use, "passing TODOs" are treated as failure conditions (and are
reported as such in the generated JUnit). If you wish to treat these as a
"pass" and not a "fail" condition, setting C<ALLOW_PASSING_TODOS=1> in your
environment will turn these into pass conditions.
The JUnit output generated was developed to be used by Jenkins
(L<https://jenkins.io/>). That's the build tool we use at the
moment and needed to be able to generate JUnit output for.
( run in 1.890 second using v1.01-cache-2.11-cpan-5735350b133 )