App-karr
view release on metacpan or search on metacpan
t/122-foundation-runner-errors.t view on Meta::CPAN
use strict;
use warnings;
# The fork override has to be in place before App::karr::Foundation::Runner is
# compiled, because a CORE::GLOBAL replacement only binds ops compiled after it.
# That is also why this lives in its own file: fork is unusable for the rest of
# the process afterwards, and the other error-message tests drive bin/karr
# through IPC::Open3.
our $FORK_FAILS; # a package variable so the subtest below can localize it
our $LAST_CHILD_PID; # ditto: how the parent below finds the real child to reap
BEGIN {
*CORE::GLOBAL::fork = sub {
return undef if $FORK_FAILS;
my $pid = CORE::fork();
$LAST_CHILD_PID = $pid if $pid; # true only in the parent; child sees 0
return $pid;
};
}
use Test::More;
use File::Temp qw( tempdir );
use Path::Tiny qw( path );
use App::karr::Foundation::Runner;
# Ticket #77. App::karr::Foundation::Runner reported the three ways starting an
# agent command can fail at the OS level with croak, so each one handed the
# operator a line and a file inside karr:
#
# pipe failed: Too many open files at /.../lib/App/karr/Foundation/Runner.pm line 67.
# fork failed: Resource temporarily unavailable at /.../Runner.pm line 70.
# open log: Permission denied at /.../Runner.pm line 83.
#
# "The kernel refused you a process" is an operator's problem, not a bug report,
# so the errno has to survive and the call site has to go.
#
# The pipe case is the one not exercised here: making pipe(2) fail means
# exhausting the descriptor table, and the soft limit on a normal box is a
# million. It shares its single line of code shape with the two below.
{
# Stands in for App::karr::Foundation. The Runner holds it weakly, so the
# caller has to keep it alive -- $foundation below is not a spare variable.
package FakeFoundation;
sub new { bless {}, shift }
sub _stream_to_terminal { 0 }
sub _prompt_for { '' }
sub _append_log { }
sub _say_verbose { }
sub dry_run { 0 }
}
my $foundation = FakeFoundation->new;
my $runner = App::karr::Foundation::Runner->new( foundation => $foundation );
subtest 'a refused fork is reported without a karr source location' => sub {
my $repo = path( tempdir( CLEANUP => 1 ) );
local $FORK_FAILS = 1;
eval { $runner->_run_command( $repo, { command => 'true', max_runtime => 5 } ) };
my $err = $@;
ok $err, 'the run fails';
like $err, qr/^fork failed: /, 'and says which call refused';
unlike $err, qr/ at \S+ line \d+/, 'no "at FILE line N." suffix'
or diag "error was:\n$err";
unlike $err, qr/Runner\.pm/, 'no karr module path'
or diag "error was:\n$err";
is scalar( grep { length } split /\n/, $err ), 1, 'exactly one line'
or diag "error was:\n$err";
};
subtest 'a log file that cannot be opened is reported the same way' => sub {
my $repo = path( tempdir( CLEANUP => 1 ) );
# A directory where the log belongs: open '>>' fails with EISDIR whatever
# the caller's privileges are, so this holds for root too. In a real run the
# foundation's own _append_log reaches the same path first; the Runner still
# owes a clean message for the case where it does not.
$repo->child('.karr.log')->mkpath;
# Unlike the subtest above, this one does not set $FORK_FAILS -- it does not
# need to. Since ticket #147 the log is opened before the fork, so the
# failure below happens with no child in existence at all.
local $LAST_CHILD_PID;
eval { $runner->_run_command( $repo, { command => 'true', max_runtime => 5 } ) };
my $err = $@;
ok $err, 'the run fails';
like $err, qr/^open log /, 'and says it was the log';
like $err, qr/\Q@{[ $repo->child('.karr.log') ]}\E/,
'naming the file, which the croak never did';
unlike $err, qr/ at \S+ line \d+/, 'no "at FILE line N." suffix'
or diag "error was:\n$err";
unlike $err, qr/Runner\.pm/, 'no karr module path'
or diag "error was:\n$err";
# Ticket #143 made this subtest reap the child the pre-#147 Runner forked
# before it died here, because a child that failed before its exec left no
# trace but Test::Builder's "Forked inside subtest, but subtest never
# finished!" on the shared STDERR -- never a counted failure, so the file
# stayed green regardless. Ticket #147 moved the log open in front of the
# fork, so there is now nothing to reap, and that is what this pins: the
# error above is raised with no agent started. t/148 covers the leak itself.
( run in 1.914 second using v1.01-cache-2.11-cpan-14f38c9f855 )