API-Docker
view release on metacpan or search on metacpan
t/stream_frames.t view on Meta::CPAN
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use FindBin;
use lib "$FindBin::Bin/lib";
use Test::API::Docker::Mock;
use JSON::MaybeXS;
use API::Docker;
# Regression coverage for karr k7: containers->logs and exec->start used to
# hand the caller the raw Docker stream, so the 8-byte frame headers ended up
# inside the log text. Every fixture below is bytes captured from the rootless
# Podman socket (5.4.2, API 1.41) with a container running
# "echo OUT; echo ERR 1>&2".
check_live_access();
# The demultiplexer is a method on the transport role; the client needs no
# connection to exercise it, the socket attribute is lazy.
my $client = API::Docker->new(
host => 'unix:///var/run/docker.sock',
api_version => '1.41',
);
my $MULTIPLEXED = load_fixture_raw('containers_logs_multiplexed.bin');
my $TTY = load_fixture_raw('containers_logs_tty.bin');
my $TTY_JSON = load_fixture_raw('containers_logs_tty_json.bin');
my $EXEC = load_fixture_raw('exec_start_multiplexed.bin');
subtest 'the captured fixtures are the bytes the engine produced' => sub {
is length $MULTIPLEXED, 24, 'multiplexed log fixture is 24 bytes';
is unpack('H*', $MULTIPLEXED),
'0100000000000004' . '4f55540a' . '0200000000000004' . '4552520a',
'multiplexed log fixture: 01 header + "OUT\n", 02 header + "ERR\n"';
is $TTY, "OUT\r\nERR\r\n", 'tty log fixture has no headers and CRLF endings';
is $EXEC, $MULTIPLEXED, 'exec/start frames are byte-identical to logs frames';
};
subtest 'demultiplexing a framed stream' => sub {
my $frames = $client->_demux_frames($MULTIPLEXED);
is_deeply $frames, [
{ stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" },
], 'two frames, stdout then stderr, headers stripped';
is join('', map { $_->{data} } @$frames), "OUT\nERR\n",
'joining the payloads gives the plain text';
};
subtest 'raw (TTY) output is not mistaken for frames' => sub {
is $client->_demux_frames($TTY), undef,
'text pty output does not walk as frames';
is $client->_demux_frames($TTY_JSON), undef,
'JSON pty output does not walk as frames';
};
subtest 'the framing walk rejects what it cannot consume exactly' => sub {
is $client->_demux_frames(''), undef, 'empty body';
is $client->_demux_frames("\x01\x00\x00\x00"), undef, 'short header';
is $client->_demux_frames("\x01\x00\x00\x00\x00\x00\x00\x04OU"), undef,
'truncated payload';
is $client->_demux_frames("\x01\x00\x00\x00\x00\x00\x00\x04OUT\nX"), undef,
'trailing byte after a complete frame';
is $client->_demux_frames("\x03\x00\x00\x00\x00\x00\x00\x01A"), undef,
'stream type above 2';
is $client->_demux_frames("\x01\x00\x00\x01\x00\x00\x00\x01A"), undef,
'non-zero padding byte';
is_deeply $client->_demux_frames("\x00\x00\x00\x00\x00\x00\x00\x01A"),
[ { stream => 'stdin', data => 'A' } ], 'stream type 0 is stdin';
};
SKIP: {
skip 'mock routes are bypassed in live mode', 3 if is_live();
subtest 'containers->logs demultiplexes' => sub {
my $docker = test_docker(
'GET /containers/deadbeef/logs' => sub { $MULTIPLEXED },
);
my $frames = $docker->containers->logs('deadbeef');
is_deeply $frames, [
{ stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" },
], 'frames, not header bytes';
};
subtest 'containers->logs on a TTY container' => sub {
my $docker = test_docker(
'GET /containers/deadbeef/logs' => sub { $TTY },
);
my $frames = $docker->containers->logs('deadbeef');
is_deeply $frames, [ { stream => 'raw', data => "OUT\r\nERR\r\n" } ],
'one raw frame; stream is a plain string, never undef';
ok defined $frames->[0]{stream}, 'no caller needs a defined-check';
my $json = test_docker(
'GET /containers/deadbeef/logs' => sub { $TTY_JSON },
);
is_deeply $json->containers->logs('deadbeef'),
[ { stream => 'raw', data => qq[{"msg":"hi"}\r\n] } ],
'a container printing JSON is returned verbatim, not decoded';
my $forced = test_docker(
'GET /containers/deadbeef/logs' => sub { $MULTIPLEXED },
);
is_deeply $forced->containers->logs('deadbeef', tty => 1),
[ { stream => 'raw', data => $MULTIPLEXED } ],
'tty => 1 suppresses demultiplexing';
};
subtest 'exec->start demultiplexes' => sub {
my $docker = test_docker(
'POST /exec/abc123/start' => sub { $EXEC },
);
is_deeply $docker->exec->start('abc123', Detach => 0), [
{ stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" },
], 'same frame shape as logs';
my $empty = test_docker(
'POST /exec/abc123/start' => sub { undef },
);
is_deeply $empty->exec->start('abc123', Detach => 1), [],
'a detached start produces no frames';
};
}
SKIP: {
skip 'live write tests disabled (API_DOCKER_TEST_WRITE=1 to enable)', 1
unless can_write();
subtest 'live: frames off a real engine' => sub {
my $docker = API::Docker->new(host => $ENV{API_DOCKER_TEST_HOST});
my $name = 'apidocker-t-frames-' . $$;
my $created = $docker->containers->create(
Image => 'alpine:3',
Cmd => [ 'sh', '-c', 'echo OUT; echo ERR 1>&2' ],
Tty => JSON->false,
name => $name,
);
my $id = $created->{Id};
register_cleanup(sub {
eval { $docker->containers->remove($id, force => 1) };
});
$docker->containers->start($id);
$docker->containers->wait($id);
my $frames = $docker->containers->logs($id);
is ref $frames, 'ARRAY', 'logs returns an ArrayRef';
ok scalar(@$frames) >= 1, 'at least one frame';
my %seen = map { $_->{stream} => 1 } @$frames;
ok $seen{stdout}, 'stdout frame present';
ok $seen{stderr}, 'stderr frame present';
like join('', map { $_->{data} } @$frames), qr/OUT/,
'payload carries the text';
unlike join('', map { $_->{data} } @$frames), qr/\x00\x00\x00/,
'no frame header bytes leaked into the payload';
};
}
done_testing;
( run in 1.182 second using v1.01-cache-2.11-cpan-54e63673c56 )