The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
BEGIN{ if (not $] < 5.006) { require warnings; warnings->import } }

select(STDERR); $|=1;
select(STDOUT); $|=1;

use Test::More;
use t::Helper;
use t::Frontend;
use Config;
use File::Temp ();
use IO::CaptureOutput qw/capture/;
use Probe::Perl ();

#--------------------------------------------------------------------------#
# Skip on Win32 except for release testing
#--------------------------------------------------------------------------#

if ( $^O eq "MSWin32" ) {
    plan skip_all => "\$ENV{RELEASE_TESTING} required for Win32 timeout testing", 
        unless $ENV{RELEASE_TESTING};
    eval "use Win32::Job ()";
    plan skip_all => "Can't interrupt hung processes without Win32::Job"
        if $@;
}

#--------------------------------------------------------------------------#
# fixtures
#--------------------------------------------------------------------------#

my $perl = Probe::Perl->find_perl_interpreter();

my $quote = $^O eq 'MSWin32' || $^O eq 'MSDOS' ? q{"} : q{'};

#--------------------------------------------------------------------------#
# Test planning
#--------------------------------------------------------------------------#

my @cases = (
    {
        label => "regular < global < delay",
        program => '$now=time(); 1 while( time() - $now < 60); print qq{foo\n}; exit 0',
        output => [],
        timeout => 5,
        command_timeout => 30,
        delay => 60,
        exit_code => 9,
    },
    {
        label => "regular < delay < global",
        program => '$now=time(); 1 while( time() - $now < 30); print qq{foo\n}; exit 0',
        output => [],
        timeout => 5,
        delay => 30,
        command_timeout => 60,
        exit_code => 9,
    },
    {
        label => "global < regular < delay",
        program => '$now=time(); 1 while( time() - $now < 60); print qq{foo\n}; exit 0',
        output => [],
        command_timeout => 2,
        timeout => 5,
        delay => 60,
        exit_code => 9,
    },
    {
        label => "global < delay < regular",
        program => '$now=time(); 1 while( time() - $now < 5); print qq{foo\n}; exit 0',
        output => ["foo\n"],
        command_timeout => 2,
        delay => 5,
        timeout => 60,
        exit_code => 0,
    },
    {
        label => "delay < regular < global",
        program => '$now=time(); 1 while( time() - $now < 2); print qq{foo\n}; exit 0',
        output => ["foo\n"],
        delay => 2,
        timeout => 30,
        command_timeout => 60,
        exit_code => 0,
    },
    {
        label => "delay < global < regular",
        program => '$now=time(); 1 while( time() - $now < 2); print qq{foo\n}; exit 0',
        output => ["foo\n"],
        delay => 2,
        command_timeout => 30,
        timeout => 60,
        exit_code => 0,
    },
    {
        label => "global < delay",
        program => '$now=time(); 1 while( time() - $now < 30); print qq{foo\n}; exit 0',
        output => [],
        command_timeout => 5,
        delay => 30,
        exit_code => 9,
    },
    {
        label => "delay < global",
        program => '$now=time(); 1 while( time() - $now < 2); print qq{foo\n}; exit 0',
        output => ["foo\n"],
        delay => 2,
        command_timeout => 30,
        exit_code => 0,
    },
);

my $tests_per_case = 4 + test_fake_config_plan();
plan tests => 1 + $tests_per_case * @cases;

#--------------------------------------------------------------------------#
# tests
#--------------------------------------------------------------------------#

require_ok( "CPAN::Reporter" );

for my $c ( @cases ) {
SKIP: {
    skip "Couldn't run perl with relative path", $tests_per_case
        if $c->{relative} && system("perl -e 1") == -1;

    my @extra_config = $c->{command_timeout} 
                     ? ( command_timeout => $c->{command_timeout} ) : ();
    test_fake_config( @extra_config );

    my $fh = File::Temp->new( UNLINK => ! $ENV{PERL_CR_NO_CLEANUP} )
        or die "Couldn't create a temporary file: $!\nIs your temp drive full?";
    print {$fh} $c->{program}, "\n";
    $fh->flush;
    my ($output, $exit);
    my ($stdout, $stderr);
    my $start_time = time();
    my $cmd = $c->{relative} ? "perl" : $perl; 
    $cmd .= " $fh";
    warn "# sleeping for timeout test\n" if $c->{delay};
    eval {
        capture sub {
            ($output, $exit) = CPAN::Reporter::record_command( 
                $cmd, $c->{timeout}
            );
        }, \$stdout, \$stderr;
    };
    sleep 1; # pad the run time into the next second
    my $run_time = time() - $start_time;
    diag $@ if $@;
    my ($time_ok, $who, $diag);
    if ( $c->{timeout} ) {
        # (A) program delay, (B) regular timeout, (C) command timeout
        # ABC, ACB, BAC, BCA, CAB, CBA
        # Option 1 -- program ends before either timeout (ABC, ACB)
        if (    $c->{delay} < $c->{command_timeout}
            &&  $c->{delay} < $c->{timeout}
        ) {
            my ($next_t) = sort {$a <=> $b} ($c->{timeout}, $c->{command_timeout});
            $time_ok = $run_time < $next_t;
            $who = "no";
        }
        # Option 2 -- regular before program or command (BAC, BCA)
        elsif ( $c->{timeout} < $c->{command_timeout} 
            &&  $c->{timeout} < $c->{delay}
        ) {
            my ($next_t) = sort {$a <=> $b} ($c->{delay},$c->{command_timeout});
            $time_ok = $run_time < $next_t;
            $who = "regular";
        }
        # Option 3 -- command before program or regular (CAB, CBA)
        # C does nothing so are A,B in right order?
        else {
            # command timeout should be the default
            if ( $c->{timeout} < $c->{delay} ) {
                # did command timeout kill?
                $time_ok = $run_time < $c->{delay};
                $who = "regular"
            }
            else {
                # did no timeout happen
                $time_ok = $run_time < $c->{timeout};
                $who = "no"
            }
        }
        $diag = sprintf( 
            "timeout (%d) : command_timeout (%d) : ran (%d) : sleep (%d)", 
            $c->{timeout}, $c->{command_timeout}, $run_time, $c->{delay} 
        );
    }
    else {
        # command timeout should be the default
        $diag = sprintf( "timeout (%d) : ran (%d) : sleep (%d)", 
            $c->{command_timeout}, $run_time, $c->{delay} 
        );
        if ( $c->{command_timeout} < $c->{delay} ) {
            # did command timeout kill?
            $time_ok = $run_time < $c->{delay};
            $who = "command"
        }
        else {
            # did no timeout happen
            $time_ok = $run_time < $c->{command_timeout};
            $who = "no"
        }
    }

    ok( $time_ok, "$c->{label}: $who timeout") or diag $diag;
    like( $stdout, "/" . quotemeta(join(q{},@{ $output || [] })) . "/", 
        "$c->{label}: captured stdout" 
    );
    is_deeply( $output, $c->{output},  "$c->{label}: output as expected" )
        or diag "STDOUT:\n$stdout\n\nSTDERR:\n$stderr\n";
    is( $exit, $c->{exit_code}, "$c->{label}: exit code correct" ); 
} # SKIP
}