diff options
Diffstat (limited to 't/httpd-corner.t')
-rw-r--r-- | t/httpd-corner.t | 121 |
1 files changed, 72 insertions, 49 deletions
diff --git a/t/httpd-corner.t b/t/httpd-corner.t index 0a613a9e..7539573c 100644 --- a/t/httpd-corner.t +++ b/t/httpd-corner.t @@ -1,12 +1,14 @@ -# Copyright (C) 2016-2021 all contributors <meta@public-inbox.org> +#!perl -w +# Copyright (C) all contributors <meta@public-inbox.org> # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt> # note: our HTTP server should be standalone and capable of running # generic PSGI/Plack apps. -use strict; use v5.10.1; use PublicInbox::TestCommon; +use v5.12; use PublicInbox::TestCommon; use Time::HiRes qw(gettimeofday tv_interval); +use autodie qw(getsockopt setsockopt); use PublicInbox::Spawn qw(spawn popen_rd); require_mods(qw(Plack::Util Plack::Builder HTTP::Date HTTP::Status)); -use Digest::SHA qw(sha1_hex); +use PublicInbox::SHA qw(sha1_hex); use IO::Handle (); use IO::Socket::UNIX; use Fcntl qw(:seek); @@ -19,26 +21,27 @@ ok(defined mkfifo($fifo, 0777), 'created FIFO'); my $err = "$tmpdir/stderr.log"; my $out = "$tmpdir/stdout.log"; my $psgi = "./t/httpd-corner.psgi"; -my $sock = tcp_server() or die; +my $sock = tcp_server(); my @zmods = qw(PublicInbox::GzipFilter IO::Uncompress::Gunzip); # Make sure we don't clobber socket options set by systemd or similar # using socket activation: my ($defer_accept_val, $accf_arg, $TCP_DEFER_ACCEPT); -if ($^O eq 'linux') { +SKIP: { + skip 'TCP_DEFER_ACCEPT is Linux-only', 1 if $^O ne 'linux'; $TCP_DEFER_ACCEPT = eval { Socket::TCP_DEFER_ACCEPT() } // 9; - setsockopt($sock, IPPROTO_TCP, $TCP_DEFER_ACCEPT, 5) or die; + setsockopt($sock, IPPROTO_TCP, $TCP_DEFER_ACCEPT, 5); my $x = getsockopt($sock, IPPROTO_TCP, $TCP_DEFER_ACCEPT); - defined $x or die "getsockopt: $!"; $defer_accept_val = unpack('i', $x); - if ($defer_accept_val <= 0) { - die "unexpected TCP_DEFER_ACCEPT value: $defer_accept_val"; - } -} elsif ($^O eq 'freebsd' && system('kldstat -m accf_data >/dev/null') == 0) { + ok($defer_accept_val > 0, 'TCP_DEFER_ACCEPT val non-zero') or + xbail "unexpected TCP_DEFER_ACCEPT value: $defer_accept_val"; +} +SKIP: { + require_mods '+accf_data'; require PublicInbox::Daemon; my $var = $PublicInbox::Daemon::SO_ACCEPTFILTER; $accf_arg = pack('a16a240', 'dataready', ''); - setsockopt($sock, SOL_SOCKET, $var, $accf_arg) or die "setsockopt: $!"; + setsockopt($sock, SOL_SOCKET, $var, $accf_arg); } sub unix_server ($) { @@ -53,14 +56,40 @@ sub unix_server ($) { my $upath = "$tmpdir/s"; my $unix = unix_server($upath); +my $alt = tcp_server(); my $td; my $spawn_httpd = sub { my (@args) = @_; - my $cmd = [ '-httpd', @args, "--stdout=$out", "--stderr=$err", $psgi ]; - $td = start_script($cmd, undef, { 3 => $sock, 4 => $unix }); + my $x = tcp_host_port($alt); + my $cmd = [ '-httpd', @args, "--stdout=$out", "--stderr=$err", $psgi, + '-l', "http://$x/?psgi=t/alt.psgi,env.PI_CONFIG=/path/to/alt". + ",err=$tmpdir/alt.err" ]; + my $env = { PI_CONFIG => '/dev/null' }; + $td = start_script($cmd, $env, { 3 => $sock, 4 => $unix, 5 => $alt }); }; $spawn_httpd->(); +{ + my $conn = conn_for($alt, 'alt PSGI path'); + $conn->write("GET / HTTP/1.0\r\n\r\n"); + $conn->read(my $buf, 4096); + like($buf, qr!^/path/to/alt\z!sm, + 'alt.psgi loaded on alt socket with correct env'); + + $conn = conn_for($sock, 'default PSGI path'); + $conn->write("GET /PI_CONFIG HTTP/1.0\r\n\r\n"); + $conn->read($buf, 4096); + like($buf, qr!^/dev/null\z!sm, + 'default PSGI on original socket'); + my $log = capture("$tmpdir/alt.err"); + ok(grep(/ALT/, @$log), 'alt psgi.errors written to'); + $log = capture($err); + ok(!grep(/ALT/, @$log), 'STDERR not written to'); + is(unlink($err, "$tmpdir/alt.err"), 2, 'unlinked stderr and alt.err'); + + $td->kill('USR1'); # trigger reopen_logs +} + if ('test worker death') { my $conn = conn_for($sock, 'killed worker'); $conn->write("GET /pid HTTP/1.1\r\nHost:example.com\r\n\r\n"); @@ -82,6 +111,10 @@ if ('test worker death') { like($body, qr/\A[0-9]+\z/, '/pid response'); isnt($body, $pid, 'respawned worker'); } +{ # check on prior USR1 signal + ok(-e $err, 'stderr recreated after USR1'); + ok(-e "$tmpdir/alt.err", 'alt.err recreated after USR1'); +} { my $conn = conn_for($sock, 'Header spaces bogus'); $conn->write("GET /empty HTTP/1.1\r\nSpaced-Out : 3\r\n\r\n"); @@ -289,7 +322,7 @@ sub conn_for { $spawn_httpd->('-W0'); } -sub delay { select(undef, undef, undef, shift || rand(0.02)) } +sub delay { tick(shift || rand(0.02)) } my $str = 'abcdefghijklmnopqrstuvwxyz'; my $len = length $str; @@ -310,7 +343,7 @@ SKIP: { my $url = "$base/sha1"; my ($r, $w); pipe($r, $w) or die "pipe: $!"; - my $cmd = [$curl, qw(--tcp-nodelay -T- -HExpect: -sSN), $url]; + my $cmd = [$curl, qw(--tcp-nodelay -T- -HExpect: -gsSN), $url]; open my $cout, '+>', undef or die; open my $cerr, '>', undef or die; my $rdr = { 0 => $r, 1 => $cout, 2 => $cerr }; @@ -327,7 +360,7 @@ SKIP: { seek($cout, 0, SEEK_SET); is(<$cout>, sha1_hex($str), 'read expected body'); - my $fh = popen_rd([$curl, '-sS', "$base/async-big"]); + my $fh = popen_rd([$curl, '-gsS', "$base/async-big"]); my $n = 0; my $non_zero = 0; while (1) { @@ -335,19 +368,19 @@ SKIP: { $n += $r; $buf =~ /\A\0+\z/ or $non_zero++; } - close $fh or die "close curl pipe: $!"; + $fh->close or die "close curl pipe: $!"; is($?, 0, 'curl succesful'); is($n, 30 * 1024 * 1024, 'got expected output from curl'); is($non_zero, 0, 'read all zeros'); require_mods(@zmods, 4); - my $buf = xqx([$curl, '-sS', "$base/psgi-return-gzip"]); + my $buf = xqx([$curl, '-gsS', "$base/psgi-yield-gzip"]); is($?, 0, 'curl succesful'); IO::Uncompress::Gunzip::gunzip(\$buf => \(my $out)); is($out, "hello world\n"); my $curl_rdr = { 2 => \(my $curl_err = '') }; - $buf = xqx([$curl, qw(-sSv --compressed), - "$base/psgi-return-compressible"], undef, $curl_rdr); + $buf = xqx([$curl, qw(-gsSv --compressed), + "$base/psgi-yield-compressible"], undef, $curl_rdr); is($?, 0, 'curl --compressed successful'); is($buf, "goodbye world\n", 'gzipped response as expected'); like($curl_err, qr/\bContent-Encoding: gzip\b/, @@ -355,8 +388,8 @@ SKIP: { } { - my $conn = conn_for($sock, 'psgi_return ENOENT'); - print $conn "GET /psgi-return-enoent HTTP/1.1\r\n\r\n" or die; + my $conn = conn_for($sock, 'psgi_yield ENOENT'); + print $conn "GET /psgi-yield-enoent HTTP/1.1\r\n\r\n" or die; my $buf = ''; sysread($conn, $buf, 16384, length($buf)) until $buf =~ /\r\n\r\n/; like($buf, qr!HTTP/1\.[01] 500\b!, 'got 500 error on ENOENT'); @@ -594,43 +627,33 @@ SKIP: { SKIP: { skip 'TCP_DEFER_ACCEPT is Linux-only', 1 if $^O ne 'linux'; my $var = $TCP_DEFER_ACCEPT; - defined(my $x = getsockopt($sock, IPPROTO_TCP, $var)) or die; + my $x = getsockopt($sock, IPPROTO_TCP, $var); is(unpack('i', $x), $defer_accept_val, 'TCP_DEFER_ACCEPT unchanged if previously set'); }; SKIP: { - skip 'SO_ACCEPTFILTER is FreeBSD-only', 1 if $^O ne 'freebsd'; - skip 'accf_data not loaded: kldload accf_data' if !defined $accf_arg; + require_mods '+accf_data'; my $var = $PublicInbox::Daemon::SO_ACCEPTFILTER; - defined(my $x = getsockopt($sock, SOL_SOCKET, $var)) or die; + my $x = getsockopt($sock, SOL_SOCKET, $var); is($x, $accf_arg, 'SO_ACCEPTFILTER unchanged if previously set'); }; SKIP: { - skip 'only testing lsof(8) output on Linux', 1 if $^O ne 'linux'; - my $lsof = require_cmd('lsof', 1) or skip 'no lsof in PATH', 1; - my $null_in = ''; - my $rdr = { 2 => \(my $null_err), 0 => \$null_in }; - my @lsof = xqx([$lsof, '-p', $td->{pid}], undef, $rdr); - my $d = [ grep(/\(deleted\)/, @lsof) ]; - is_deeply($d, [], 'no lingering deleted inputs') or diag explain($d); + skip 'only testing /proc/PID/fd on Linux', 1 if $^O ne 'linux'; + my $fd_dir = "/proc/$td->{pid}/fd"; + -d $fd_dir or skip '/proc/$PID/fd missing', 1; + my @child = grep defined, map readlink, glob "$fd_dir/*"; + my @d = grep /\(deleted\)/, @child; + is_deeply(\@d, [], 'no lingering deleted inputs') or diag explain(\@d); # filter out pipes inherited from the parent - my @this = xqx([$lsof, '-p', $$], undef, $rdr); - my $bad; - my $extract_inodes = sub { - map {; - my @f = split(' ', $_); - my $inode = $f[-2]; - $bad = $_ if $inode !~ /\A[0-9]+\z/; - $inode => 1; - } grep (/\bpipe\b/, @_); - }; - my %child = $extract_inodes->(@lsof); + my @this = grep defined, map readlink, glob "/proc/$$/fd/*"; + my $extract_inodes = sub { map { $_ => 1 } grep /\bpipe\b/, @_ }; + my %child = $extract_inodes->(@child); my %parent = $extract_inodes->(@this); - skip("inode not in expected format: $bad", 1) if defined($bad); delete @child{(keys %parent)}; - is_deeply([], [keys %child], 'no extra pipes with -W0'); + is_deeply([], [keys %child], 'no extra pipes with -W0') or + diag explain([child => \%child, parent => \%parent]); }; # ensure compatibility with other PSGI servers @@ -646,13 +669,13 @@ SKIP: { my $app = require $psgi; test_psgi($app, sub { my ($cb) = @_; - my $req = GET('http://example.com/psgi-return-gzip'); + my $req = GET('http://example.com/psgi-yield-gzip'); my $res = $cb->($req); my $buf = $res->content; IO::Uncompress::Gunzip::gunzip(\$buf => \(my $out)); is($out, "hello world\n", 'got expected output'); - $req = GET('http://example.com/psgi-return-enoent'); + $req = GET('http://example.com/psgi-yield-enoent'); $res = $cb->($req); is($res->code, 500, 'got error on ENOENT'); seek($tmperr, 0, SEEK_SET) or die; |