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 )