The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use Signals::XSIG;
use strict;
use warnings;
use POSIX ();
use Config;
eval { require Time::HiRes };

# running the default emulator of Signals::XSIG should produce the
# same behavior as not using Signals::XSIG
#
# t/20a-defaults.t: signals that typically end the program somehow
# t/20b-defaults.t: signals that typically suspend or resume the program
# t/20c-defaults.t: signals that do not visibly affect the program

# it takes a few seconds to test each signal
# so this is the most time consuming test.

$ENV{"PERL5LIB"} = join ':', @INC;
if ($^O eq 'MSWin32') {
    $ENV{"PERL5LIB"} = join ';', @INC;
}

if (${^TAINT}) {
    $ENV{PATH} = "";
    ($^X) = $^X =~ /(.*)/;
    ($ENV{PERL5LIB}) = $ENV{PERL5LIB} =~ /(.*)/s;
}


my @sig_names = split ' ', $Config{sig_name};
my @sig_num = split ' ', $Config{sig_num};
my ($SIGCONT) = grep { $sig_names[$_] eq 'CONT' } @sig_num;
my %sig_exists = map { $_ => 1 } @sig_names;

# XXX -
# should we use  print ...
# or             syswrite select(), ... 
# ?

my $program_without_XSIG = <<'__PROGRAM_WITHOUT_XSIG__';
$|=0;
$SIG{'__SIGNAL__'} = 'DEFAULT';
print "Hello world";
kill '__SIGNAL__', $$;
sleep 1;
print "\nFoo!";
exit 0;
__PROGRAM_WITHOUT_XSIG__
;

my $program_with_XSIG = <<'__PROGRAM_WITH_XSIG__';
use Signals::XSIG;
$|=0;
$XSIG{'__SIGNAL__'} = [ sub { }, 'DEFAULT' ];
print "Hello world";
kill '__SIGNAL__', $$;
sleep 1;
print "\nFoo!";
exit 0;
__PROGRAM_WITH_XSIG__
;

# pause time was 3s. 5s is safer if your system is loaded.
our $PAUSE_TIME = $ENV{PAUSE_TIME} || 5;

sub pause {
    # 2.5-3 seconds is *usually* long enough to spawn a process
    # and give it time to send itself a signal that will suspend it.
    # If you test on a heavily loaded or old system, it might
    # need even more time.
    sleep $PAUSE_TIME || 1;
}

sub sig_exists {
    return exists $sig_exists{$_[0]};
}

sub test_default_behavior_for_signal {
    my ($signal) = @_;

    my $program1 = $program_without_XSIG;
    $program1 =~ s/__SIGNAL__/$signal/g;

    my $program2 = $program_with_XSIG;
    $program2 =~ s/__SIGNAL__/$signal/g;

    my $PID = "$signal.$$";
    open(PROG1, '>', "control_group.$PID.tt");
    print PROG1 $program1;
    close PROG1;

    open(PROG2, '>', "experimental_group.$PID.tt");
    print PROG2 $program2;
    close PROG2;

    unlink "out1.$PID","out2.$PID";

    open(OUT1, '>', "out1.$PID");
    my $pid1 = fork();
    if ($pid1 == 0) {
        open(STDOUT, '>&' . fileno(*OUT1));
        exec($^X,"control_group.$PID.tt");
        die;
    }

    open(OUT2, '>', "out2.$PID");
    my $pid2 = fork();
    if ($pid2 == 0) {
        open(STDOUT, '>&' . fileno(*OUT2));
        exec($^X,"experimental_group.$PID.tt");
        die;
    }

    pause();
    my $xpid1 = my $ypid1 = waitpid $pid1, &POSIX::WNOHANG;
    my $status1 = $?;

    my $xpid2 = my $ypid2 = waitpid $pid2, &POSIX::WNOHANG;
    my $status2 = $?;

    # some signals suspend a program; we need to deliver a SIGCONT

    kill 'CONT', $pid1, $pid2;
    $ypid1 || $ypid2 || pause();
    kill 'KILL', $pid1, $pid2;
    if ($ypid1 == 0) {
        $ypid1 = waitpid $pid1, 0;
        $status1 = $?;
    }
    if ($ypid2 == 0) {
        $ypid2 = waitpid $pid2, 0;
        $status2 = $?;
    }
    close OUT1;
    close OUT2;

    open(IN1, '<', "out1.$PID");
    my $in1 = join'', <IN1>;
    close IN1;
    open(IN2, '<', "out2.$PID");
    my $in2 = join'', <IN2>;
    close IN2;

    if (@ARGV == 0) {
        unlink "control_group.$PID.tt", "experimental_group.$PID.tt";
        unlink "out1.$PID", "out2.$PID";
    }

    return (
        { xpid => $xpid1, ypid => $ypid1, output => $in1, status => $status1 },
        { xpid => $xpid2, ypid => $ypid2, output => $in2, status => $status2 },
	      "control_group.$PID.tt", "experimental_group.$PID.tt",
	      "out1.$PID", "out2.$PID" );

}

sub ok_test_behavior {
    my ($basic, $module, $signal) = @_;
    my $failed = 0;

    ok($basic->{status} == $module->{status},
       "SIG$signal exit status was the same "
       . $basic->{status} . " == " . $module->{status})
        or $failed++;

    my $msg = $basic->{output} eq $module->{output}
        ? "" : "system: [$basic->{output}] ; module: [$module->{output}]";

    ok($basic->{output} eq $module->{output}
       # OpenBSD failure point on 0.09: SIGILL, SIGBUS, SIGSEGV
       #    should produce no output, but module produces
       #    "Hello world". This shouldn't be a deal killer.

       || ($^O =~ /openbsd/ && $basic->{output} eq ''),

       "program output with SIG$signal was the same $msg"
       . length($basic->{output}) . " === " . length($module->{output}))
        or $failed++;

    ok(!!$basic->{xpid} == !!$module->{xpid},
       "suspend behavior was the same for SIG$signal "
       . "($basic->{xpid} $basic->{ypid} / "
       . "$module->{xpid} $module->{ypid})");
    if ($failed) {
        diag "Default behavior failures for SIG$signal";
        return 0;
    }
    return 1;
}

sub on_failure_recommend_spike {

    # run  spike/analyze_default_signal_behavior.pl  on the signals that
    # failed this test. This way, we get information about what the
    # signals *should* do on this system in the test output, and we
    # can incorporate this data into the next fix.

    my (@failed_sigs) = @_;
    return if $ENV{NO_SPIKE};
    close STDOUT; open STDOUT, '>&STDERR';
    system($^X, "spike/analyze_default_signal_behavior.pl", 1, @failed_sigs);

}

1;