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

use strict;
use warnings;

use Test::More;
use Test::Exception;
require Test::NoWarnings if $ENV{RELEASE_TESTING};

use IPC::Cmd qw( can_run run );
use IPC::Run ();
use Probe::Perl;

use Test::SVN::Repo;

if ($^O eq 'MSWin32') {
    plan skip_all => 'Tests not valid on Win32';
    exit;
}

my $svn;
unless ($svn = can_run('svn')) {
    plan skip_all => 'Subversion not installed';
    exit;
}

my %users = ( userA => 'passA', userB => 'passB' );

note 'Port range tests'; {

    # This mysteriously doesn't work on win32.
    # I can manually start multiple svnserve instances on a single port.
    # Its as if they get queued up - the first one serves the requests,
    # and the second takes over once the first has exited.

    my $repo = Test::SVN::Repo->new( users      => \%users,
                                     start_port => 50000,
                                     end_port   => 60000 );
    my $port = $repo->server_port;
    ok($port >= $repo->start_port, '... port is within specified range');
    ok($port <= $repo->end_port,   '... port is within specified range');

    # Try creating a server on a port we know is taken
    my $retry_count = 5;
    my $tempdir = File::Temp->newdir;
    throws_ok { Test::SVN::Repo->new(users       => \%users,
                                     start_port  => $port,
                                     end_port    => $port,
                                     retry_count => $retry_count,
                                     root_path   => $tempdir,
                                     keep_files  => 0) }
        qr/Giving up after $retry_count attempts/,
        '... server gives up if no ports available';
    ok(! -d $tempdir, '... and root path gets cleaned up');
}

note 'Check that svnserve gets cleaned up'; {

    # Killing the child process doesn't seem to work on win32.
    # IPC::Run confirms this behaviour. Processes can only be KILLED
    # under win32.

    for my $signame ( qw( ABRT BUS EMT FPE HUP ILL INT PIPE QUIT SEGV SYS TERM TRAP ) ) {
        next unless exists $SIG{$signame};

        my $pid;
        lives_ok { $pid = spawn_and_signal($signame) }
            '... child process started okay';

        like($pid, qr/^\d+$/, '... got valid pid for server process');

        # Check that the server (grandchild process) exits if we
        # kill its parent
        ok(! process_exists($pid), '... svnserve process has shutdown after receiving signal ' . $signame)
    }
}

note 'Forking'; {

    # Two repos, one local, one global
    our $global_repo = Test::SVN::Repo->new( users => \%users );
    my  $local_repo  = Test::SVN::Repo->new( users => \%users );

    ok(run_ok($svn, 'info', $global_repo->url), '... global server is up');
    ok(run_ok($svn, 'info', $local_repo->url),  '... local server is up');

    my $child_count = 0;
    for (1 .. 8) {
        my $pid = fork;
        next unless defined $pid;
        if ($pid) {
            $child_count++;
        }
        else {
            # Just exit in the child
            exit 0;
        }
    }
    for (1 .. $child_count) {
        waitpid(-1, 0);
    }

    ok(run_ok($svn, 'info', $global_repo->url),
        '... global server is still up');

    ok(run_ok($svn, 'info', $local_repo->url),
        '... local server is still up');
}

note 'Exit time cleanup for non-server mode'; {
    my $tempdir = File::Temp->newdir;
    in_child(sub {
        our $repo =
            Test::SVN::Repo->new( root_path => $tempdir, keep_files => 0 );
    });

    ok(! -d $tempdir, '... root path got cleaned up');
}

note 'Exit time file cleanup for server mode'; {
    my $tempdir = File::Temp->newdir;
    in_child(sub {
        our $repo =
            Test::SVN::Repo->new( root_path  => $tempdir,
                                  keep_files => 0,
                                  users      => \%users );
    });

    ok(! -d $tempdir, '... root path got cleaned up');
}

note 'Exit time server cleanup for server mode'; {
    my $tempdir = File::Temp->newdir;
    my $server_pid = run_repo_in_child($tempdir);
    ok(! process_exists($server_pid), '... server got cleaned up');
}

note 'Exit time foreign process non-cleanup'; {
    my $tempdir = File::Temp->newdir;
    my $repo = Test::SVN::Repo->new( users => \%users );

    my $server_pid = run_repo_in_child($tempdir);
    ok(! process_exists($server_pid), '... child server got cleaned up');

    ok(process_exists($repo->server_pid), '... parent server still running');
    ok(-d $repo->root_path, '... parent root path still there');
}

Test::NoWarnings::had_no_warnings() if $ENV{RELEASE_TESTING};
done_testing();

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

sub process_exists {
    my ($pid) = @_;
    return kill(0, $pid);
}

sub run_ok {
    my (@cmd) = @_;
    return scalar run( command => \@cmd );
}

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

    my $code = <<'END';
my $repo = Test::SVN::Repo->new( users => { a => 'b' } );
$| = 1;
print $repo->server_pid, "\n";
1 while 1;
END

    # Spawn a child process that starts a server (grandchild process).
    my $perl = Probe::Perl->find_perl_interpreter;
    my @cmd = ( $perl, '-MTest::SVN::Repo', '-e' => $code);
    my ($in, $out, $err);
    my $h = IPC::Run::start(\@cmd, \$in, \$out, \$err);

    # Obtain the server pid (grandchild)
    my $pid;
    while (not $pid) {
        die "Child process has died: $err" if not $h->pumpable;
        $h->pump;
        $pid = $out;
        chomp $pid;
    }

    # Kill the child process
    $h->signal($signal);
    $h->finish;

    return $pid;
}

sub in_child {
    my $coderef = shift;
    my $pid = fork;
    return unless defined $pid;
    exit($coderef->()) unless $pid;
    waitpid($pid, 0);
    return ($? >> 8);
}

sub run_repo_in_child {
    my ($root_path) = @_;

    my $pid_file = 'server.pid';
    in_child(sub {
        our $repo =
            Test::SVN::Repo->new( root_path  => $root_path,
                                  keep_files => 1,
                                  users      => \%users );

        # Write the server pid to a file in tempdir
        $repo->root_path->file($pid_file)->spew($repo->server_pid);
        return 0;
    });

    # And read the server pid back in again
    my $server_pid = Path::Class::File->new($root_path, $pid_file)->slurp;
    return $server_pid;
}