App-karr
view release on metacpan or search on metacpan
t/74-cli-transport-robustness.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use lib 't/lib';
use TestGit qw( require_git_c );
require_git_c();
use File::Temp qw( tempdir );
use Path::Tiny qw( path );
use App::karr::Git;
use App::karr::Task;
use Git::Native::Remote;
# Regression tests for the git-CLI transport fallback (#42, #43).
#
# #42 _cli_transport read `$? >> 8` only. That is 0 for a child killed by a
# signal just as it is for a clean exit, so a git the OOM killer, a
# Ctrl-C on the process group or a SIGPIPE took down was reported as a
# successful transport: "Created task 1", exit 0, empty remote.
# #43 it slurped stdout to EOF before touching stderr. Past 64 KiB of stderr
# the child blocks on a full pipe, so it never exits and never closes
# stdout, while the parent is still blocked reading stdout -- a deadlock
# with no timeout anywhere. A diverged board reaches that at ~700
# rejected refs, and it can strike inside bin/karr's END flush, after
# the command has already printed its result.
#
# Both are driven with a fake `git` on PATH, so they are deterministic and do
# not depend on board size or network conditions.
# Write an executable fake `git` (a Perl script, for portable signal control)
# into its own directory and return that directory.
sub fake_git {
my ($body) = @_;
my $bin = tempdir( CLEANUP => 1 );
my $exe = path( $bin, 'git' );
$exe->spew_utf8("#!$^X\nuse strict;\nuse warnings;\n$body");
chmod 0755, "$exe" or die "chmod: $!";
return $bin;
}
sub repo_with_remote {
my $work = tempdir( CLEANUP => 1 );
system( 'git', 'init', '-q', '--bare', "$work/origin.git" );
system("git clone -q '$work/origin.git' '$work/a' 2>/dev/null");
system( 'git', '-C', "$work/a", 'config', 'user.email', 'a@karr.test' );
system( 'git', '-C', "$work/a", 'config', 'user.name', 'agent-a' );
return ( $work, App::karr::Git->new( dir => "$work/a" ) );
}
# Run $code with a hang-breaker. Returns ( $result, $hung ); $hung stays true
# even when the alarm's die is swallowed further down the stack.
sub without_hanging {
my ( $seconds, $code ) = @_;
my $hung = 0;
my $result;
eval {
local $SIG{ALRM} = sub { $hung = 1; die "transport hung\n" };
alarm $seconds;
$result = $code->();
alarm 0;
1;
} or alarm 0;
return ( $result, $hung );
}
subtest '#42: git killed by a signal is a failed transport, not a successful one' => sub {
my ( $work, $git ) = repo_with_remote();
$git->save_task_ref(
App::karr::Task->new(
id => 1, title => 'Must not be lost silently',
status => 'todo', priority => 'high', class => 'standard',
)
);
my $bin = fake_git( <<'FAKE' );
$SIG{TERM} = 'DEFAULT';
kill 'TERM', $$; # die from a signal: exit status 0, signal bits 15
sleep 30;
FAKE
my $rv = do {
local $ENV{PATH} = "$bin:$ENV{PATH}";
$git->_cli_transport( 'push', 'origin', ['+refs/karr/*:refs/karr/*'] );
};
ok !$rv, '_cli_transport reports failure when git dies from a signal';
like $git->last_error, qr/killed by signal 15/,
'last_error names the signal';
# The end-to-end claim of the ticket: push() must not answer "yes" while
# the remote never received anything.
my $pushed = do {
no warnings 'redefine';
local *Git::Native::Remote::push =
sub { die "forced libgit2 failure\n" };
local $ENV{PATH} = "$bin:$ENV{PATH}";
$git->push;
( run in 0.663 second using v1.01-cache-2.11-cpan-6736b670a1e )