From: Eric Wong <bofh@yhbt.net>
To: unicorn-public@yhbt.net
Subject: [PATCH 0/5..5/5] more tests to Perl 5 for stability
Date: Mon, 6 May 2024 20:10:21 +0000 [thread overview]
Message-ID: <20240506201021.M727962@dcvr> (raw)
[-- Attachment #1: Type: text/plain, Size: 1394 bytes --]
It's far easier to maintain tests in a language that's been
"dead" for 20 years :P This is another step towards freeing us
up to make more internal changes, too; as well as avoiding slow
Ruby startup overhead. (Perl 5 startup is slow, too, but not
nearly as slow as Ruby)
Eric Wong (5):
GNUmakefile: build writes shebang-modified files
t/*.t: use write_file helper function
tests: port broken-app test to Perl 5
tests: move test/unit/test_request.rb to Perl 5
port test/unit/test_ccc.rb to Perl 5
GNUmakefile | 1 +
t/active-unix-socket.t | 13 +--
t/broken-app.ru | 13 ---
t/client_body_buffer_size.t | 5 +-
t/heartbeat-timeout.t | 4 +-
t/integration.ru | 11 +++
t/integration.t | 128 +++++++++++++++++++++++++--
t/lib.perl | 2 +-
t/reload-bad-config.t | 17 ++--
t/reopen-logs.t | 5 +-
t/t0009-broken-app.sh | 56 ------------
t/winch_ttin.t | 7 +-
t/working_directory.t | 16 +---
test/unit/test_ccc.rb | 92 -------------------
test/unit/test_request.rb | 170 ------------------------------------
15 files changed, 153 insertions(+), 387 deletions(-)
delete mode 100644 t/broken-app.ru
delete mode 100755 t/t0009-broken-app.sh
delete mode 100644 test/unit/test_ccc.rb
delete mode 100644 test/unit/test_request.rb
[-- Attachment #2: 0001-GNUmakefile-build-writes-shebang-modified-files.patch --]
[-- Type: text/x-diff, Size: 772 bytes --]
From 5e9dbfd071aa939677aaf3d269115fb88e606311 Mon Sep 17 00:00:00 2001
From: Eric Wong <bofh@yhbt.net>
Date: Sun, 5 May 2024 22:15:35 +0000
Subject: [PATCH 1/5] GNUmakefile: build writes shebang-modified files
This makes it easier to run individual integration tests via
prove(1) rather than all at once with gmake(1).
---
GNUmakefile | 1 +
1 file changed, 1 insertion(+)
diff --git a/GNUmakefile b/GNUmakefile
index 70e7e10..227842c 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -78,6 +78,7 @@ man1_bins := $(addsuffix .1, $(base_bins))
man1_paths := $(addprefix man/man1/, $(man1_bins))
tmp_bins = $(addprefix $(tmp_bin)/, unicorn unicorn_rails)
pid := $(shell echo $$PPID)
+build: $(tmp_bins)
$(tmp_bin)/%: bin/% | $(tmp_bin)
$(INSTALL) -m 755 $< $@.$(pid)
[-- Attachment #3: 0002-t-.t-use-write_file-helper-function.patch --]
[-- Type: text/x-diff, Size: 8088 bytes --]
From 9cbf87fd110acc36c3b6eec14231aed3be78ecf4 Mon Sep 17 00:00:00 2001
From: Eric Wong <bofh@yhbt.net>
Date: Sun, 5 May 2024 22:15:36 +0000
Subject: [PATCH 2/5] t/*.t: use write_file helper function
This shortens the tests a bit for readability.
---
t/active-unix-socket.t | 13 +++----------
t/client_body_buffer_size.t | 5 ++---
t/heartbeat-timeout.t | 4 +---
t/integration.t | 5 ++---
t/reload-bad-config.t | 17 ++++++-----------
t/reopen-logs.t | 5 +----
t/winch_ttin.t | 7 ++-----
t/working_directory.t | 16 ++++------------
8 files changed, 21 insertions(+), 51 deletions(-)
diff --git a/t/active-unix-socket.t b/t/active-unix-socket.t
index ff731b5..ab3c973 100644
--- a/t/active-unix-socket.t
+++ b/t/active-unix-socket.t
@@ -11,29 +11,22 @@ END { kill('TERM', values(%to_kill)) if keys %to_kill }
my $u1 = "$tmpdir/u1.sock";
my $u2 = "$tmpdir/u2.sock";
{
- open my $fh, '>', "$tmpdir/u1.conf.rb";
- print $fh <<EOM;
+ write_file '>', "$tmpdir/u1.conf.rb", <<EOM;
pid "$tmpdir/u.pid"
listen "$u1"
stderr_path "$err_log"
EOM
- close $fh;
-
- open $fh, '>', "$tmpdir/u2.conf.rb";
- print $fh <<EOM;
+ write_file '>', "$tmpdir/u2.conf.rb", <<EOM;
pid "$tmpdir/u.pid"
listen "$u2"
stderr_path "$tmpdir/err2.log"
EOM
- close $fh;
- open $fh, '>', "$tmpdir/u3.conf.rb";
- print $fh <<EOM;
+ write_file '>', "$tmpdir/u3.conf.rb", <<EOM;
pid "$tmpdir/u3.pid"
listen "$u1"
stderr_path "$tmpdir/err3.log"
EOM
- close $fh;
}
my @uarg = qw(-D -E none t/integration.ru);
diff --git a/t/client_body_buffer_size.t b/t/client_body_buffer_size.t
index d479901..c8e871d 100644
--- a/t/client_body_buffer_size.t
+++ b/t/client_body_buffer_size.t
@@ -4,11 +4,10 @@
use v5.14; BEGIN { require './t/lib.perl' };
use autodie;
-open my $conf_fh, '>', $u_conf;
-$conf_fh->autoflush(1);
-print $conf_fh <<EOM;
+my $conf_fh = write_file '>', $u_conf, <<EOM;
client_body_buffer_size 0
EOM
+$conf_fh->autoflush(1);
my $srv = tcp_server();
my $host_port = tcp_host_port($srv);
my @uarg = (qw(-E none t/client_body_buffer_size.ru -c), $u_conf);
diff --git a/t/heartbeat-timeout.t b/t/heartbeat-timeout.t
index 694867a..0ae0764 100644
--- a/t/heartbeat-timeout.t
+++ b/t/heartbeat-timeout.t
@@ -6,14 +6,12 @@ use autodie;
use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
mkdir "$tmpdir/alt";
my $srv = tcp_server();
-open my $fh, '>', $u_conf;
-print $fh <<EOM;
+write_file '>', $u_conf, <<EOM;
pid "$tmpdir/pid"
preload_app true
stderr_path "$err_log"
timeout 3 # WORST FEATURE EVER
EOM
-close $fh;
my $ar = unicorn(qw(-E none t/heartbeat-timeout.ru -c), $u_conf, { 3 => $srv });
diff --git a/t/integration.t b/t/integration.t
index d17ace0..93480fa 100644
--- a/t/integration.t
+++ b/t/integration.t
@@ -18,13 +18,12 @@ if ('ensure Perl does not set SO_KEEPALIVE by default') {
$val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
}
my $t0 = time;
-open my $conf_fh, '>', $u_conf;
-$conf_fh->autoflush(1);
my $u1 = "$tmpdir/u1";
-print $conf_fh <<EOM;
+my $conf_fh = write_file '>', $u_conf, <<EOM;
early_hints true
listen "$u1"
EOM
+$conf_fh->autoflush(1);
my $ar = unicorn(qw(-E none t/integration.ru -c), $u_conf, { 3 => $srv });
my $curl = which('curl');
local $ENV{NO_PROXY} = '*'; # for curl
diff --git a/t/reload-bad-config.t b/t/reload-bad-config.t
index c023b88..4c17968 100644
--- a/t/reload-bad-config.t
+++ b/t/reload-bad-config.t
@@ -6,32 +6,27 @@ use autodie;
my $srv = tcp_server();
my $host_port = tcp_host_port($srv);
my $ru = "$tmpdir/config.ru";
-my $u_conf = "$tmpdir/u.conf.rb";
-open my $fh, '>', $ru;
-print $fh <<'EOM';
+write_file '>', $ru, <<'EOM';
use Rack::ContentLength
use Rack::ContentType, 'text/plain'
config = ru = "hello world\n" # check for config variable conflicts, too
run lambda { |env| [ 200, {}, [ ru.to_s ] ] }
EOM
-close $fh;
-open $fh, '>', $u_conf;
-print $fh <<EOM;
+write_file '>', $u_conf, <<EOM;
preload_app true
stderr_path "$err_log"
EOM
-close $fh;
my $ar = unicorn(qw(-E none -c), $u_conf, $ru, { 3 => $srv });
my ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid at start');
is($bdy, "hello world\n", 'body matches expected');
-open $fh, '>>', $ru;
-say $fh '....this better be a syntax error in any version of ruby...';
-close $fh;
+write_file '>>', $ru, <<'EOM';
+....this better be a syntax error in any version of ruby...
+EOM
$ar->do_kill('HUP'); # reload
my @l;
@@ -42,7 +37,7 @@ for (1..1000) {
}
diag slurp($err_log) if $ENV{V};
ok(grep(/error reloading/, @l), 'got error reloading');
-open $fh, '>', $err_log;
+open my $fh, '>', $err_log; # truncate
close $fh;
($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
diff --git a/t/reopen-logs.t b/t/reopen-logs.t
index 76a4dbd..14bc6ef 100644
--- a/t/reopen-logs.t
+++ b/t/reopen-logs.t
@@ -4,14 +4,11 @@
use v5.14; BEGIN { require './t/lib.perl' };
use autodie;
my $srv = tcp_server();
-my $u_conf = "$tmpdir/u.conf.rb";
my $out_log = "$tmpdir/out.log";
-open my $fh, '>', $u_conf;
-print $fh <<EOM;
+write_file '>', $u_conf, <<EOM;
stderr_path "$err_log"
stdout_path "$out_log"
EOM
-close $fh;
my $auto_reap = unicorn('-c', $u_conf, 't/reopen-logs.ru', { 3 => $srv } );
my ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
diff --git a/t/winch_ttin.t b/t/winch_ttin.t
index c507959..3a3d430 100644
--- a/t/winch_ttin.t
+++ b/t/winch_ttin.t
@@ -4,13 +4,11 @@
use v5.14; BEGIN { require './t/lib.perl' };
use autodie;
use POSIX qw(mkfifo);
-my $u_conf = "$tmpdir/u.conf.rb";
my $u_sock = "$tmpdir/u.sock";
my $fifo = "$tmpdir/fifo";
mkfifo($fifo, 0666) or die "mkfifo($fifo): $!";
-open my $fh, '>', $u_conf;
-print $fh <<EOM;
+write_file '>', $u_conf, <<EOM;
pid "$tmpdir/pid"
listen "$u_sock"
stderr_path "$err_log"
@@ -19,11 +17,10 @@ after_fork do |server, worker|
File.open("$fifo", "wb") { |fp| fp.syswrite worker.nr.to_s }
end
EOM
-close $fh;
unicorn('-D', '-c', $u_conf, 't/integration.ru')->join;
is($?, 0, 'daemonized properly');
-open $fh, '<', "$tmpdir/pid";
+open my $fh, '<', "$tmpdir/pid";
chomp(my $pid = <$fh>);
ok(kill(0, $pid), 'daemonized PID works');
my $quit = sub { kill('QUIT', $pid) if $pid; $pid = undef };
diff --git a/t/working_directory.t b/t/working_directory.t
index f9254eb..f1c0a35 100644
--- a/t/working_directory.t
+++ b/t/working_directory.t
@@ -5,15 +5,13 @@ use v5.14; BEGIN { require './t/lib.perl' };
use autodie;
mkdir "$tmpdir/alt";
my $ru = "$tmpdir/alt/config.ru";
-open my $fh, '>', $u_conf;
-print $fh <<EOM;
+write_file '>', $u_conf, <<EOM;
pid "$pid_file"
preload_app true
stderr_path "$err_log"
working_directory "$tmpdir/alt" # the whole point of this test
before_fork { |_,_| \$master_ppid = Process.ppid }
EOM
-close $fh;
my $common_ru = <<'EOM';
use Rack::ContentLength
@@ -21,12 +19,10 @@ use Rack::ContentType, 'text/plain'
run lambda { |env| [ 200, {}, [ "#{$master_ppid}\n" ] ] }
EOM
-open $fh, '>', $ru;
-print $fh <<EOM;
+write_file '>', $ru, <<EOM;
#\\--daemonize --listen $u_sock
$common_ru
EOM
-close $fh;
unicorn('-c', $u_conf)->join; # will daemonize
chomp($daemon_pid = slurp($pid_file));
@@ -39,9 +35,7 @@ check_stderr;
if ('test without CLI switches in config.ru') {
truncate $err_log, 0;
- open $fh, '>', $ru;
- print $fh $common_ru;
- close $fh;
+ write_file '>', $ru, $common_ru;
unicorn('-D', '-l', $u_sock, '-c', $u_conf)->join; # will daemonize
chomp($daemon_pid = slurp($pid_file));
@@ -68,8 +62,7 @@ if ('ensures broken working_directory (missing config.ru) is OK') {
if ('fooapp.rb (not config.ru) works with working_directory') {
truncate $err_log, 0;
my $fooapp = "$tmpdir/alt/fooapp.rb";
- open $fh, '>', $fooapp;
- print $fh <<EOM;
+ write_file '>', $fooapp, <<EOM;
class Fooapp
def self.call(env)
b = "dir=#{Dir.pwd}"
@@ -78,7 +71,6 @@ class Fooapp
end
end
EOM
- close $fh;
my $srv = tcp_server;
my $auto_reap = unicorn(qw(-c), $u_conf, qw(-I. fooapp.rb),
{ -C => '/', 3 => $srv });
[-- Attachment #4: 0003-tests-port-broken-app-test-to-Perl-5.patch --]
[-- Type: text/x-diff, Size: 4208 bytes --]
From e61a1152a613032927613b805a46c4d831bad00c Mon Sep 17 00:00:00 2001
From: Eric Wong <bofh@yhbt.net>
Date: Sun, 5 May 2024 22:15:37 +0000
Subject: [PATCH 3/5] tests: port broken-app test to Perl 5
Save some inodes and startup time by folding it into the
integration test.
---
t/broken-app.ru | 13 ----------
t/integration.ru | 1 +
t/integration.t | 18 +++++++++++++-
t/lib.perl | 2 +-
t/t0009-broken-app.sh | 56 -------------------------------------------
5 files changed, 19 insertions(+), 71 deletions(-)
delete mode 100644 t/broken-app.ru
delete mode 100755 t/t0009-broken-app.sh
diff --git a/t/broken-app.ru b/t/broken-app.ru
deleted file mode 100644
index 5966bff..0000000
--- a/t/broken-app.ru
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: false
-# we do not want Rack::Lint or anything to protect us
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-map "/" do
- run lambda { |env| [ 200, {}, [ "OK\n" ] ] }
-end
-map "/raise" do
- run lambda { |env| raise "BAD" }
-end
-map "/nil" do
- run lambda { |env| nil }
-end
diff --git a/t/integration.ru b/t/integration.ru
index 6df481c..3a0d99c 100644
--- a/t/integration.ru
+++ b/t/integration.ru
@@ -100,6 +100,7 @@ def rack_input_tests(env)
when '/early_hints_rack2'; early_hints(env, "r\n2")
when '/early_hints_rack3'; early_hints(env, %w(r 3))
when '/broken_app'; raise RuntimeError, 'hello'
+ when '/nil'; nil
else '/'; [ 200, {}, [ env_dump(env) ] ]
end # case PATH_INFO (GET)
when 'POST'
diff --git a/t/integration.t b/t/integration.t
index 93480fa..c9a7877 100644
--- a/t/integration.t
+++ b/t/integration.t
@@ -122,7 +122,23 @@ check_stderr;
($status, $hdr, $bdy) = do_req($srv, 'GET /broken_app HTTP/1.0');
like($status, qr!\AHTTP/1\.[0-1] 500\b!, 'got 500 error on broken endpoint');
is($bdy, undef, 'no response body after exception');
-truncate($errfh, 0);
+seek $errfh, 0, SEEK_SET;
+{
+ my $nxt;
+ while (!defined($nxt) && defined($_ = <$errfh>)) {
+ $nxt = <$errfh> if /app error/;
+ }
+ ok $nxt, 'got app error' and
+ like $nxt, qr/\bintegration\.ru/, 'got backtrace';
+}
+seek $errfh, 0, SEEK_SET;
+truncate $errfh, 0;
+
+($status, $hdr, $bdy) = do_req($srv, 'GET /nil HTTP/1.0');
+like($status, qr!\AHTTP/1\.[0-1] 500\b!, 'got 500 error on nil endpoint');
+like slurp($err_log), qr/app error/, 'exception logged for nil';
+seek $errfh, 0, SEEK_SET;
+truncate $errfh, 0;
my $ck_early_hints = sub {
my ($note) = @_;
diff --git a/t/lib.perl b/t/lib.perl
index 8c842b1..382f08c 100644
--- a/t/lib.perl
+++ b/t/lib.perl
@@ -30,7 +30,7 @@ $pid_file = "$tmpdir/pid";
$fifo = "$tmpdir/fifo";
$u_sock = "$tmpdir/u.sock";
$u_conf = "$tmpdir/u.conf.rb";
-open($errfh, '>>', $err_log);
+open($errfh, '+>>', $err_log);
if (my $t = $ENV{TAIL}) {
my @tail = $t =~ /tail/ ? split(/\s+/, $t) : (qw(tail -F));
diff --git a/t/t0009-broken-app.sh b/t/t0009-broken-app.sh
deleted file mode 100755
index 895b178..0000000
--- a/t/t0009-broken-app.sh
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 9 "graceful handling of broken apps"
-
-t_begin "setup and start" && {
- unicorn_setup
- unicorn -E none -D broken-app.ru -c $unicorn_config
- unicorn_wait_start
-}
-
-t_begin "normal response is alright" && {
- test xOK = x"$(curl -sSf http://$listen/)"
-}
-
-t_begin "app raised exception" && {
- curl -sSf http://$listen/raise 2> $tmp || :
- grep -F 500 $tmp
- > $tmp
-}
-
-t_begin "app exception logged and backtrace not swallowed" && {
- grep -F 'app error' $r_err
- grep -A1 -F 'app error' $r_err | tail -1 | grep broken-app.ru:
- dbgcat r_err
- > $r_err
-}
-
-t_begin "trigger bad response" && {
- curl -sSf http://$listen/nil 2> $tmp || :
- grep -F 500 $tmp
- > $tmp
-}
-
-t_begin "app exception logged" && {
- grep -F 'app error' $r_err
- > $r_err
-}
-
-t_begin "normal responses alright afterwards" && {
- > $tmp
- curl -sSf http://$listen/ >> $tmp &
- curl -sSf http://$listen/ >> $tmp &
- curl -sSf http://$listen/ >> $tmp &
- curl -sSf http://$listen/ >> $tmp &
- wait
- test xOK = x$(sort < $tmp | uniq)
-}
-
-t_begin "teardown" && {
- kill $unicorn_pid
-}
-
-t_begin "check stderr" && check_stderr
-
-t_done
[-- Attachment #5: 0004-tests-move-test-unit-test_request.rb-to-Perl-5.patch --]
[-- Type: text/x-diff, Size: 11298 bytes --]
From 01224642a20e91de5ea18c6f20856142158068a8 Mon Sep 17 00:00:00 2001
From: Eric Wong <bofh@yhbt.net>
Date: Sun, 5 May 2024 22:15:38 +0000
Subject: [PATCH 4/5] tests: move test/unit/test_request.rb to Perl 5
Another step towards having more freedom to change our internals
and having a more stable language for tests to reduce
maintenance overhead by avoiding Ruby incompatibilities.
---
t/integration.ru | 6 ++
t/integration.t | 72 +++++++++++++++-
test/unit/test_request.rb | 170 --------------------------------------
3 files changed, 75 insertions(+), 173 deletions(-)
delete mode 100644 test/unit/test_request.rb
diff --git a/t/integration.ru b/t/integration.ru
index 3a0d99c..a6b022a 100644
--- a/t/integration.ru
+++ b/t/integration.ru
@@ -47,6 +47,7 @@ def env_dump(env)
else
case k
when 'rack.version', 'rack.after_reply'; h[k] = v
+ when 'rack.input'; h[k] = v.class.to_s
end
end
end
@@ -112,6 +113,11 @@ def rack_input_tests(env)
when 'PUT'
case env['PATH_INFO']
when %r{\A/rack_input}; rack_input_tests(env)
+ when '/env_dump'; [ 200, {}, [ env_dump(env) ] ]
+ end
+ when 'OPTIONS'
+ case env['REQUEST_URI']
+ when '*'; [ 200, {}, [ env_dump(env) ] ]
end
end # case REQUEST_METHOD
end) # run
diff --git a/t/integration.t b/t/integration.t
index c9a7877..3b1d6df 100644
--- a/t/integration.t
+++ b/t/integration.t
@@ -103,14 +103,75 @@ is_deeply([ grep(/^x-r3: /, @$hdr) ],
SKIP: {
eval { require JSON::PP } or skip "JSON::PP missing: $@", 1;
- ($status, $hdr, my $json) = do_req $srv, 'GET /env_dump';
+ my $get_json = sub {
+ my (@req) = @_;
+ my @r = do_req $srv, @req;
+ my $env = eval { JSON::PP->new->decode($r[2]) };
+ diag "$@ (r[2]=$r[2])" if $@;
+ is ref($env), 'HASH', "@req response body is JSON";
+ (@r, $env)
+ };
+ ($status, $hdr, my $json, my $env) = $get_json->('GET /env_dump');
is($status, undef, 'no status for HTTP/0.9');
is($hdr, undef, 'no header for HTTP/0.9');
unlike($json, qr/^Connection: /smi, 'no connection header for 0.9');
unlike($json, qr!\AHTTP/!s, 'no HTTP/1.x prefix for 0.9');
- my $env = JSON::PP->new->decode($json);
- is(ref($env), 'HASH', 'JSON decoded body to hashref');
is($env->{SERVER_PROTOCOL}, 'HTTP/0.9', 'SERVER_PROTOCOL is 0.9');
+ is $env->{'rack.url_scheme'}, 'http', 'rack.url_scheme default';
+ is $env->{'rack.input'}, 'StringIO', 'StringIO for no content';
+
+ my $req = 'OPTIONS *';
+ ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0");
+ is $env->{REQUEST_PATH}, '', "$req => REQUEST_PATH";
+ is $env->{PATH_INFO}, '', "$req => PATH_INFO";
+ is $env->{REQUEST_URI}, '*', "$req => REQUEST_URI";
+
+ $req = 'GET http://e:3/env_dump?y=z';
+ ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0");
+ is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH";
+ is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO";
+ is $env->{QUERY_STRING}, 'y=z', "$req => QUERY_STRING";
+
+ $req = 'GET http://e:3/env_dump#frag';
+ ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0");
+ is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH";
+ is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO";
+ is $env->{QUERY_STRING}, '', "$req => QUERY_STRING";
+ is $env->{FRAGMENT}, 'frag', "$req => FRAGMENT";
+
+ $req = 'GET http://e:3/env_dump?a=b#frag';
+ ($status, $hdr, $json, $env) = $get_json->("$req HTTP/1.0");
+ is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH";
+ is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO";
+ is $env->{QUERY_STRING}, 'a=b', "$req => QUERY_STRING";
+ is $env->{FRAGMENT}, 'frag', "$req => FRAGMENT";
+
+ for my $proto (qw(https http)) {
+ $req = "X-Forwarded-Proto: $proto";
+ ($status, $hdr, $json, $env) = $get_json->(
+ "GET /env_dump HTTP/1.0\r\n".
+ "X-Forwarded-Proto: $proto");
+ is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH";
+ is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO";
+ is $env->{'rack.url_scheme'}, $proto, "$req => rack.url_scheme";
+ }
+
+ $req = 'X-Forwarded-Proto: ftp'; # invalid proto
+ ($status, $hdr, $json, $env) = $get_json->(
+ "GET /env_dump HTTP/1.0\r\n".
+ "X-Forwarded-Proto: ftp");
+ is $env->{REQUEST_PATH}, '/env_dump', "$req => REQUEST_PATH";
+ is $env->{PATH_INFO}, '/env_dump', "$req => PATH_INFO";
+ is $env->{'rack.url_scheme'}, 'http', "$req => rack.url_scheme";
+
+ ($status, $hdr, $json, $env) = $get_json->("PUT /env_dump HTTP/1.0\r\n".
+ 'Content-Length: 0');
+ is $env->{'rack.input'}, 'StringIO', 'content-length: 0 uses StringIO';
+
+ ($status, $hdr, $json, $env) = $get_json->("PUT /env_dump HTTP/1.0\r\n".
+ 'Content-Length: 1');
+ is $env->{'rack.input'}, 'Unicorn::TeeInput',
+ 'content-length: 1 uses TeeInput';
}
# cf. <CAO47=rJa=zRcLn_Xm4v2cHPr6c0UswaFC_omYFEH+baSxHOWKQ@mail.gmail.com>
@@ -179,6 +240,11 @@ if ('bad requests') {
($status, $hdr) = do_req $srv, 'GET /env_dump HTTP/1/1';
like($status, qr!\AHTTP/1\.[01] 400 \b!, 'got 400 on bad request');
+ for my $abs_uri (qw(ssh+http://e/ ftp://e/x http+ssh://e/x)) {
+ ($status, $hdr) = do_req $srv, "GET $abs_uri HTTP/1.0";
+ like $status, qr!\AHTTP/1\.[01] 400 \b!, "400 on $abs_uri";
+ }
+
$c = tcp_start($srv);
print $c 'GET /';
my $buf = join('', (0..9), 'ab');
diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb
deleted file mode 100644
index 9d1b350..0000000
--- a/test/unit/test_request.rb
+++ /dev/null
@@ -1,170 +0,0 @@
-# -*- encoding: binary -*-
-# frozen_string_literal: false
-
-# Copyright (c) 2009 Eric Wong
-# You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
-# the GPLv2+ (GPLv3+ preferred)
-
-require './test/test_helper'
-
-include Unicorn
-
-class RequestTest < Test::Unit::TestCase
-
- MockRequest = Class.new(StringIO)
-
- AI = Addrinfo.new(Socket.sockaddr_un('/unicorn/sucks'))
-
- def setup
- @request = HttpRequest.new
- @app = lambda do |env|
- [ 200, { 'content-length' => '0', 'content-type' => 'text/plain' }, [] ]
- end
- @lint = Rack::Lint.new(@app)
- end
-
- def test_options
- client = MockRequest.new("OPTIONS * HTTP/1.1\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal '', env['REQUEST_PATH']
- assert_equal '', env['PATH_INFO']
- assert_equal '*', env['REQUEST_URI']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_absolute_uri_with_query
- client = MockRequest.new("GET http://e:3/x?y=z HTTP/1.1\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal '/x', env['REQUEST_PATH']
- assert_equal '/x', env['PATH_INFO']
- assert_equal 'y=z', env['QUERY_STRING']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_absolute_uri_with_fragment
- client = MockRequest.new("GET http://e:3/x#frag HTTP/1.1\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal '/x', env['REQUEST_PATH']
- assert_equal '/x', env['PATH_INFO']
- assert_equal '', env['QUERY_STRING']
- assert_equal 'frag', env['FRAGMENT']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_absolute_uri_with_query_and_fragment
- client = MockRequest.new("GET http://e:3/x?a=b#frag HTTP/1.1\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal '/x', env['REQUEST_PATH']
- assert_equal '/x', env['PATH_INFO']
- assert_equal 'a=b', env['QUERY_STRING']
- assert_equal 'frag', env['FRAGMENT']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_absolute_uri_unsupported_schemes
- %w(ssh+http://e/ ftp://e/x http+ssh://e/x).each do |abs_uri|
- client = MockRequest.new("GET #{abs_uri} HTTP/1.1\r\n" \
- "Host: foo\r\n\r\n")
- assert_raises(HttpParserError) { @request.read_headers(client, AI) }
- end
- end
-
- def test_x_forwarded_proto_https
- client = MockRequest.new("GET / HTTP/1.1\r\n" \
- "X-Forwarded-Proto: https\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal "https", env['rack.url_scheme']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_x_forwarded_proto_http
- client = MockRequest.new("GET / HTTP/1.1\r\n" \
- "X-Forwarded-Proto: http\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal "http", env['rack.url_scheme']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_x_forwarded_proto_invalid
- client = MockRequest.new("GET / HTTP/1.1\r\n" \
- "X-Forwarded-Proto: ftp\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal "http", env['rack.url_scheme']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_rack_lint_get
- client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal "http", env['rack.url_scheme']
- assert_equal '127.0.0.1', env['REMOTE_ADDR']
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_no_content_stringio
- client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal StringIO, env['rack.input'].class
- end
-
- def test_zero_content_stringio
- client = MockRequest.new("PUT / HTTP/1.1\r\n" \
- "Content-Length: 0\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal StringIO, env['rack.input'].class
- end
-
- def test_real_content_not_stringio
- client = MockRequest.new("PUT / HTTP/1.1\r\n" \
- "Content-Length: 1\r\n" \
- "Host: foo\r\n\r\n")
- env = @request.read_headers(client, AI)
- assert_equal Unicorn::TeeInput, env['rack.input'].class
- end
-
- def test_rack_lint_put
- client = MockRequest.new(
- "PUT / HTTP/1.1\r\n" \
- "Host: foo\r\n" \
- "Content-Length: 5\r\n" \
- "\r\n" \
- "abcde")
- env = @request.read_headers(client, AI)
- assert ! env.include?(:http_body)
- assert_kind_of Array, @lint.call(env)
- end
-
- def test_rack_lint_big_put
- count = 100
- bs = 0x10000
- buf = (' ' * bs).freeze
- length = bs * count
- client = Tempfile.new('big_put')
- client.syswrite(
- "PUT / HTTP/1.1\r\n" \
- "Host: foo\r\n" \
- "Content-Length: #{length}\r\n" \
- "\r\n")
- count.times { assert_equal bs, client.syswrite(buf) }
- assert_equal 0, client.sysseek(0)
- env = @request.read_headers(client, AI)
- assert ! env.include?(:http_body)
- assert_equal length, env['rack.input'].size
- count.times {
- tmp = env['rack.input'].read(bs)
- tmp << env['rack.input'].read(bs - tmp.size) if tmp.size != bs
- assert_equal buf, tmp
- }
- assert_nil env['rack.input'].read(bs)
- env['rack.input'].rewind
- assert_kind_of Array, @lint.call(env)
- end
-end
[-- Attachment #6: 0005-port-test-unit-test_ccc.rb-to-Perl-5.patch --]
[-- Type: text/x-diff, Size: 6013 bytes --]
From d6d127f50f9225bf51ef6ce0abce9bad87efaae3 Mon Sep 17 00:00:00 2001
From: Eric Wong <bofh@yhbt.net>
Date: Sun, 5 May 2024 22:15:39 +0000
Subject: [PATCH 5/5] port test/unit/test_ccc.rb to Perl 5
We'll fold this into integration.t to reduce startup time
penalties and get the benefit of a stable language to reduce
maintenance overhead.
---
t/integration.ru | 4 ++
t/integration.t | 33 ++++++++++++++++
test/unit/test_ccc.rb | 92 -------------------------------------------
3 files changed, 37 insertions(+), 92 deletions(-)
delete mode 100644 test/unit/test_ccc.rb
diff --git a/t/integration.ru b/t/integration.ru
index a6b022a..aaed608 100644
--- a/t/integration.ru
+++ b/t/integration.ru
@@ -87,6 +87,7 @@ def rack_input_tests(env)
[ 200, h, [ dig.hexdigest ] ]
end
+$nr_aborts = 0
run(lambda do |env|
case env['REQUEST_METHOD']
when 'GET'
@@ -101,7 +102,10 @@ def rack_input_tests(env)
when '/early_hints_rack2'; early_hints(env, "r\n2")
when '/early_hints_rack3'; early_hints(env, %w(r 3))
when '/broken_app'; raise RuntimeError, 'hello'
+ when '/aborted'; $nr_aborts += 1; [ 200, {}, [] ]
+ when '/nr_aborts'; [ 200, { 'nr-aborts' => "#$nr_aborts" }, [] ]
when '/nil'; nil
+ when '/read_fifo'; [ 200, {}, [ File.read(env['HTTP_READ_FIFO']) ] ]
else '/'; [ 200, {}, [ env_dump(env) ] ]
end # case PATH_INFO (GET)
when 'POST'
diff --git a/t/integration.t b/t/integration.t
index 3b1d6df..2d448cd 100644
--- a/t/integration.t
+++ b/t/integration.t
@@ -405,6 +405,39 @@ EOM
my $wpid = readline($fifo_fh);
like($wpid, qr/\Apid=\d+\z/a , 'new worker ready');
$ck_early_hints->('ccc on');
+
+ $c = tcp_start $srv, 'GET /env_dump HTTP/1.0';
+ vec(my $rvec = '', fileno($c), 1) = 1;
+ select($rvec, undef, undef, 10) or BAIL_OUT 'timed out env_dump';
+ ($status, $hdr) = slurp_hdr($c);
+ like $status, qr!\AHTTP/1\.[01] 200!, 'got part of first response';
+ ok $hdr, 'got all headers';
+
+ # start a slow TCP request
+ my $rfifo = "$tmpdir/rfifo";
+ mkfifo_die $rfifo;
+ $c = tcp_start $srv, "GET /read_fifo HTTP/1.0\r\nRead-FIFO: $rfifo";
+ tcp_start $srv, 'GET /aborted HTTP/1.0' for (1..100);
+ write_file '>', $rfifo, 'TFIN';
+ ($status, $hdr) = slurp_hdr($c);
+ like $status, qr!\AHTTP/1\.[01] 200!, 'got part of first response';
+ $bdy = <$c>;
+ is $bdy, 'TFIN', 'got slow response from TCP socket';
+
+ # slow Unix socket request
+ $c = unix_start $u1, "GET /read_fifo HTTP/1.0\r\nRead-FIFO: $rfifo";
+ vec($rvec = '', fileno($c), 1) = 1;
+ select($rvec, undef, undef, 10) or BAIL_OUT 'timed out Unix CCC';
+ unix_start $u1, 'GET /aborted HTTP/1.0' for (1..100);
+ write_file '>', $rfifo, 'UFIN';
+ ($status, $hdr) = slurp_hdr($c);
+ like $status, qr!\AHTTP/1\.[01] 200!, 'got part of first response';
+ $bdy = <$c>;
+ is $bdy, 'UFIN', 'got slow response from Unix socket';
+
+ ($status, $hdr, $bdy) = do_req $srv, 'GET /nr_aborts HTTP/1.0';
+ like "@$hdr", qr/nr-aborts: 0\b/,
+ 'aborted connections unseen by Rack app';
}
if ('max_header_len internal API') {
diff --git a/test/unit/test_ccc.rb b/test/unit/test_ccc.rb
deleted file mode 100644
index a0a2bff..0000000
--- a/test/unit/test_ccc.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: false
-require 'socket'
-require 'unicorn'
-require 'io/wait'
-require 'tempfile'
-require 'test/unit'
-require './test/test_helper'
-
-class TestCccTCPI < Test::Unit::TestCase
- def test_ccc_tcpi
- start_pid = $$
- host = '127.0.0.1'
- srv = TCPServer.new(host, 0)
- port = srv.addr[1]
- err = Tempfile.new('unicorn_ccc')
- rd, wr = IO.pipe
- sleep_pipe = IO.pipe
- pid = fork do
- sleep_pipe[1].close
- reqs = 0
- rd.close
- worker_pid = nil
- app = lambda do |env|
- worker_pid ||= begin
- at_exit { wr.write(reqs.to_s) if worker_pid == $$ }
- $$
- end
- reqs += 1
-
- # will wake up when writer closes
- sleep_pipe[0].read if env['PATH_INFO'] == '/sleep'
-
- [ 200, {'content-length'=>'0', 'content-type'=>'text/plain'}, [] ]
- end
- ENV['UNICORN_FD'] = srv.fileno.to_s
- opts = {
- listeners: [ "#{host}:#{port}" ],
- stderr_path: err.path,
- check_client_connection: true,
- }
- uni = Unicorn::HttpServer.new(app, opts)
- uni.start.join
- end
- wr.close
-
- # make sure the server is running, at least
- client = tcp_socket(host, port)
- client.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
- assert client.wait(10), 'never got response from server'
- res = client.read
- assert_match %r{\AHTTP/1\.1 200}, res, 'got part of first response'
- assert_match %r{\r\n\r\n\z}, res, 'got end of response, server is ready'
- client.close
-
- # start a slow request...
- sleeper = tcp_socket(host, port)
- sleeper.write("GET /sleep HTTP/1.1\r\nHost: example.com\r\n\r\n")
-
- # and a bunch of aborted ones
- nr = 100
- nr.times do |i|
- client = tcp_socket(host, port)
- client.write("GET /collections/#{rand(10000)} HTTP/1.1\r\n" \
- "Host: example.com\r\n\r\n")
- client.close
- end
- sleep_pipe[1].close # wake up the reader in the worker
- res = sleeper.read
- assert_match %r{\AHTTP/1\.1 200}, res, 'got part of first sleeper response'
- assert_match %r{\r\n\r\n\z}, res, 'got end of sleeper response'
- sleeper.close
- kpid = pid
- pid = nil
- Process.kill(:QUIT, kpid)
- _, status = Process.waitpid2(kpid)
- assert status.success?
- reqs = rd.read.to_i
- warn "server got #{reqs} requests with #{nr} CCC aborted\n" if $DEBUG
- assert_operator reqs, :<, nr
- assert_operator reqs, :>=, 2, 'first 2 requests got through, at least'
- ensure
- return if start_pid != $$
- srv.close if srv
- if pid
- Process.kill(:QUIT, pid)
- _, status = Process.waitpid2(pid)
- assert status.success?
- end
- err.close! if err
- rd.close if rd
- end
-end
reply other threads:[~2024-05-06 20:10 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://yhbt.net/unicorn/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20240506201021.M727962@dcvr \
--to=bofh@yhbt.net \
--cc=unicorn-public@yhbt.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://yhbt.net/unicorn.git/
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).