API-Docker
view release on metacpan or search on metacpan
t/stream_error.t view on Meta::CPAN
is $@, '', 'and neither does one that literally carries errorDetail';
is scalar @$events, 2, 'the event is handed over as data';
};
# ---------------------------------------------------------------------------
subtest 'the stringification contract' => sub {
my $t = transport(load_fixture_raw('images_build_error_stream.ndjson'));
eval { $t->images->build(context => 'tar-bytes') };
my $err = $@;
# Everything else in this distribution croaks strings and consumers rely on
# it -- ../p5-dist-zilla-plugin-docker-api strips Carp's location tail off
# $@ with a substitution. All of this has to keep working unchanged.
like "$err", qr/ at \S+ line \d+\.?/,
'carries Carp\'s location suffix, exactly as the plain croak it replaces';
like $err, qr/exit status 7/, 'matches a regex without an explicit stringify';
ok $err, 'boolean-true, so if ($@) still detects it';
is $err . '', "$err", 'concatenation goes through the overload';
is sprintf('%s', $err), "$err", 'so does sprintf %s';
ok $err eq "$err", 'and string comparison';
is $err->message . $err->location, "$err",
'message and location are separable, and together they are the string';
unlike $err->message, qr/ at \S+ line \d+/,
'message on its own is the bare reason, no location';
# The consumer's exact treatment, from Dist::Zilla::Plugin::Docker::API.
my $copy = $err;
$copy =~ s/\s+at\s+\S+\s+line\s+\d+\.?//g;
$copy =~ s/\s+/ /g;
$copy =~ s/^\s+|\s+$//g;
is ref \$copy, 'SCALAR', 's/// on the exception yields a plain string';
like $copy, qr/exit status 7\z/,
'the location is stripped and the reason survives';
isa_ok $err, 'API::Docker::Error::Stream',
'the original is untouched by the copy\'s substitution';
};
subtest 'a message that ends in a newline still gets a location' => sub {
# Engine messages end in "\n" and Carp appends no location to a message
# that already does, so the trailing whitespace has to come off first.
my $body = encode_json({ errorDetail => { message => "boom\n" } }) . "\n";
eval { transport($body)->_request('POST', '/build', ndjson => 1) };
like "$@", qr/boom at \S+ line \d+\./,
'the trailing newline is trimmed and the suffix lands';
};
subtest 'errorDetail without a usable message falls back' => sub {
my $body = encode_json({ errorDetail => {}, error => 'flat error text' }) . "\n";
eval { transport($body)->_request('POST', '/build', ndjson => 1) };
like "$@", qr/flat error text/,
'the flat error key is the fallback when errorDetail has no message';
$body = encode_json({ errorDetail => {} }) . "\n";
eval { transport($body)->_request('POST', '/build', ndjson => 1) };
like "$@", qr/no message given/, 'and there is a last resort';
isa_ok $@, 'API::Docker::Error::Stream', 'still the exception class';
};
subtest 'the query string stays out of the message' => sub {
# /build carries buildargs in the query string, which can hold credentials.
my $body = encode_json({ errorDetail => { message => 'nope' } }) . "\n";
eval {
transport($body)->_request('POST', '/build',
ndjson => 1,
params => { t => 'app:v1', buildargs => { NPM_TOKEN => 'sekrit' } },
);
};
like "$@", qr{\QPOST /v1.41/build\E}, 'the endpoint is named';
unlike "$@", qr/sekrit/, 'the build args are not';
};
# ---------------------------------------------------------------------------
SKIP: {
skip 'mock routes are bypassed in live mode', 1 if is_live();
subtest 'which endpoints opt out of the check' => sub {
my %saw;
my $docker = test_docker(
'POST /build' => sub { $saw{build} = { @_[2 .. $#_] }; [] },
'POST /images/create' => sub { $saw{pull} = { @_[2 .. $#_] }; [] },
'POST /images/nginx/push' => sub { $saw{push} = { @_[2 .. $#_] }; [] },
'GET /events' => sub { $saw{events} = { @_[2 .. $#_] }; [] },
);
$docker->images->build(context => 'tar-bytes', t => 'x:1');
$docker->images->pull(fromImage => 'nginx');
$docker->images->push('nginx');
$docker->system->events(since => 1, until => 2);
# The check defaults on, so an operation endpoint says nothing at all --
# a new streaming endpoint gets the loud behaviour without being told.
ok !exists $saw{$_}{croak_on_error}, "$_ leaves the check at its default"
for qw( build pull push );
is $saw{events}{croak_on_error}, 0, 'events is the one endpoint opting out';
};
}
SKIP: {
skip 'live write tests disabled (API_DOCKER_TEST_WRITE=1 to enable)', 1
unless can_write();
subtest 'live: a build that fails croaks against a real engine' => sub {
# Measured, not assumed: the engine answers 200 and reports the failure
# inside the stream, and the build never commits an image. But rm=1 (the
# default) only removes the intermediate container on a *successful*
# build -- a failing RUN step leaves it behind, under an engine-chosen
# name (Docker: priceless_driscoll, ...) or, on Podman, as a buildah
# "working container" that GET /containers/json never lists at all
# (measured against 5.8.4). karr k63: a day of live runs stranded 14 such
# containers, on both engines, invisible to register_cleanup because none
# of them are named by the test -- the daemon creates them, not us.
#
# forcerm=1 is the engine's own answer to exactly this case ("always
# remove intermediate containers, even upon failure"). The build stream
# was checked first, as the ticket suggested: Docker's classic builder
# does put the id in a "Running in <id>" stream line, but Podman's
# buildah backend never mentions an id anywhere in the stream or the
# response headers, so scraping the stream cannot be the general fix.
# forcerm=1 was then measured directly (raw curl probes against both
# sockets, karr k63) to close the leak on both: Docker's stream gains a
( run in 0.914 second using v1.01-cache-2.11-cpan-85d3896f969 )