App-karr
view release on metacpan or search on metacpan
t/68-syncguard-end-flush.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 Cwd qw( abs_path getcwd );
use IPC::Open3 qw( open3 );
use Symbol qw( gensym );
use App::karr::Git;
use App::karr::SyncGuard;
use App::karr::Role::SyncLifecycle;
# Ticket #37: the SyncGuard insurance push never fired at a usable moment on
# the CLI.
#
# App::karr::Role::SyncLifecycle arms a guard for every writing command and
# stashes it on the command object, so a body that dies after writing refs but
# before sync_after should still push. It never did: bin/karr wraps the run in
# an eval and exits from the handler, MooX::Cmd's command chain keeps the
# command object alive, and the guard was therefore first reaped in global
# destruction -- where #34 (rightly) forbids all libgit2 work, leaving nothing
# but a "run karr sync" notice.
#
# The fix is a process-wide registry of armed guards drained by
# App::karr::SyncGuard->flush_armed, which bin/karr calls from an END block:
# the last point before global destruction, and one that also covers the exit()
# calls inside command bodies. The DESTRUCT branch of DESTROY stays as the last
# resort for embedders that never flush -- that is t/66's subject, not this
# file's.
#
# Observable end to end as: `karr move 1,999 in-progress --claim X` moves task
# 1 (writing refs/karr/tasks/1/data), then dies on the missing 999. Before the
# fix the remote kept the pre-move task; now it carries the write.
my $ROOT = abs_path('.');
my $BIN = "$ROOT/bin/karr";
sub _run_karr {
my ( $cwd, @argv ) = @_;
my $old = getcwd();
chdir $cwd or die "chdir $cwd: $!";
my $stderr = gensym;
my $pid = open3( my $in, my $out, $stderr, $^X, "-I$ROOT/lib", $BIN, @argv );
close $in;
my $stdout_text = do { local $/; <$out> };
my $stderr_text = do { local $/; <$stderr> };
waitpid( $pid, 0 );
my $exit = $? >> 8;
chdir $old or die "chdir $old: $!";
return {
exit => $exit,
stdout => defined $stdout_text ? $stdout_text : '',
stderr => defined $stderr_text ? $stderr_text : '',
};
}
# A throwaway work repo with a bare origin, both under CLEANUP. Never the
# repository this suite runs in.
sub _board_repo {
my ( $bare_out, @tasks ) = @_;
my $repo = tempdir( CLEANUP => 1 );
my $bare = tempdir( CLEANUP => 1 );
system( 'git', 'init', '-q', $repo ) == 0 or die 'git init failed';
system( 'git', 'init', '-q', '--bare', $bare ) == 0 or die 'git init --bare failed';
system( 'git', '-C', $repo, 'config', 'user.email', 'test@example.com' );
system( 'git', '-C', $repo, 'config', 'user.name', 'Test User' );
system( 'git', '-C', $repo, 'commit', '-q', '--allow-empty', '-m', 'init' ) == 0
or die 'git commit failed';
system( 'git', '-C', $repo, 'remote', 'add', 'origin', $bare ) == 0
or die 'git remote add failed';
# init first: a write command in a repository without a board is refused
# (#62), so `create` alone no longer conjures one.
is _run_karr( $repo, 'init', '--name', 'Flush Board' )->{exit}, 0,
'setup: karr init exits 0';
for my $title (@tasks) {
is _run_karr( $repo, 'create', $title )->{exit}, 0,
t/68-syncguard-end-flush.t view on Meta::CPAN
my $git = CountingGit->new;
my ( $stderr, $err ) = capture_stderr( sub {
my $board = FlushBoard->new( git => $git );
$board->sync_before;
$board->sync_after;
is( App::karr::SyncGuard->flush_armed, 0,
'sync_after deregistered the guard, so the flush finds nothing' );
} );
is( $err, undef, 'clean lifecycle' );
is( $git->pushes, 1, 'exactly one push across the whole lifecycle' );
};
subtest 'a sync_after that failed all 3 attempts disarms the guard' => sub {
local $App::karr::Git::WRITES = 1;
# sync_after has already spent the three attempts and croaked with the
# "run karr sync" guidance; re-running them from the flush would double the
# delay and the noise on a command that is already failing.
my $git = CountingGit->new(0);
my $croak;
my ( $stderr, $err ) = capture_stderr( sub {
my $board = FlushBoard->new( git => $git );
$board->sync_before;
$croak = eval { $board->sync_after; 1 } ? undef : $@;
is( App::karr::SyncGuard->flush_armed, 0,
'the spent guard is not flushed again' );
} );
like( $croak, qr/Push failed after 3 attempts/,
'sync_after still croaks -- disarming does not swallow the failure' );
is( $git->pushes, 3, 'three attempts total, not six' );
};
subtest 'flush_armed never dies, even on a guard whose git blows up' => sub {
local $App::karr::Git::WRITES = 1;
my $guard = App::karr::SyncGuard->new( git => ExplodingGit->new, quiet => 1 );
my ( $stderr, $err ) = capture_stderr(
sub { App::karr::SyncGuard->flush_armed } );
is( $err, undef, 'the exception is caught, not rethrown at the caller' );
like( $stderr, qr/libgit2 went sideways/, 'and it is warned about' );
};
subtest 'the registry holds no strong reference and leaks no keys' => sub {
is( armed_count(), 0, 'registry starts clean' );
my $git = CountingGit->new;
{
my $guard = App::karr::SyncGuard->new( git => $git, quiet => 1 );
is( armed_count(), 1, 'an armed guard registers itself' );
$guard->done;
is( armed_count(), 0, 'done() deregisters immediately' );
}
{
local $App::karr::Git::WRITES = 0;
my $guard = App::karr::SyncGuard->new( git => $git, quiet => 1 );
is( armed_count(), 1, 'a second guard registers' );
}
is( armed_count(), 0,
'scope exit frees the guard: the registry never kept it alive' );
is( scalar( keys %App::karr::SyncGuard::ARMED ), 0,
'and DESTROY removed the key too, so the registry does not grow' );
};
done_testing;
( run in 2.205 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )