The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

use strict;
use warnings;
use Test::More tests => 17;
use TAP::Harness;
use IO::Scalar;
use File::Slurp qw(write_file);

###############################################################################
# Ensure timing correctness, when test has a plan
#
# Test once with merged output off, then once with it on; want to make sure that
# merging diagnostic output into the TAP doesn't monkey up the timings.
correct_timing_test_has_plan: {
    my $test     = qq|
        BEGIN { sleep 3 };
        END   { sleep 2 };
        use Test::More tests => 3;
        sleep 0;    pass "one";
        sleep 2;    pass "two";

        sleep 1;    diag "foo";
        sleep 1;    diag "bar";
        sleep 3;    diag "foobar";
        pass "three";
    |;
    my $expect = {
        '(init)'     => 3,
        '1 - one'    => 0,
        '2 - two'    => 2,
        '3 - three'  => 5,
        '(teardown)' => 2,
    };

    unmerged: {
        my $results = run_test($test, {
            timer => 1,
            merge => 0,
        } );
        ok $results, 'got JUnit - timing correctness w/test plan (unmerged)';
        verify_timings($results, $expect);
    }

    merged: {
        my $results = run_test($test, {
            timer => 1,
            merge => 1,
        } );
        ok $results, 'got JUnit - timing correctness w/test plan (merged)';
        verify_timings($results, $expect);
    }
}

###############################################################################
# Ensure timing correctness, when test has no plan
#
# The *first* test isn't going to be predictable/accurate w.r.t. the calculated
# timing, as it'll also involve the startup overhead.  As such, its skipped (by
# denoting it as "skip" in its test name).
correct_timing_test_unplanned: {
    my $test     = qq|
        BEGIN { sleep 3 };
        END   { sleep 2 };
        use Test::More qw(no_plan);
        sleep 0;    pass "one";
        sleep 2;    pass "two";

        sleep 1;    diag "foo";
        sleep 1;    diag "bar";
        sleep 3;    diag "foobar";
        pass "three";
    |;
    my $expect = {
        '1 - one'    => 3,  # init time is *hidden* in initial test
        '2 - two'    => 2,
        '3 - three'  => 5,
        '(teardown)' => 2,
    };

    my $results = run_test($test, {
        timer => 1,
        merge => 1,
    } );
    ok $results, 'got JUnit - timing correctness w/o test plan';
    verify_timings($results, $expect);
}


sub run_test {
    my $code = shift;
    my $opts = shift;
    my $file = "test-$$.t";

    my $junit = undef;
    my $fh    = IO::Scalar->new(\$junit);
    my $harness = TAP::Harness->new( {
        formatter_class => 'TAP::Formatter::JUnit',
        stdout          => $fh,
        %{$opts},
    } );

    write_file($file, $code);
    $harness->runtests($file);
    unlink $file;

    return $junit;
}

sub verify_timings {
    my $junit  = shift;
    my $expect = shift;

    my @lines = split /^/, $junit;
    my @tests = grep { /<testcase/ } @lines;

    foreach my $test (@tests) {
        my ($time) = ($test =~ /time="([^"]+)"/);
        my ($name) = ($test =~ /name="([^"]+)"/);
        if ((!defined $time) || (!defined $name)) {
            fail "... unexpected test line: $test";
            next;
        }

        if (exists $expect->{$name}) {
            rounds_to($time, $expect->{$name}, "... test timing: $name");
        }
        else {
            fail "... unexpected test name: $name";
            diag $test;
        }
    }
}

sub rounds_to {
    my ($got, $expected, $message) = @_;
    my $r_got      = sprintf('%1.0f', $got);
    my $r_expected = sprintf('%1.0f', $expected);
    local $Test::Builder::Level = $Test::Builder::Level + 1;
    is $r_got, $r_expected, $message;
}