EV-YACurl
view release on metacpan or search on metacpan
t/lib/TestServer.pm view on Meta::CPAN
use EV ();
use IO::Socket::INET;
use POSIX ();
my %REASON = (
200 => q(OK),
204 => q(No Content),
301 => q(Moved Permanently),
302 => q(Found),
400 => q(Bad Request),
404 => q(Not Found),
500 => q(Internal Server Error),
);
# A minimal HTTP/1.1 server for the test suite. The handler is called with a
# request hashref and returns ($status, \@headers, $body); returning nothing
# closes the connection without a reply.
sub new {
my ($class, $handler) = @_;
my $listener = IO::Socket::INET->new(
LocalAddr => '127.0.0.1',
Listen => 32,
ReuseAddr => 1,
) or die "TestServer: listen failed: $!";
my $port = $listener->sockport;
my $pid = fork;
die "TestServer: fork failed: $!" unless defined $pid;
if (!$pid) {
$SIG{PIPE} = 'IGNORE';
eval { setpgrp(0, 0) }; # so stop() can reach the per-connection children
eval { _serve($listener, $handler) };
POSIX::_exit(0);
}
close $listener;
return bless { pid => $pid, port => $port }, $class;
}
sub port { $_[0]{port} }
sub base_url { 'http://127.0.0.1:' . $_[0]{port} }
sub stop {
my ($self) = @_;
return unless $self->{pid};
kill 'KILL', -$self->{pid} or kill 'KILL', $self->{pid};
waitpid $self->{pid}, 0;
$self->{pid} = undef;
}
sub DESTROY { local $?; $_[0]->stop }
my $watchdog;
# Turns a wedged test into a failure instead of a hang. It has to be an EV
# watcher, since $SIG{ALRM} is only dispatched between Perl ops and so never
# arrives while blocked in EV::run, and it has to exit rather than die, since EV
# catches exceptions from a watcher callback and carries on. keepalive(0) stops
# it holding the loop open. A wedge that never re-enters EV::run is not covered.
sub watchdog {
my ($seconds) = @_;
$seconds ||= 60;
$watchdog = EV::signal(ALRM => sub {
print STDERR "# watchdog: no progress after ${seconds}s\n";
exit 1;
});
$watchdog->keepalive(0);
alarm $seconds;
}
sub _serve {
my ($listener, $handler) = @_;
$SIG{CHLD} = 'IGNORE';
# One child per connection, so a keep-alive connection sitting idle cannot
# stall the accept loop for the next one.
while (my $conn = $listener->accept) {
my $pid = fork;
if (!defined $pid || $pid) {
close $conn;
next;
}
close $listener;
$conn->autoflush(1);
my $buffer = '';
while (my $request = _read_request($conn, \$buffer)) {
my ($status, $headers, $body) = $handler->($request);
last unless defined $status;
$body = '' unless defined $body;
$headers ||= [];
my $out = "HTTP/1.1 $status " . ($REASON{$status} || 'Status') . "\r\n"
. "Content-Length: " . length($body) . "\r\n";
my $typed = 0;
for (my $i = 0; $i < @$headers; $i += 2) {
$typed = 1 if lc $headers->[$i] eq 'content-type';
$out .= "$headers->[$i]: $headers->[$i + 1]\r\n";
}
$out .= "Content-Type: text/plain\r\n" unless $typed;
$out .= "\r\n$body";
last unless _write_all($conn, $out);
last if lc($request->{headers}{connection} || '') eq 'close';
}
close $conn;
POSIX::_exit(0);
}
}
sub _fill {
my ($conn, $buffer, $want) = @_;
while (length($$buffer) < $want) {
my $chunk;
my $got = sysread $conn, $chunk, 65536;
return 0 unless $got;
$$buffer .= $chunk;
}
return 1;
}
sub _read_line {
my ($conn, $buffer) = @_;
my $end;
while (($end = index $$buffer, "\r\n") < 0) {
my $chunk;
my $got = sysread $conn, $chunk, 65536;
return undef unless $got;
$$buffer .= $chunk;
}
my $line = substr $$buffer, 0, $end, '';
substr $$buffer, 0, 2, '';
return $line;
( run in 1.611 second using v1.01-cache-2.11-cpan-14f38c9f855 )