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 IO::Async::Test;

use Test::More;
use Test::Refcount;

use POSIX qw( ENOENT SIGTERM SIGUSR1 );
use constant ENOENT_MESSAGE => do { local $! = ENOENT; "$!" };

use IO::Async::Process;

use IO::Async::Loop;
use IO::Async::OS;

plan skip_all => "POSIX fork() is not available" unless IO::Async::OS->HAVE_POSIX_FORK;

my $loop = IO::Async::Loop->new_builtin;

testing_loop( $loop );

{
   my ( $invocant, $exitcode );

   my $process = IO::Async::Process->new(
      code => sub { return 0 },
      on_finish => sub { ( $invocant, $exitcode ) = @_; },
   );

   is_oneref( $process, '$process has refcount 1 before $loop->add' );

   is( $process->notifier_name, "nopid", '$process->notifier_name before $loop->add' );

   ok( !$process->is_running, '$process is not yet running' );
   ok( !defined $process->pid, '$process has no PID yet' );

   $loop->add( $process );

   is_refcount( $process, 2, '$process has refcount 2 after $loop->add' );

   my $pid = $process->pid;

   ok( $process->is_running, '$process is running' );
   ok( defined $pid, '$process now has a PID' );

   is( $process->notifier_name, "$pid", '$process->notifier_name after $loop->add' );

   wait_for { defined $exitcode };

   is( $invocant, $process, '$_[0] in on_finish is $process' );
   undef $invocant; # refcount

   ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { 0 }' );
   is( ($exitcode >> 8), 0,     'WEXITSTATUS($exitcode) after sub { 0 }' );

   ok( !$process->is_running, '$process no longer running' );
   ok( defined $process->pid, '$process still has PID after exit' );

   is( $process->notifier_name, "[$pid]", '$process->notifier_name after exit' );

   ok( $process->is_exited,     '$process->is_exited after sub { 0 }' );
   is( $process->exitstatus, 0, '$process->exitstatus after sub { 0 }' );

   ok( !defined $process->loop, '$process no longer in Loop' );

   is_oneref( $process, '$process has refcount 1 before EOS' );
}

{
   my $process = IO::Async::Process->new(
      code => sub { return 3 },
      on_finish => sub { },
   );

   $loop->add( $process );

   wait_for { !$process->is_running };

   ok( $process->is_exited,     '$process->is_exited after sub { 3 }' );
   is( $process->exitstatus, 3, '$process->exitstatus after sub { 3 }' );
}

{
   my ( $invocant, $exception, $exitcode );

   my $process = IO::Async::Process->new(
      code => sub { die "An exception\n" },
      on_finish => sub { die "Test failed early\n" },
      on_exception => sub { ( $invocant, $exception, undef, $exitcode ) = @_ },
   );

   is_oneref( $process, '$process has refcount 1 before $loop->add' );

   $loop->add( $process );

   is_refcount( $process, 2, '$process has refcount 2 after $loop->add' );

   wait_for { defined $exitcode };

   is( $invocant, $process, '$_[0] in on_exception is $process' );
   undef $invocant; # refcount

   ok( ($exitcode & 0x7f) == 0,      'WIFEXITED($exitcode) after sub { die }' );
   is( ($exitcode >> 8), 255,        'WEXITSTATUS($exitcode) after sub { die }' );
   is( $exception, "An exception\n", '$exception after sub { die }' );

   ok( $process->is_exited,           '$process->is_exited after sub { die }' );
   is( $process->exitstatus, 255,     '$process->exitstatus after sub { die }' );
   is( $process->exception, "An exception\n", '$process->exception after sub { die }' );

   is_oneref( $process, '$process has refcount 1 before EOS' );
}

{
   my $exitcode;

   my $process = IO::Async::Process->new(
      code => sub { die "An exception\n" },
      on_finish => sub { ( undef, $exitcode ) = @_ },
   );

   $loop->add( $process );

   wait_for { defined $exitcode };

   ok( ($exitcode & 0x7f) == 0, 'WIFEXITED($exitcode) after sub { die } on_finish' );
   is( ($exitcode >> 8), 255,   'WEXITSTATUS($exitcode) after sub { die } on_finish' );

   ok( $process->is_exited,           '$process->is_exited after sub { die } on_finish' );
   is( $process->exitstatus, 255,     '$process->exitstatus after sub { die } on_finish' );
   is( $process->exception, "An exception\n", '$process->exception after sub { die } on_finish' );
}

{
   my $process = IO::Async::Process->new(
      command => [ $^X, "-e", '1' ],
      on_finish => sub { },
   );

   $loop->add( $process );

   wait_for { !$process->is_running };

   ok( $process->is_exited,     '$process->is_exited after perl -e 1' );
   is( $process->exitstatus, 0, '$process->exitstatus after perl -e 1' );
}

{
   my $process = IO::Async::Process->new(
      command => [ $^X, "-e", 'exit 5' ],
      on_finish => sub { },
   );

   $loop->add( $process );

   wait_for { !$process->is_running };

   ok( $process->is_exited,     '$process->is_exited after perl -e exit 5' );
   is( $process->exitstatus, 5, '$process->exitstatus after perl -e exit 5' );
}

{
   # Just be paranoid in case anyone actually has this
   my $donotexist = "/bin/donotexist";
   $donotexist .= "X" while -e $donotexist;

   my ( $exception, $errno );

   my $process = IO::Async::Process->new(
      command => $donotexist,
      on_finish => sub { die "Test failed early\n" },
      on_exception => sub { ( undef, $exception, $errno ) = @_ },
   );

   $loop->add( $process );

   wait_for { !$process->is_running };

   is( $errno+0, ENOENT,         '$errno number after donotexist' ); 
   is( "$errno", ENOENT_MESSAGE, '$errno string after donotexist' );

   ok( $process->is_exited,           '$process->is_exited after donotexist' );
   is( $process->exitstatus, 255,     '$process->exitstatus after donotexist' );
   is( $process->errno,  ENOENT,         '$process->errno number after donotexist' );
   is( $process->errstr, ENOENT_MESSAGE, '$process->errno string after donotexist' );
   is( $process->exception, "", '$process->exception after donotexist' );
}

{
   $ENV{TEST_KEY} = "foo";

   my $process = IO::Async::Process->new(
      code => sub { $ENV{TEST_KEY} eq "bar" ? 0 : 1 },
      setup => [
         env => { TEST_KEY => "bar" },
      ],
      on_finish => sub { },
   );

   $loop->add( $process );

   wait_for { !$process->is_running };

   ok( $process->is_exited,     '$process->is_exited after %ENV test' );
   is( $process->exitstatus, 0, '$process->exitstatus after %ENV test' );
}

SKIP: {
   skip "This OS does not have signals", 2 unless IO::Async::OS->HAVE_SIGNALS;

   my $child_ready;
   $loop->watch_signal( USR1 => sub { $child_ready++ } );

   my $parentpid = $$;
   my $process = IO::Async::Process->new(
      code => sub {
         my $exitcode = 10;
         eval {
            local $SIG{TERM} = sub { $exitcode = 20; die };
            kill SIGUSR1 => $parentpid;
            sleep 60; # block on signal
         };
         return $exitcode;
      },
      on_finish => sub { },
   );

   $loop->add( $process );

   wait_for { $child_ready };

   $process->kill( SIGTERM );

   wait_for { !$process->is_running };

   ok( $process->is_exited,      '$process->is_exited after ->kill' );
   is( $process->exitstatus, 20, '$process->exitstatus after ->kill' );

   $loop->unwatch_signal( USR1 => );
}

done_testing;