#!/usr/bin/perl use strict; use warnings; my $opt_suitename = shift @ARGV; my $opt_multifile = 0; my $opt_mfprefix; if (defined $ARGV[0]) { $opt_multifile = 1; $opt_mfprefix = $ARGV[0]; } use TAP::Parser; use Time::HiRes qw(gettimeofday tv_interval); use XML::Generator qw(:noimport); my $suite_name = $opt_suitename || 'make test'; my $tout = join("", ); my $tap = TAP::Parser->new( { tap => $tout } ); my $xmlgen = XML::Generator->new( ':pretty', ':std'); my @properties = _get_properties($xmlgen); my $test_results = _parse_tests( $tap, $xmlgen ); if ($opt_multifile) { _gen_junit_multifile_xml( $xmlgen, \@properties, $test_results ); } else { print STDOUT _get_junit_xml( $xmlgen, \@properties, $test_results ); } exit; #------------------------------------------------------------------------------- sub _get_junit_xml { my ( $xmlgen, $properties, $test_results ) = @_; my $xml = "\n" . $xmlgen->testsuites({ name => $suite_name, }, @$test_results ); return $xml; } sub _gen_junit_multifile_xml { my ( $xmlgen, $properties, $test_results ) = @_; my $count = 1; foreach my $testsuite (@$test_results) { open OUT, ">${opt_mfprefix}.${count}.xml" or die "cannot write ${opt_mfprefix}-${count}.xml"; print OUT "\n"; print OUT $testsuite; close OUT; $count++; } } sub _parse_tests { my ( $parser, $xmlgen ) = @_; my $ctx = { testsuites => [ ], test_name => 'unknown', plan_ntests => 0, case_id => 0, }; _new_ctx($ctx); my $lastunk = ''; # unknown t/basic_lint......... # plan 1..1 # comment # Running under perl version 5.008008 for linux # comment # Current time local: Thu Jan 24 17:44:30 2008 # comment # Current time GMT: Thu Jan 24 17:44:30 2008 # comment # Using Test.pm version 1.25 # unknown /usr/bin/perl -T -w ../spamassassin.raw -C log/test_rules_copy --siteconfigpath log/localrules.tmp -p log/test_default.cf -L --lint # unknown Checking anything # test ok 1 # test ok 2 # unknown t/basic_meta......... # plan 1..2 # comment # Running under perl version 5.008008 for linux # comment # Current time local: Thu Jan 24 17:44:31 2008 # comment # Current time GMT: Thu Jan 24 17:44:31 2008 # comment # Using Test.pm version 1.25 # test not ok 1 # comment # Failed test 1 in t/basic_meta.t at line 91 # test ok 2 # unknown Failed 1/2 subtests # unknown t/basic_obj_api...... # plan 1..4 # comment # Running under perl version 5.008008 for linux # comment # Current time local: Thu Jan 24 17:44:33 2008 # comment # Current time GMT: Thu Jan 24 17:44:33 2008 # comment # Using Test.pm version 1.25 # test ok 1 # test ok 2 # test ok 3 # test ok 4 # test ok 9 # unknown # unknown Test Summary Report # unknown ------------------- # unknown t/basic_meta.t (Wstat: 0 Tests: 2 Failed: 1) # unknown Failed test: 1 # unknown Files=3, Tests=7, 6 wallclock secs ( 0.01 usr 0.00 sys + 4.39 cusr 0.23 csys = 4.63 CPU) # unknown Result: FAIL # unknown Failed 1/3 test programs. 1/7 subtests failed. # unknown make: *** [test_dynamic] Error 255 while ( my $r = $parser->next ) { my $t = $r->type; my $s = $r->as_string; $s =~ s/\s+$//; if ($t eq 'unknown') { $lastunk = $s; # PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e" "test_harness(1, 'blib/lib', 'blib/arch')" t/basic_* # if ($s =~ /test_harness\(.*?\)" (.+)$/) { # $suite_name = $1; # } if ($s =~ /^Test Summary Report$/) { # create a block for the summary $ctx->{plan_ntests} = 0; $ctx->{test_name} = "Test Summary Report"; $ctx->{case_tests} = 1; _finish_test_block($ctx); } elsif ($s =~ /^Result: FAIL$/) { $ctx->{case_tests}++; $ctx->{case_failures}++; my $test_case = { classname => $ctx->{test_name}, name => $ctx->{test_name}, 'time' => 0, }; my $failure = $xmlgen->failure({ type => "OverallTestsFailed", message => $s }, $s); push @{$ctx->{test_cases}}, $xmlgen->testcase($test_case, $failure); } } elsif ($t eq 'plan') { if ($ctx->{plan_ntests}) { # only if there have been tests planned _finish_test_block($ctx); } $ctx->{plan_ntests} = 0; $s =~ /(\d+)$/ and $ctx->{plan_ntests} = $1+0; $ctx->{test_name} = $lastunk; $ctx->{test_name} =~ s/\.*\s*$//gs; $ctx->{test_name} .= ".t"; } elsif ($t eq 'test') { my $ntest = 0; if ($s =~ /(?:not |)\S+ (\d+)/) { $ntest = $1+0; } if ($ntest > $ctx->{plan_ntests}) { # jump in test numbers, more than planned; this is probably TAP::Parser's wierdness. # (when it sees the "ok" line at the end of a test case with no number, # it outputs the current total number of tests so far.) next; } my $test_case = { classname => $ctx->{test_name}, name => "$ctx->{test_name}: $ntest", 'time' => 0, }; $ctx->{case_tests}++; my $failure = undef; if ($s =~ /^not /i) { $ctx->{case_failures}++; $failure = $xmlgen->failure({ type => "TAPTestFailed", message => $s }, $s); } push @{$ctx->{test_cases}}, $xmlgen->testcase($test_case, $failure); } $ctx->{sysout} .= $s."\n"; } _finish_test_block($ctx); return $ctx->{testsuites}; } sub _new_ctx { my $ctx = shift; $ctx->{start_time} = [gettimeofday]; $ctx->{test_cases} = []; $ctx->{case_tests} = 0; $ctx->{case_failures} = 0; $ctx->{case_time} = 0; $ctx->{case_id}++; $ctx->{sysout} = ''; return $ctx; } sub _finish_test_block { my $ctx = shift; $ctx->{sysout} =~ s/\n\S+\.*\s*\n$/\n/s; # remove next test's "t/foo....." line my $elapsed_time = 0; # TODO #my $elapsed_time = tv_interval( $ctx->{start_time}, [gettimeofday] ); # clean it up to valid Java packagename format my $name = $suite_name.":".$ctx->{test_name}; $name =~ s/[^-:_A-Za-z0-9]+/_/gs; my $testsuite = { 'time' => $elapsed_time, 'name' => $name, 'package' => $name, tests => $ctx->{case_tests}, failures => $ctx->{case_failures}, 'id' => $ctx->{case_id}, errors => 0, }; my $system_out = 'system-out'; my $system_err = 'system-err'; push @{$ctx->{testsuites}}, $xmlgen->testsuite($testsuite, @{$ctx->{test_cases}}, $xmlgen->$system_out($ctx->{sysout}), $xmlgen->$system_err()); _new_ctx($ctx); }; sub _get_properties { my $xmlgen = shift; my @props; foreach my $key ( sort keys %ENV ) { push @props, $xmlgen->property( { name => "$key", value => $ENV{$key} } ); } return @props; } __END__ =head1 NAME junit_xml.pl - Run Perl tests and get JUnit-style XML output =head1 SYNOPSIS junit_xml.pl < file1 =head1 DESCRIPTION Experimental script to run perl test files and produce the same XML output as produced by the ant task. =head1 DEPENDENCIES TAP::Parser Time::HiRes XML::Generator =head1 BUGS - Doesn't do anything with the STDERR from tests. - Doesn't fill in the 'errors' attribute in the element. - Doesn't handle "todo" or "skip" - Doesn't get the elapsed time for each 'test' (i.e. assertion.) =head1 AUTHOR original by Matisse Enzer entirely rewritten by Justin Mason =head1 COPYRIGHT & LICENSE Copyright (c) 2007 Matisse Enzer. All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut # JUnit references: # see http://www.nabble.com/JUnit-4-XML-schematized--td13946472.html