Sunday, January 21, 2007

JUnit-Style XML from Perl Test Files

Here is an experimental program that runs Perl test files and produces the same sort of XML output as the <junit> ant task. Getting this XML output is helpful when running Perl tests under CruiseControl.
#!/usr/bin/perl

use strict;
use warnings;

use Test::Harness::Straps;
use Time::HiRes qw(gettimeofday tv_interval);
use XML::Generator ':noimport';

my @files = @ARGV;

my $strap     = Test::Harness::Straps->new;
my $generator = XML::Generator->new(':pretty');

my @properties = _get_properties($generator);
my $test_results = _run_tests( $strap, $generator, @files );

my $xml = _get_junit_xml( $generator, \@properties, $test_results );

print "$xml\n";

exit;

#-------------------------------------------------------------------------------

sub _get_junit_xml {
    my ( $generator, $properties, $test_results ) = @_;

    my $system_out = 'system-out';
    my $system_err = 'system-err';
    my $xml        = '';
    $xml .= "\n";
    $xml .= $generator->testsuite(
        {
            errors   => 0,
            failures => $test_results->{total_failures},
            name     => 'name of the test suite',
            tests    => $test_results->{total_tests},
            'time'   => $test_results->{total_time},
        },
        $generator->properties(@properties),
        @{ $test_results->{test_cases} },
        $generator->$system_out(),
        $generator->$system_err(),

    );
    return $xml;
}

sub _run_tests {
    my ( $strap, $generator, @files ) = @_;
    my @test_cases     = ();
    my $total_tests    = 0;
    my $total_time     = 0;
    my $total_failures = 0;

    foreach my $test_file (@files) {
        my $start_time   = [gettimeofday];
        my $file_results = $strap->analyze_file($test_file);
        my $elapsed_time = tv_interval( $start_time, [gettimeofday] );
        $total_time += $elapsed_time;
        foreach my $assertion ( @{ $file_results->details } ) {
            if ( _skip($assertion) ) {
                next;
            }
            $total_tests++;
            $total_failures += _is_failure($assertion);
            my $test_case = {
                classname => $test_file,
                name      => $assertion->{name},
                'time'    => 0,
            };
            push @test_cases, $generator->testcase($test_case);
        }
    }
    my $test_results = {
        total_time     => $total_time,
        test_cases     => \@test_cases,
        total_tests    => $total_tests,
        total_failures => $total_failures,
    };
    return $test_results;
}

sub _skip {
    my $assertion = shift;
    return $assertion->{type} =~ / skip | todo /x;
}

sub _is_failure {
    my $assertion = shift;
    return !$assertion->{ok};
}

sub _get_properties {
    my $generator  = shift;
    my @properties = ();
    foreach my $key ( sort keys %ENV ) {
        push @properties,
          $generator->property( { name => "$key", value => $ENV{$key} } );
    }
    return @properties;
}

__END__

=head1 NAME

junit_xml.pl - Run Perl tests and get JUnit-style XML output

=head1 SYNOPSIS

   junit_xml.pl file1 [ file2 ... ]

=head1 DESCRIPTION

Experimental script to run perl test files and produce the
same XML output as produced by the <junit> ant task.

=head1 DEPENDENCIES

 Test::Harness
 Time::HiRes
 XML::Generator 

=head1 BUGS

  - Doesn't do anything with the STDERR from tests.
  - Doesn't fill in the 'errors' attribute in the  <testsuite> element.
  - Doesn't handle when all tests in file are skipped (skip_all)
  - Doesn't get the elapsed time for each 'test' (i.e. assertion.)


=head1 AUTHOR

Matisse Enzer <matisse@matisse.net>

=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
Updates: This post was originally published on 2007-01-21. 2008-05-26: I noticed that Justin Mason has created a better script for converting TAP output to JUnit-style XML

Technorati Tags: ,

2 comments:

Randy J. Ray said...

I know this is an older post, but have you seen the CPAN module, TAP::Harness::JUnit?

Matisse Enzer said...

Thanks Randy, I did not know about TAP::Harness::JUnit and am glad to see it out there.