Net-LibSSH
view release on metacpan or search on metacpan
t/06-exit-status-ordering.t view on Meta::CPAN
use strict;
use warnings;
use lib 't/lib';
use Test::More;
use TestSSHD;
use Net::LibSSH;
# The POD for exit_status() says: "Call this after reading all output;
# returns -1 until the remote process has exited." That describes the
# safe order of operations, but does not say what happens if a caller
# calls it first. This test pins down what actually happens today rather
# than assuming "-1", per the ticket's instruction to document real
# behaviour instead of encoding an assumption.
#
# Finding: libssh's ssh_channel_get_exit_status() pumps the session's
# packet loop internally while it waits for the exit-status message, so
# on the libssh build under test, reading exit_status() BEFORE any read()
# still (a) returns the correct exit code, not -1, and (b) does not
# discard the undrained output -- it stays buffered inside libssh and a
# later read() still returns it in full. Verified here for a small
# command and for 300000 bytes of output. Every assertion here is guarded
# by an alarm() so that if a different libssh build blocks instead
# (rather than pumping the loop), this file fails fast with a diagnostic
# instead of hanging the suite.
my $srv = TestSSHD->start;
plan skip_all => 'sshd or ssh-keygen not available' unless $srv;
my $ssh = Net::LibSSH->new;
$ssh->option(host => $srv->host);
$ssh->option(port => $srv->port);
$ssh->option(user => scalar getpwuid($<));
$ssh->option(knownhosts => '/dev/null');
$ssh->connect
or plan skip_all => 'connect failed: ' . ($ssh->error // '');
$ssh->auth_publickey($srv->client_key)
or plan skip_all => 'auth failed: ' . ($ssh->error // '');
sub with_timeout {
my ($seconds, $code) = @_;
my $result;
local $@;
eval {
local $SIG{ALRM} = sub { die "TIMEOUT\n" };
alarm($seconds);
$result = $code->();
alarm(0);
};
my $err = $@;
alarm(0);
return ($result, $err);
}
# small output, non-zero exit, exit_status() read before ANY read() of stdout
{
my $ch = $ssh->channel;
ok $ch->exec('echo some_output_before_exit; exit 5'), 'exec() succeeds';
my ($rc, $err) = with_timeout(10, sub { $ch->exit_status });
ok !$err, 'exit_status() called before any read() returns within 10s'
or diag "exit_status() before drain timed out or died: $err";
SKIP: {
skip 'exit_status() before drain did not return -- see diagnostic above', 2 if $err;
is $rc, 5, 'exit_status() returns the correct code even though stdout was never drained';
my ($out, $rerr) = with_timeout(10, sub { $ch->read });
ok !$rerr, 'the undrained output can still be read afterward';
is $out, "some_output_before_exit\n",
'exit_status() read first did not discard the undrained output';
}
$ch->close;
}
# moderate output (300000 bytes of known content), exit_status() before ANY read
{
my $ch = $ssh->channel;
ok $ch->exec('yes ABCDEFGHIJ | head -c 300000'), 'exec() succeeds';
my ($rc, $err) = with_timeout(15, sub { $ch->exit_status });
ok !$err, 'exit_status() called before any read() returns within 15s, even with 300000 undrained bytes waiting'
or diag "exit_status() before drain timed out or died: $err";
SKIP: {
skip 'exit_status() before drain did not return -- see diagnostic above', 2 if $err;
is $rc, 0, 'exit_status() resolves correctly with 300000 bytes of undrained stdout waiting';
my ($data, $rerr) = with_timeout(15, sub {
my $buf = '';
while (1) {
my $chunk = $ch->read(65536);
( run in 0.403 second using v1.01-cache-2.11-cpan-7f9471e7e0a )