Skip to content

Instantly share code, notes, and snippets.

@pfig
Forked from melo/perl_smoker
Created May 21, 2011 21:21
Show Gist options
  • Save pfig/984910 to your computer and use it in GitHub Desktop.
Save pfig/984910 to your computer and use it in GitHub Desktop.

Revisions

  1. Pedro Figueiredo revised this gist May 22, 2011. 1 changed file with 9 additions and 9 deletions.
    18 changes: 9 additions & 9 deletions tap2junit
    Original file line number Diff line number Diff line change
    @@ -1,16 +1,16 @@
    #!/usr/bin/env perl
    =head1 NAME
    tap-to-junit-xml - convert perl-style TAP test output to JUnit-style XML
    tap2junit - convert perl-style TAP test output to JUnit-style XML
    =head1 SYNOPSIS
    tap-to-junit-xml [--help|--man]
    [--[no]hidesummary]
    [--input <tap input file>]
    [--output <junit output file>]
    [--puretap]
    [<test suite name>] [outputprefix]
    tap2junit [--help|--man]
    [--[no]hidesummary]
    [--input <tap input file>]
    [--output <junit output file>]
    [--puretap]
    [<test suite name>] [outputprefix]
    =head1 DESCRIPTION
    @@ -47,7 +47,7 @@ will display it (neither has an effect when --puretap is specified).
    =head1 EXAMPLE
    prove -v 2>&1 | tee tests.log
    tap-to-junit-xml "make test" testxml/tests < tests.log
    tap2junit "make test" testxml/tests < tests.log
    (JUnit-formatted XML is now in "testxml/tests*.xml".)
    @@ -516,7 +516,7 @@ sub _finish_test_block {

    sub cdata {
    my $s = shift;
    $s =~ s/\]\]>/\](warning: defanged by tap-to-junit-xml)\]>/gs;
    $s =~ s/\]\]>/\](warning: defanged by tap2junit)\]>/gs;
    return '<![CDATA['.$s.']]>';
    }

  2. Pedro Figueiredo renamed this gist May 22, 2011. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. @melo melo revised this gist Feb 5, 2011. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions perl_smoker
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,8 @@
    #!/bin/sh
    #
    # Use this as Jenkins Build "Execute shell" script
    #
    # Pedro Melo <[email protected]>

    ## Die on any errors
    set -ex
  4. @melo melo created this gist Feb 5, 2011.
    61 changes: 61 additions & 0 deletions perl_smoker
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    #!/bin/sh

    ## Die on any errors
    set -ex

    export OUTPUT=$WORKSPACE/logs
    rm -rf $OUTPUT
    mkdir -p $OUTPUT


    ## Perl setup: one local::lib per workspace + plus reuse the system wide version
    echo
    echo "**** Setup perl"
    eval $( perl -Mlocal::lib=$HOME/.perl5/current/local )
    OLD_PERL5LIB=$PERL5LIB
    eval $( perl -Mlocal::lib=$WORKSPACE/local )
    export PERL5LIB=$PERL5LIB:$OLD_PERL5LIB
    export PATH=~/.perl5/current/core/bin:$PATH

    echo PERL5LIB
    echo $PERL5LIB | perl -pe 's/:/\n /g; print " $_\n"'
    echo PATH
    echo $PATH | perl -pe 's/:/\n /g; print " $_\n"'


    ## Install dependencies
    echo
    echo "**** Install dependencies"
    cpanm --installdeps .

    ## TAP setup
    export PERL_TEST_HARNESS_DUMP_TAP=$OUTPUT/tap
    export TEST_VERBOSE=1


    ## Cleanup old runs
    rm -rf Makefile Makefile.old blib *.tar.gz $PERL_TEST_HARNESS_DUMP_TAP cover_db
    mkdir -p $PERL_TEST_HARNESS_DUMP_TAP


    ## Prepare out distro
    echo
    echo "**** Prepare module"
    unset PERL_MM_OPT
    if [ -e Makefile.PL ] ; then
    perl Makefile.PL PREFIX=$WORKSPACE/install_root
    make
    fi


    ## Run the tests
    echo
    echo "**** Run tests"
    #prove -vbl -MDevel::Cover=-silent,off,-summary,off >&1 | tee -a $OUTPUT/tests.tap
    prove -vbl >&1 | tee -a $OUTPUT/tests.tap


    ## Prepare JUnit stuff
    echo
    echo "**** Convert to JUnit"
    ~/releases/bin/tap-to-junit-xml --input=$OUTPUT/tests.tap --output=$OUTPUT/tests.xml
    547 changes: 547 additions & 0 deletions tap-to-junit-xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,547 @@
    #!/usr/bin/env perl
    =head1 NAME
    tap-to-junit-xml - convert perl-style TAP test output to JUnit-style XML
    =head1 SYNOPSIS
    tap-to-junit-xml [--help|--man]
    [--[no]hidesummary]
    [--input <tap input file>]
    [--output <junit output file>]
    [--puretap]
    [<test suite name>] [outputprefix]
    =head1 DESCRIPTION
    Parse test suite output in TAP (Test Anything Protocol,
    C<http://testanything.org/>) format, and produce XML output in a similar format
    to that produced by the <junit> ant task. This is useful for consumption by
    continuous-integration systems like Hudson (C<https://hudson.dev.java.net/>).
    C<"test suite name"> is a descriptive string used as the B<name> attribute on the
    top-level <testsuites> node of the output XML. Defaults to "make test".
    If C<outputprefix> is specified, multi-file output will be generated, with
    multiple XML files created using C<outputprefix> as the start of their
    filenames. The files are separated by testplan. This option is ignored
    if --puretap is specified (TAP only allows one testplan per input file).
    This prefix may contain slashes, in which case the files will be
    placed into a directory hierarchy accordingly (although care should be taken to
    ensure these directories exist in advance).
    If --input I<file name> is not specified, STDIN will be read.
    If C<outputprefix> or --output is not specified, a single XML file will be
    generated on STDOUT.
    --output I<file name> is used to write a single XML file to I<file name>.
    --puretap parses a single TAP source and handles parse errors and directives
    (todo, skip, bailout). --puretap ignores unknown (non-TAP) input. Without
    --puretap, the script will parse some additional non-TAP test input, such as
    Perl tests that can include a "Test Summary Report", but it won't generate
    correct XML unless the TAP testplan comes before the test cases.
    --hidesummary report (the default) will hide the summary report, --no-hidesummary
    will display it (neither has an effect when --puretap is specified).
    =head1 EXAMPLE
    prove -v 2>&1 | tee tests.log
    tap-to-junit-xml "make test" testxml/tests < tests.log
    (JUnit-formatted XML is now in "testxml/tests*.xml".)
    =head1 DEPENDENCIES
    Getopt::Long
    Pod::Usage
    TAP::Parser
    Time::HiRes
    XML::Generator
    =head1 BUGS
    - Output is optimized for Hudson, and may not look quite as good in
    other UIs.
    - Doesn't do anything with the STDERR from tests.
    - Doesn't fill in the 'errors' attribute in the <testsuite> element.
    (--puretap handles parse errors)
    - Doesn't handle "todo" or "skip" (--puretap does)
    - Doesn't get the elapsed time for each 'test' (i.e. assertion.)
    (TAP output has no elapsed time convention).
    =head1 SOURCE
    http://github.com/jmason/tap-to-junit-xml/tree/master
    =head1 AUTHOR
    original, junit_xml.pl, by Matisse Enzer <matisse at matisse.net>; see
    C<http://twoalpha.blogspot.com/2007/01/junit-style-xml-from-perl-test-files.html>.
    pretty much entirely rewritten by Justin Mason <junit at jmason.org>, Feb 2008.
    Miscellaneous fixes and mods (--puretap) by Jascha Lee <jascha at yahoo-inc.com>, Mar 2009.
    =head1 VERSION
    Mar 27 2008 jm
    Mar 17 2009 jl
    =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

    use strict;
    use warnings;
    use lib "$ENV{HOME}/lib";

    use Getopt::Long qw(:config no_ignore_case);
    use Pod::Usage;
    use TAP::Parser;
    use Time::HiRes qw(gettimeofday tv_interval);
    use XML::Generator qw(:noimport);

    my %opts;
    pod2usage() unless GetOptions( \%opts, 'help|h',
    'hidesummary!',
    'input=s',
    'man',
    'output=s',
    'puretap'
    );

    pod2usage(-verbose => 1) if defined $opts{'help'};
    pod2usage(-verbose => 2) if defined $opts{'man'};

    my $opt_suitename = shift @ARGV;
    my $opt_multifile = 0;
    my $opt_mfprefix;

    if (defined $ARGV[0]) {
    $opt_multifile = 1;
    $opt_mfprefix = $ARGV[0];
    }

    # should the 'Test Summary Report' at the end of a test suite be displayed
    # as if it was a testcase? in my opinion, no
    my $HIDE_TEST_SUMMARY_REPORT = defined $opts{'hidesummary'} ? $opts{'hidesummary'} : 1;

    my $suite_name = $opt_suitename || 'make test';
    my $safe_suite_name = $suite_name; $safe_suite_name =~ s/[^-:_A-Za-z0-9]+/_/gs;

    # TODO: it'd be nice to respect 'Universal desirable behavior #1' from
    # http://testanything.org/wiki/index.php/TAP_Consumers -- 'Should work on the
    # TAP as a stream (ie. as each line is received) rather than wait until all the
    # TAP is received'. But it seems TAP::Parser itself doesn't support it!
    # maybe when TAP::Parser does that, we'll do it too.
    my $tapfh;
    if ( defined $opts{'input'} ) {
    open $tapfh, '<', $opts{'input'} or die "Can't open TAP file '$opts{'input'}': $!\n";
    }
    else {
    $tapfh = \*STDIN;
    }

    my $outfh;
    if ( defined $opts{'output'} ) {
    open $outfh, '>', $opts{'output'} or die "Can't open output file '$opts{'output'}' for writing: $!\n";
    }
    else {
    $outfh = \*STDOUT;
    }

    my $tap = TAP::Parser->new( { source => $tapfh } );
    my $xmlgen = XML::Generator->new( ':pretty');
    my $xmlgenunescaped = XML::Generator->new( escape => 'unescaped',
    conformance => 'strict',
    pretty => 2
    );
    my @properties = _get_properties($xmlgen);
    if ( defined $opts{'puretap'} ) {
    #
    # Instead of trying to parse everything in one pass, which fails if the
    # testplan is last, parse through the results for the test cases and
    # then construct the <testsuite> information from the TAP and wrap it
    # around the test cases. Ignore 'unknown' information. [JL]
    #
    my @testcases = _parse_testcases( $tap, $xmlgen );
    errorOut( $tap, $xmlgen ) if $tap->parse_errors;
    print $outfh $xmlgen->testsuites(
    $xmlgen->testsuite( { name => $safe_suite_name,
    tests => $tap->tests_planned,
    failures => scalar $tap->failed,
    errors => 0,
    time => 0,
    id => 1 },
    @testcases ));

    }
    else {
    my $test_results = _parse_tests( $tap, $xmlgen );
    if ($opt_multifile) {
    _gen_junit_multifile_xml( $xmlgen, \@properties, $test_results );
    } else {
    print $outfh _get_junit_xml( $xmlgen, \@properties, $test_results );
    }
    }
    exit;

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

    sub _get_junit_xml {
    my ( $xmlgen, $properties, $test_results ) = @_;
    my $xml = "<?xml version='1.0' encoding='UTF-8' ?>\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 "<?xml version='1.0' encoding='UTF-8' ?>\n";
    print OUT $testsuite;
    close OUT;
    $count++;
    }
    }

    #
    # Wrap up parse errors and output them as test cases.
    #
    sub errorOut {
    my $parser = shift;
    my $xmlgen = shift;
    die "errorOut() needs some args" unless $parser and $xmlgen;
    my ($xml, @errors, $name);
    my $count = 1;
    foreach my $error ( $parser->parse_errors ) {
    $name = sprintf "%s%02d", 'Error_', $count++;
    $xml = $xmlgen->testcase( { name => $name,
    classname => 'TestsNotRun.ParseError',
    time => 0 },

    $xmlgen->error( { type => 'TAPParseError',
    message => $error } ));
    push @errors, $xml;
    }
    print $outfh $xmlgen->testsuites(
    $xmlgen->testsuite( { name => 'TestsNotRun.ParseError',
    tests => $tap->tests_planned,
    failures => 0,
    errors => scalar $tap->parse_errors,
    time => 0,
    id => 1 },
    @errors ));
    exit 86;
    }

    #
    # Construct an array of XML'd test cases
    #
    sub _parse_testcases {
    my $parser = shift;
    my $xmlgen = shift;
    return () unless $parser and $xmlgen;
    my ($name, $directive, $xml, @testcases);

    while ( my $result = $parser->next ) {
    if ( $result->is_bailout ) {
    $xml = $xmlgen->testcase( { name => 'BailOut',
    classname => "$safe_suite_name.Tests",
    time => 0 },

    $xmlgen->error( { type => 'BailOut',
    message => $result->explanation } ));

    push @testcases, $xml;
    last;
    }
    next unless $result->is_test;
    $directive = $result->directive;
    $name = sprintf "%s%02d", 'Test_', $result->number;
    $name .= "_$directive" if $directive;
    if ( $result->is_ok ) {
    $xml = $xmlgen->testcase( { name => $name,
    classname => "$safe_suite_name.Tests",
    time => 0 } );
    push @testcases, $xml;
    }
    else {
    $xml = $xmlgen->testcase( { name => $name,
    classname => "$safe_suite_name.Tests",
    time => 0 },
    $xmlgen->failure( { type => 'TAPTestFailed',
    message => $result->as_string } ));
    push @testcases, $xml;
    }
    }

    return @testcases;
    }

    sub _parse_tests {
    my ( $parser, $xmlgen ) = @_;

    my $ctx = {
    testsuites => [ ],
    test_name => 'notest',
    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+$//;

    # warn "JMD $t $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 <testsuite> 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 => test_name_to_classname($ctx->{test_name}),
    name => 'result',
    'time' => 0,
    };
    my $failure = $xmlgen->failure({
    type => "OverallTestsFailed",
    message => $s
    }, "__FAILUREMESSAGETODO__");

    if (!$HIDE_TEST_SUMMARY_REPORT) {
    push @{$ctx->{test_cases}}, $xmlgen->testcase($test_case, $failure);
    }
    }
    elsif ($s =~ /^(\S+?)\.\.\.+1\.\.(\d+?)\s*$/) {
    # perl 5.6.x "Test" format plan line
    # unknown t/basic_lint....................1..1

    my ($name, $nt) = ($1,$2);
    if ($ctx->{plan_ntests}) { # only if there have been tests planned
    _finish_test_block($ctx);
    }

    $ctx->{plan_ntests} = $nt+0;
    $ctx->{test_name} = "$name.t";
    }
    }
    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;
    }

    # clean this up in a Hudson-compatible way; ":" and "/" are out, "." also causes
    # trouble by creating an extra "directory" in the results

    my $test_case = {
    classname => test_name_to_classname($ctx->{test_name}),
    name => sprintf("test %6d", $ntest), # space-padding ensures ordering
    'time' => 0,
    };

    $ctx->{case_tests}++;
    my $failure = undef;
    if ($s =~ /^not /i) {
    $ctx->{case_failures}++;
    $failure = $xmlgen->failure({
    type => "TAPTestFailed",
    message => $s
    }, "__FAILUREMESSAGETODO__");
    push @{$ctx->{test_cases}}, $xmlgen->testcase($test_case, $failure);
    }
    else {
    push @{$ctx->{test_cases}}, $xmlgen->testcase($test_case);
    }
    }

    $ctx->{sysout} .= $s."\n";
    }

    if (scalar(@{$ctx->{test_cases}}) == 0 &&
    scalar(@{$ctx->{testsuites}}) == 0)
    {
    # no tests found! create a <testsuite> block containing *something* at least
    $ctx->{case_tests}++;
    my $test_case = {
    classname => test_name_to_classname($ctx->{test_name}),
    name => 'result',
    'time' => 0,
    };
    push @{$ctx->{test_cases}}, $xmlgen->testcase($test_case);
    }

    _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 (or at least something Hudson will
    # consume)
    my $name = $ctx->{test_name};
    $name =~ s/[^-:_A-Za-z0-9]+/_/gs;
    $name = "$safe_suite_name.$name"; # a "directory" for the suite name

    my $testsuite = {
    'time' => $elapsed_time,
    'name' => $name,
    tests => $ctx->{case_tests},
    failures => $ctx->{case_failures},
    'id' => $ctx->{case_id},
    errors => 0,
    };

    my @fixedcases = ();
    foreach my $tc (@{$ctx->{test_cases}}) {
    if ($tc =~ s/__FAILUREMESSAGETODO__/ cdata($ctx->{sysout}) /ges) {
    push @fixedcases, \$tc; # inhibits escaping!
    } else {
    push @fixedcases, $tc;
    }
    }

    # use "unescaped"; we have already fixed escaping on these strings.
    # note that a reference means 'this is unescaped', bizarrely.
    push @{$ctx->{testsuites}}, $xmlgenunescaped->testsuite($testsuite,
    @fixedcases,
    \("<system-out>\n".cdata($ctx->{sysout})."\n</system-out>"),
    \("<system-err />"));

    _new_ctx($ctx);
    };

    sub cdata {
    my $s = shift;
    $s =~ s/\]\]>/\](warning: defanged by tap-to-junit-xml)\]>/gs;
    return '<![CDATA['.$s.']]>';
    }

    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;
    }

    sub test_name_to_classname {
    my $safe = shift;
    $safe =~ s/[^-:_A-Za-z0-9]+/_/gs;
    $safe = "$safe_suite_name.$safe"; # a "directory" for the suite name
    $safe;
    }

    __END__
    # JUnit references:
    # http://www.nabble.com/JUnit-4-XML-schematized--td13946472.html
    # http://jra1mw.cvs.cern.ch:8180/cgi-bin/jra1mw.cgi/org.glite.testing.unit/config/JUnitXSchema.xsd?view=markup
    # skipped tests:
    # https://hudson.dev.java.net/issues/show_bug.cgi?id=1251
    # Hudson source:
    # http://fisheye5.cenqua.com/browse/hudson/hudson/main/core/src/main/java/hudson/tasks/junit/CaseResult.java