API-Docker
view release on metacpan or search on metacpan
t/truncated_response.t view on Meta::CPAN
# exec/start, which is every raw-stream endpoint there is.
my $client = API::Docker->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
# The peer writes and then half-closes: we see the end of the response, and a
# request written afterwards still lands rather than raising SIGPIPE. Both
# ends are handed back so the caller can close them when it is done.
sub pair_with {
my ($bytes) = @_;
socketpair(my $ours, my $theirs, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
or plan skip_all => "socketpair unavailable: $!";
syswrite $theirs, $bytes;
shutdown $theirs, 1;
return ($ours, $theirs);
}
# Drive a reader over such a pair and report which of the two happened.
sub over_pair {
my ($bytes, $drive) = @_;
my ($ours, $theirs) = pair_with($bytes);
my @out = eval { $drive->($ours) };
my $err = $@;
close $ours;
close $theirs;
return ($err, \@out);
}
# A handle that hands over at most $step bytes per sysread, for the two
# assertions a socketpair cannot make. Everything written to a socketpair
# before it is read is already in the kernel's buffer, so one sysread takes
# the lot and a chunk is always complete by the time the callback sees any of
# it -- which is exactly the state the `stopped` guard is about not being in.
{
package Test::Truncated::Stepped;
sub TIEHANDLE {
my ($class, $data, $step) = @_;
return bless { data => $data, pos => 0, step => $step }, $class;
}
sub READ {
my $self = $_[0];
my $left = length($self->{data}) - $self->{pos};
return 0 unless $left;
my $n = $self->{step} < $left ? $self->{step} : $left;
$n = $_[2] if defined $_[2] && $_[2] < $n;
$_[1] = substr($self->{data}, $self->{pos}, $n);
$self->{pos} += $n;
return $n;
}
sub CLOSE { 1 }
}
sub stepped {
my ($data, $step) = @_;
no warnings 'once';
my $glob = \do { local *HANDLE };
tie *$glob, 'Test::Truncated::Stepped', $data, $step;
return $glob;
}
sub truncated_ok {
my ($err, %want) = @_;
isa_ok $err, 'API::Docker::Error::Truncated' or return;
is $err->phase, $want{phase}, 'phase: ' . $want{phase};
is $err->partial, $want{partial}, 'the bytes that did arrive';
is $err->expected, $want{expected}, 'the count the response announced';
is $err->received, $want{received}, 'and how many of them came';
like "$err", $want{message}, 'the message says what was cut short';
return;
}
# ---------------------------------------------------------------------------
# The four shapes, measured exactly as karr k64 measured them
# ---------------------------------------------------------------------------
subtest 'Content-Length announces 11, 9 bytes arrive' => sub {
my ($err) = over_pair(
"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nhello wor",
sub { $client->_read_response($_[0], 'GET', {}) });
truncated_ok $err,
phase => 'content-length',
partial => 'hello wor',
expected => 11,
received => 9,
message => qr/the body stopped 2 bytes short of the 11 it announced; 9 bytes arrived/;
};
subtest 'a chunk announces 11 (b), 5 bytes arrive' => sub {
my ($err) = over_pair(
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nhello",
sub { $client->_read_response($_[0], 'GET', {}) });
truncated_ok $err,
phase => 'chunk-data',
partial => 'hello',
expected => 11,
received => 5,
message => qr/a chunk stopped 6 bytes short of the 11 it announced/;
};
subtest 'a complete chunk with no terminating zero chunk' => sub {
my ($err) = over_pair(
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n",
sub { $client->_read_response($_[0], 'GET', {}) });
truncated_ok $err,
phase => 'chunk-header',
partial => 'hello',
expected => undef,
received => undef,
message => qr/no terminating zero chunk/;
};
subtest 'a chunk header cut in half' => sub {
my ($err) = over_pair(
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n1",
sub { $client->_read_response($_[0], 'GET', {}) });
( run in 0.603 second using v1.01-cache-2.11-cpan-4716db65a44 )