about summary refs log tree commit homepage
path: root/t
diff options
context:
space:
mode:
Diffstat (limited to 't')
-rw-r--r--t/GNUmakefile75
-rw-r--r--t/README21
-rw-r--r--t/active-unix-socket.t117
-rw-r--r--t/back-out-of-upgrade.t44
-rwxr-xr-xt/bin/content-md5-put36
-rwxr-xr-xt/bin/sha1sum.rb17
-rw-r--r--t/broken-app.ru1
-rw-r--r--t/client_body_buffer_size.ru (renamed from t/t0116.ru)3
-rw-r--r--t/client_body_buffer_size.t80
-rw-r--r--t/detach.ru1
-rw-r--r--t/env.ru1
-rw-r--r--t/fails-rack-lint.ru1
-rw-r--r--t/heartbeat-timeout.ru5
-rw-r--r--t/heartbeat-timeout.t62
-rw-r--r--t/hijack.ru55
-rw-r--r--t/integration.ru116
-rw-r--r--t/integration.t357
-rw-r--r--t/lib.perl309
-rw-r--r--t/listener_names.ru1
-rw-r--r--t/oob_gc.ru4
-rw-r--r--t/oob_gc_path.ru4
-rw-r--r--t/pid.ru1
-rw-r--r--t/preread_input.ru22
-rw-r--r--t/rack-input-tests.ru21
-rw-r--r--t/reload-bad-config.t54
-rw-r--r--t/reopen-logs.ru (renamed from t/t0006.ru)1
-rw-r--r--t/reopen-logs.t39
-rwxr-xr-xt/t0000-http-basic.sh50
-rwxr-xr-xt/t0001-reload-bad-config.sh53
-rwxr-xr-xt/t0002-config-conflict.sh49
-rwxr-xr-xt/t0002-parser-error.sh94
-rwxr-xr-xt/t0003-working_directory.sh51
-rwxr-xr-xt/t0004-heartbeat-timeout.sh69
-rwxr-xr-xt/t0004-working_directory_broken.sh24
-rwxr-xr-xt/t0005-working_directory_app.rb.sh40
-rwxr-xr-xt/t0006-reopen-logs.sh83
-rwxr-xr-xt/t0007-working_directory_no_embed_cli.sh44
-rwxr-xr-xt/t0008-back_out_of_upgrade.sh110
-rwxr-xr-xt/t0009-winch_ttin.sh59
-rwxr-xr-xt/t0011-active-unix-socket.sh79
-rw-r--r--t/t0013.ru1
-rw-r--r--t/t0014.ru1
-rwxr-xr-xt/t0018-write-on-close.sh23
-rwxr-xr-xt/t0019-max_header_len.sh49
-rwxr-xr-xt/t0100-rack-input-tests.sh124
-rwxr-xr-xt/t0116-client_body_buffer_size.sh80
-rwxr-xr-xt/t0200-rack-hijack.sh51
-rw-r--r--t/t0300-no-default-middleware.sh2
-rw-r--r--t/t0301.ru5
-rwxr-xr-xt/t9000-preread-input.sh48
-rw-r--r--t/test-lib.sh7
-rw-r--r--t/winch_ttin.t67
-rw-r--r--t/working_directory.t94
-rw-r--r--t/write-on-close.ru11
54 files changed, 1391 insertions, 1425 deletions
diff --git a/t/GNUmakefile b/t/GNUmakefile
index 5f5d9bc..0ac9b9a 100644
--- a/t/GNUmakefile
+++ b/t/GNUmakefile
@@ -1,74 +1,5 @@
-# we can run tests in parallel with GNU make
+# there used to be more, here, but we stopped relying on recursive make
 all::
+        $(MAKE) -C .. test-integration
 
-pid := $(shell echo $$PPID)
-
-RUBY = ruby
-RAKE = rake
--include ../local.mk
-ifeq ($(RUBY_VERSION),)
-  RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
-endif
-
-ifeq ($(RUBY_VERSION),)
-  $(error unable to detect RUBY_VERSION)
-endif
-
-RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
-export RUBY_ENGINE
-
-MYLIBS := $(RUBYLIB)
-
-T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
-
-all:: $(T)
-
-# can't rely on "set -o pipefail" since we don't require bash or ksh93 :<
-t_pfx = trash/$@-$(RUBY_ENGINE)-$(RUBY_VERSION)
-TEST_OPTS =
-# TRACER = strace -f -o $(t_pfx).strace -s 100000
-# TRACER = /usr/bin/time -o $(t_pfx).time
-
-ifdef V
-  ifeq ($(V),2)
-    TEST_OPTS += --trace
-  else
-    TEST_OPTS += --verbose
-  endif
-endif
-
-random_blob:
-        dd if=/dev/urandom bs=1M count=30 of=$@.$(pid)
-        mv $@.$(pid) $@
-
-$(T): random_blob
-
-dependencies := socat curl
-deps := $(addprefix .dep+,$(dependencies))
-$(deps): dep_bin = $(lastword $(subst +, ,$@))
-$(deps):
-        @which $(dep_bin) > $@.$(pid) 2>/dev/null || :
-        @test -s $@.$(pid) || \
-          { echo >&2 "E '$(dep_bin)' not found in PATH=$(PATH)"; exit 1; }
-        @mv $@.$(pid) $@
-dep: $(deps)
-
-test_prefix := $(CURDIR)/../test/$(RUBY_ENGINE)-$(RUBY_VERSION)
-$(test_prefix)/.stamp:
-        $(MAKE) -C .. test-install
-
-$(T): export RUBY := $(RUBY)
-$(T): export RAKE := $(RAKE)
-$(T): export PATH := $(test_prefix)/bin:$(PATH)
-$(T): export RUBYLIB := $(test_prefix)/lib:$(MYLIBS)
-$(T): dep $(test_prefix)/.stamp trash/.gitignore
-        $(TRACER) $(SHELL) $(SH_TEST_OPTS) $@ $(TEST_OPTS)
-
-trash/.gitignore:
-        mkdir -p $(@D)
-        echo '*' > $@
-
-clean:
-        $(RM) -r trash/*
-
-.PHONY: $(T) clean
+.PHONY: all
diff --git a/t/README b/t/README
index 0d9b697..7bd093d 100644
--- a/t/README
+++ b/t/README
@@ -5,16 +5,19 @@ TCP ports or Unix domain sockets.  They're all designed to run
 concurrently with other tests to minimize test time, but tests may be
 run independently as well.
 
-We write our tests in Bourne shell because that's what we're
-comfortable writing integration tests with.
+New tests are written in Perl 5 because we need a stable language
+to test real-world behavior and Ruby introduces incompatibilities
+at a far faster rate than Perl 5.  Perl is Ruby's older cousin, so
+it should be easy-to-learn for Rubyists.
+
+Old tests are in Bourne shell and slowly being ported to Perl 5.
 
 == Requirements
 
-* {Ruby 1.9.3+}[https://www.ruby-lang.org/en/] (duh!)
+* {Ruby 2.5.0+}[https://www.ruby-lang.org/en/]
+* {Perl 5.14+}[https://www.perl.org/] # your distro should have it
 * {GNU make}[https://www.gnu.org/software/make/]
-* {socat}[http://www.dest-unreach.org/socat/]
 * {curl}[https://curl.haxx.se/]
-* standard UNIX shell utilities (Bourne sh, awk, sed, grep, ...)
 
 We do not use bashisms or any non-portable, non-POSIX constructs
 in our shell code.  We use the "pipefail" option if available and
@@ -26,9 +29,13 @@ with {dash}[http://gondor.apana.org.au/~herbert/dash/] and
 
 To run the entire test suite with 8 tests running at once:
 
-  make -j8
+  make -j8 && prove -vw
+
+To run one individual test (Perl5):
+
+  prove -vw t/integration.t
 
-To run one individual test:
+To run one individual test (shell):
 
   make t0000-simple-http.sh
 
diff --git a/t/active-unix-socket.t b/t/active-unix-socket.t
new file mode 100644
index 0000000..ff731b5
--- /dev/null
+++ b/t/active-unix-socket.t
@@ -0,0 +1,117 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+use v5.14; BEGIN { require './t/lib.perl' };
+use IO::Socket::UNIX;
+use autodie;
+no autodie 'kill';
+my %to_kill;
+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;
+pid "$tmpdir/u.pid"
+listen "$u1"
+stderr_path "$err_log"
+EOM
+        close $fh;
+
+        open $fh, '>', "$tmpdir/u2.conf.rb";
+        print $fh <<EOM;
+pid "$tmpdir/u.pid"
+listen "$u2"
+stderr_path "$tmpdir/err2.log"
+EOM
+        close $fh;
+
+        open $fh, '>', "$tmpdir/u3.conf.rb";
+        print $fh <<EOM;
+pid "$tmpdir/u3.pid"
+listen "$u1"
+stderr_path "$tmpdir/err3.log"
+EOM
+        close $fh;
+}
+
+my @uarg = qw(-D -E none t/integration.ru);
+
+# this pipe will be used to notify us when all daemons die:
+pipe(my $p0, my $p1);
+fcntl($p1, POSIX::F_SETFD, 0);
+
+# start the first instance
+unicorn('-c', "$tmpdir/u1.conf.rb", @uarg)->join;
+is($?, 0, 'daemonized 1st process');
+chomp($to_kill{u1} = slurp("$tmpdir/u.pid"));
+like($to_kill{u1}, qr/\A\d+\z/s, 'read pid file');
+
+chomp(my $worker_pid = readline(unix_start($u1, 'GET /pid')));
+like($worker_pid, qr/\A\d+\z/s, 'captured worker pid');
+ok(kill(0, $worker_pid), 'worker is kill-able');
+
+
+# 2nd process conflicts on PID
+unicorn('-c', "$tmpdir/u2.conf.rb", @uarg)->join;
+isnt($?, 0, 'conflicting PID file fails to start');
+
+chomp(my $pidf = slurp("$tmpdir/u.pid"));
+is($pidf, $to_kill{u1}, 'pid file contents unchanged after start failure');
+
+chomp(my $pid2 = readline(unix_start($u1, 'GET /pid')));
+is($worker_pid, $pid2, 'worker PID unchanged');
+
+
+# 3rd process conflicts on socket
+unicorn('-c', "$tmpdir/u3.conf.rb", @uarg)->join;
+isnt($?, 0, 'conflicting UNIX socket fails to start');
+
+chomp($pid2 = readline(unix_start($u1, 'GET /pid')));
+is($worker_pid, $pid2, 'worker PID still unchanged');
+
+chomp($pidf = slurp("$tmpdir/u.pid"));
+is($pidf, $to_kill{u1}, 'pid file contents unchanged after 2nd start failure');
+
+{ # teardown initial process via SIGKILL
+        ok(kill('KILL', delete $to_kill{u1}), 'SIGKILL initial daemon');
+        close $p1;
+        vec(my $rvec = '', fileno($p0), 1) = 1;
+        is(select($rvec, undef, undef, 5), 1, 'timeout for pipe HUP');
+        is(my $undef = <$p0>, undef, 'process closed pipe writer at exit');
+        ok(-f "$tmpdir/u.pid", 'pid file stayed after SIGKILL');
+        ok(-S $u1, 'socket stayed after SIGKILL');
+        is(IO::Socket::UNIX->new(Peer => $u1, Type => SOCK_STREAM), undef,
+                'fail to connect to u1');
+        for (1..50) { # wait for init process to reap worker
+                kill(0, $worker_pid) or last;
+                sleep 0.011;
+        }
+        ok(!kill(0, $worker_pid), 'worker gone after parent dies');
+}
+
+# restart the first instance
+{
+        pipe($p0, $p1);
+        fcntl($p1, POSIX::F_SETFD, 0);
+        unicorn('-c', "$tmpdir/u1.conf.rb", @uarg)->join;
+        is($?, 0, 'daemonized 1st process');
+        chomp($to_kill{u1} = slurp("$tmpdir/u.pid"));
+        like($to_kill{u1}, qr/\A\d+\z/s, 'read pid file');
+
+        chomp($pid2 = readline(unix_start($u1, 'GET /pid')));
+        like($pid2, qr/\A\d+\z/, 'worker running');
+
+        ok(kill('TERM', delete $to_kill{u1}), 'SIGTERM restarted daemon');
+        close $p1;
+        vec(my $rvec = '', fileno($p0), 1) = 1;
+        is(select($rvec, undef, undef, 5), 1, 'timeout for pipe HUP');
+        is(my $undef = <$p0>, undef, 'process closed pipe writer at exit');
+        ok(!-f "$tmpdir/u.pid", 'pid file gone after SIGTERM');
+        ok(-S $u1, 'socket stays after SIGTERM');
+}
+
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/back-out-of-upgrade.t b/t/back-out-of-upgrade.t
new file mode 100644
index 0000000..cf3b09f
--- /dev/null
+++ b/t/back-out-of-upgrade.t
@@ -0,0 +1,44 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+# test backing out of USR2 upgrade
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+my $srv = tcp_server();
+mkfifo_die $fifo;
+write_file '>', $u_conf, <<EOM;
+preload_app true
+stderr_path "$err_log"
+pid "$pid_file"
+after_fork { |s,w| File.open('$fifo', 'w') { |fp| fp.write "pid=#\$\$" } }
+EOM
+my $ar = unicorn(qw(-E none t/pid.ru -c), $u_conf, { 3 => $srv });
+
+like(my $wpid_orig_1 = slurp($fifo), qr/\Apid=\d+\z/a, 'got worker pid');
+
+ok $ar->do_kill('USR2'), 'USR2 to start upgrade';
+ok $ar->do_kill('WINCH'), 'drop old worker';
+
+like(my $wpid_new = slurp($fifo), qr/\Apid=\d+\z/a, 'got pid from new master');
+chomp(my $new_pid = slurp($pid_file));
+isnt $new_pid, $ar->{pid}, 'PID file changed';
+chomp(my $pid_oldbin = slurp("$pid_file.oldbin"));
+is $pid_oldbin, $ar->{pid}, '.oldbin PID valid';
+
+ok $ar->do_kill('HUP'), 'HUP old master';
+like(my $wpid_orig_2 = slurp($fifo), qr/\Apid=\d+\z/a, 'got worker new pid');
+ok kill('QUIT', $new_pid), 'abort old master';
+kill_until_dead $new_pid;
+
+my ($st, $hdr, $req_pid) = do_req $srv, 'GET /';
+chomp $req_pid;
+is $wpid_orig_2, "pid=$req_pid", 'new worker on old worker serves';
+
+ok !-f "$pid_file.oldbin", '.oldbin PID file gone';
+chomp(my $old_pid = slurp($pid_file));
+is $old_pid, $ar->{pid}, 'PID file restored';
+
+my @log = grep !/ERROR -- : reaped .*? exec\(\)-ed/, slurp($err_log);
+check_stderr @log;
+undef $tmpdir;
+done_testing;
diff --git a/t/bin/content-md5-put b/t/bin/content-md5-put
deleted file mode 100755
index 01da0bb..0000000
--- a/t/bin/content-md5-put
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env ruby
-# -*- encoding: binary -*-
-# simple chunked HTTP PUT request generator (and just that),
-# it reads stdin and writes to stdout so socat can write to a
-# UNIX or TCP socket (or to another filter or file) along with
-# a Content-MD5 trailer.
-require 'digest/md5'
-$stdout.sync = $stderr.sync = true
-$stdout.binmode
-$stdin.binmode
-
-bs = ENV['bs'] ? ENV['bs'].to_i : 4096
-
-if ARGV.grep("--no-headers").empty?
-  $stdout.write(
-      "PUT / HTTP/1.1\r\n" \
-      "Host: example.com\r\n" \
-      "Transfer-Encoding: chunked\r\n" \
-      "Trailer: Content-MD5\r\n" \
-      "\r\n"
-    )
-end
-
-digest = Digest::MD5.new
-if buf = $stdin.readpartial(bs)
-  begin
-    digest.update(buf)
-    $stdout.write("%x\r\n" % [ buf.size ])
-    $stdout.write(buf)
-    $stdout.write("\r\n")
-  end while $stdin.read(bs, buf)
-end
-
-digest = [ digest.digest ].pack('m').strip
-$stdout.write("0\r\n")
-$stdout.write("Content-MD5: #{digest}\r\n\r\n")
diff --git a/t/bin/sha1sum.rb b/t/bin/sha1sum.rb
deleted file mode 100755
index 53d68ce..0000000
--- a/t/bin/sha1sum.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env ruby
-# -*- encoding: binary -*-
-# Reads from stdin and outputs the SHA1 hex digest of the input
-
-require 'digest/sha1'
-$stdout.sync = $stderr.sync = true
-$stdout.binmode
-$stdin.binmode
-bs = 16384
-digest = Digest::SHA1.new
-if buf = $stdin.read(bs)
-  begin
-    digest.update(buf)
-  end while $stdin.read(bs, buf)
-end
-
-$stdout.syswrite("#{digest.hexdigest}\n")
diff --git a/t/broken-app.ru b/t/broken-app.ru
index d05d7ab..5966bff 100644
--- a/t/broken-app.ru
+++ b/t/broken-app.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # we do not want Rack::Lint or anything to protect us
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
diff --git a/t/t0116.ru b/t/client_body_buffer_size.ru
index fab5fce..1a0fb16 100644
--- a/t/t0116.ru
+++ b/t/client_body_buffer_size.ru
@@ -1,6 +1,5 @@
 #\ -E none
-use Rack::ContentLength
-use Rack::ContentType, 'text/plain'
+# frozen_string_literal: false
 app = lambda do |env|
   input = env['rack.input']
   case env["PATH_INFO"]
diff --git a/t/client_body_buffer_size.t b/t/client_body_buffer_size.t
new file mode 100644
index 0000000..d479901
--- /dev/null
+++ b/t/client_body_buffer_size.t
@@ -0,0 +1,80 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+open my $conf_fh, '>', $u_conf;
+$conf_fh->autoflush(1);
+print $conf_fh <<EOM;
+client_body_buffer_size 0
+EOM
+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);
+my $ar = unicorn(@uarg, { 3 => $srv });
+my ($c, $status, $hdr);
+my $mem_class = 'StringIO';
+my $fs_class = 'Unicorn::TmpIO';
+
+$c = tcp_start($srv, "PUT /input_class HTTP/1.0\r\nContent-Length: 0");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $mem_class, 'zero-byte file is StringIO');
+
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: 1");
+print $c '.';
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $fs_class, '1 byte file is filesystem-backed');
+
+
+my $fifo = "$tmpdir/fifo";
+POSIX::mkfifo($fifo, 0600) or die "mkfifo: $!";
+seek($conf_fh, 0, SEEK_SET);
+truncate($conf_fh, 0);
+print $conf_fh <<EOM;
+after_fork { |_,_| File.open('$fifo', 'w') { |fp| fp.write "pid=#\$\$" } }
+EOM
+$ar->do_kill('HUP');
+open my $fifo_fh, '<', $fifo;
+like(my $wpid = readline($fifo_fh), qr/\Apid=\d+\z/a ,
+        'reloaded w/ default client_body_buffer_size');
+
+
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: 1");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $mem_class, 'class for a 1 byte file is memory-backed');
+
+
+my $one_meg = 1024 ** 2;
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $one_meg");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $fs_class, '1 megabyte file is FS-backed');
+
+# reload with bigger client_body_buffer_size
+say $conf_fh "client_body_buffer_size $one_meg";
+$ar->do_kill('HUP');
+open $fifo_fh, '<', $fifo;
+like($wpid = readline($fifo_fh), qr/\Apid=\d+\z/a ,
+        'reloaded w/ bigger client_body_buffer_size');
+
+
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $one_meg");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $mem_class, '1 megabyte file is now memory-backed');
+
+my $too_big = $one_meg + 1;
+$c = tcp_start($srv, "PUT /tmp_class HTTP/1.0\r\nContent-Length: $too_big");
+($status, $hdr) = slurp_hdr($c);
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+is(readline($c), $fs_class, '1 megabyte + 1 byte file is FS-backed');
+
+
+undef $ar;
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/detach.ru b/t/detach.ru
index bbd998e..8d35951 100644
--- a/t/detach.ru
+++ b/t/detach.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentType, "text/plain"
 fifo_path = ENV["TEST_FIFO"] or abort "TEST_FIFO not set"
 run lambda { |env|
diff --git a/t/env.ru b/t/env.ru
index 388412e..86c3cfa 100644
--- a/t/env.ru
+++ b/t/env.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 run lambda { |env| [ 200, {}, [ env.inspect << "\n" ] ] }
diff --git a/t/fails-rack-lint.ru b/t/fails-rack-lint.ru
index 82bfb5f..8b8b5ec 100644
--- a/t/fails-rack-lint.ru
+++ b/t/fails-rack-lint.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 # This rack app returns an invalid status code, which will cause
 # Rack::Lint to throw an exception if it is present.  This
 # is used to check whether Rack::Lint is in the stack or not.
diff --git a/t/heartbeat-timeout.ru b/t/heartbeat-timeout.ru
index d9904e8..ccc6a8e 100644
--- a/t/heartbeat-timeout.ru
+++ b/t/heartbeat-timeout.ru
@@ -1,5 +1,6 @@
+# frozen_string_literal: false
 use Rack::ContentLength
-headers = { 'Content-Type' => 'text/plain' }
+headers = { 'content-type' => 'text/plain' }
 run lambda { |env|
   case env['PATH_INFO']
   when "/block-forever"
@@ -7,6 +8,6 @@ run lambda { |env|
     sleep # in case STOP signal is not received in time
     [ 500, headers, [ "Should never get here\n" ] ]
   else
-    [ 200, headers, [ "#$$\n" ] ]
+    [ 200, headers, [ "#$$" ] ]
   end
 }
diff --git a/t/heartbeat-timeout.t b/t/heartbeat-timeout.t
new file mode 100644
index 0000000..694867a
--- /dev/null
+++ b/t/heartbeat-timeout.t
@@ -0,0 +1,62 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+use v5.14; BEGIN { require './t/lib.perl' };
+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;
+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 });
+
+my ($status, $hdr, $wpid) = do_req($srv, 'GET /pid HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'PID request succeeds');
+like($wpid, qr/\A[0-9]+\z/, 'worker is running');
+
+my $t0 = clock_gettime(CLOCK_MONOTONIC);
+my $c = tcp_start($srv, 'GET /block-forever HTTP/1.0');
+vec(my $rvec = '', fileno($c), 1) = 1;
+is(select($rvec, undef, undef, 6), 1, 'got readiness');
+$c->blocking(0);
+is(sysread($c, my $buf, 128), 0, 'got EOF response');
+my $elapsed = clock_gettime(CLOCK_MONOTONIC) - $t0;
+ok($elapsed > 3, 'timeout took >3s');
+
+my @timeout_err = slurp($err_log);
+truncate($err_log, 0);
+is(grep(/timeout \(\d+s > 3s\), killing/, @timeout_err), 1,
+    'noted timeout error') or diag explain(\@timeout_err);
+
+# did it respawn?
+($status, $hdr, my $new_pid) = do_req($srv, 'GET /pid HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'PID request succeeds');
+isnt($new_pid, $wpid, 'spawned new worker');
+
+diag 'SIGSTOP for 4 seconds...';
+$ar->do_kill('STOP');
+sleep 4;
+$ar->do_kill('CONT');
+for my $i (1..2) {
+        ($status, $hdr, my $spid) = do_req($srv, 'GET /pid HTTP/1.0');
+        like($status, qr!\AHTTP/1\.[01] 200\b!,
+                "PID request succeeds #$i after STOP+CONT");
+        is($new_pid, $spid, "worker pid unchanged after STOP+CONT #$i");
+        if ($i == 1) {
+                diag 'sleeping 2s to ensure timeout is not delayed';
+                sleep 2;
+        }
+}
+
+$ar->join('TERM');
+check_stderr;
+undef $tmpdir;
+
+done_testing;
diff --git a/t/hijack.ru b/t/hijack.ru
deleted file mode 100644
index 02260e2..0000000
--- a/t/hijack.ru
+++ /dev/null
@@ -1,55 +0,0 @@
-use Rack::Lint
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-class DieIfUsed
-  @@n = 0
-  def each
-    abort "body.each called after response hijack\n"
-  end
-
-  def close
-    warn "closed DieIfUsed #{@@n += 1}\n"
-  end
-end
-
-envs = []
-
-run lambda { |env|
-  case env["PATH_INFO"]
-  when "/hijack_req"
-    if env["rack.hijack?"]
-      io = env["rack.hijack"].call
-      envs << env
-      if io.respond_to?(:read_nonblock) &&
-         env["rack.hijack_io"].respond_to?(:read_nonblock)
-
-        # exercise both, since we Rack::Lint may use different objects
-        env["rack.hijack_io"].write("HTTP/1.0 200 OK\r\n\r\n")
-        io.write("request.hijacked")
-        io.close
-        return [ 500, {}, DieIfUsed.new ]
-      end
-    end
-    [ 500, {}, [ "hijack BAD\n" ] ]
-  when "/hijack_res"
-    r = "response.hijacked"
-    [ 200,
-      {
-        "Content-Length" => r.bytesize.to_s,
-        "rack.hijack" => proc do |io|
-          envs << env
-          io.write(r)
-          io.close
-        end
-      },
-      DieIfUsed.new
-    ]
-  when "/normal_env_id"
-    b = "#{env.object_id}\n"
-    h = {
-      'Content-Type' => 'text/plain',
-      'Content-Length' => b.bytesize.to_s,
-    }
-    [ 200, h, [ b ] ]
-  end
-}
diff --git a/t/integration.ru b/t/integration.ru
new file mode 100644
index 0000000..6df481c
--- /dev/null
+++ b/t/integration.ru
@@ -0,0 +1,116 @@
+#!ruby
+# frozen_string_literal: false
+# Copyright (C) unicorn hackers <unicorn-public@80x24.org>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+# this goes for t/integration.t  We'll try to put as many tests
+# in here as possible to avoid startup overhead of Ruby.
+
+def early_hints(env, val)
+  env['rack.early_hints'].call('link' => val) # val may be ary or string
+  [ 200, {}, [ val.class.to_s ] ]
+end
+
+$orig_rack_200 = nil
+def tweak_status_code
+  $orig_rack_200 = Rack::Utils::HTTP_STATUS_CODES[200]
+  Rack::Utils::HTTP_STATUS_CODES[200] = "HI"
+  [ 200, {}, [] ]
+end
+
+def restore_status_code
+  $orig_rack_200 or return [ 500, {}, [] ]
+  Rack::Utils::HTTP_STATUS_CODES[200] = $orig_rack_200
+  [ 200, {}, [] ]
+end
+
+class WriteOnClose
+  def each(&block)
+    @callback = block
+  end
+
+  def close
+    @callback.call "7\r\nGoodbye\r\n0\r\n\r\n"
+  end
+end
+
+def write_on_close
+  [ 200, { 'transfer-encoding' => 'chunked' }, WriteOnClose.new ]
+end
+
+def env_dump(env)
+  require 'json'
+  h = {}
+  env.each do |k,v|
+    case v
+    when String, Integer, true, false; h[k] = v
+    else
+      case k
+      when 'rack.version', 'rack.after_reply'; h[k] = v
+      end
+    end
+  end
+  h.to_json
+end
+
+def rack_input_tests(env)
+  return [ 100, {}, [] ] if /\A100-continue\z/i =~ env['HTTP_EXPECT']
+  cap = 16384
+  require 'digest/md5'
+  dig = Digest::MD5.new
+  input = env['rack.input']
+  case env['PATH_INFO']
+  when '/rack_input/size_first'; input.size
+  when '/rack_input/rewind_first'; input.rewind
+  when '/rack_input'; # OK
+  else
+    abort "bad path: #{env['PATH_INFO']}"
+  end
+  if buf = input.read(rand(cap))
+    begin
+      raise "#{buf.size} > #{cap}" if buf.size > cap
+      dig.update(buf)
+    end while input.read(rand(cap), buf)
+    buf.clear # remove this call if Ruby ever gets escape analysis
+  end
+  h = { 'content-type' => 'text/plain' }
+  if env['HTTP_TRAILER'] =~ /\bContent-MD5\b/i
+    cmd5_b64 = env['HTTP_CONTENT_MD5'] or return [500, {}, ['No Content-MD5']]
+    cmd5_bin = cmd5_b64.unpack('m')[0]
+    if cmd5_bin != dig.digest
+      h['content-length'] = cmd5_b64.size.to_s
+      return [ 500, h, [ cmd5_b64 ] ]
+    end
+  end
+  h['content-length'] = '32'
+  [ 200, h, [ dig.hexdigest ] ]
+end
+
+run(lambda do |env|
+  case env['REQUEST_METHOD']
+  when 'GET'
+    case env['PATH_INFO']
+    when '/rack-2-newline-headers'; [ 200, { 'X-R2' => "a\nb\nc" }, [] ]
+    when '/rack-3-array-headers'; [ 200, { 'x-r3' => %w(a b c) }, [] ]
+    when '/nil-header-value'; [ 200, { 'X-Nil' => nil }, [] ]
+    when '/unknown-status-pass-through'; [ '666 I AM THE BEAST', {}, [] ]
+    when '/env_dump'; [ 200, {}, [ env_dump(env) ] ]
+    when '/write_on_close'; write_on_close
+    when '/pid'; [ 200, {}, [ "#$$\n" ] ]
+    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'
+    else '/'; [ 200, {}, [ env_dump(env) ] ]
+    end # case PATH_INFO (GET)
+  when 'POST'
+    case env['PATH_INFO']
+    when '/tweak-status-code'; tweak_status_code
+    when '/restore-status-code'; restore_status_code
+    end # case PATH_INFO (POST)
+    # ...
+  when 'PUT'
+    case env['PATH_INFO']
+    when %r{\A/rack_input}; rack_input_tests(env)
+    end
+  end # case REQUEST_METHOD
+end) # run
diff --git a/t/integration.t b/t/integration.t
new file mode 100644
index 0000000..d17ace0
--- /dev/null
+++ b/t/integration.t
@@ -0,0 +1,357 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+
+# This is the main integration test for fast-ish things to minimize
+# Ruby startup time penalties.
+
+use v5.14; BEGIN { require './t/lib.perl' };
+use autodie;
+use Socket qw(SOL_SOCKET SO_KEEPALIVE SHUT_WR);
+our $srv = tcp_server();
+our $host_port = tcp_host_port($srv);
+
+if ('ensure Perl does not set SO_KEEPALIVE by default') {
+        my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
+        unpack('i', $val) == 0 or
+                setsockopt($srv, SOL_SOCKET, SO_KEEPALIVE, pack('i', 0));
+        $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;
+early_hints true
+listen "$u1"
+EOM
+my $ar = unicorn(qw(-E none t/integration.ru -c), $u_conf, { 3 => $srv });
+my $curl = which('curl');
+local $ENV{NO_PROXY} = '*'; # for curl
+my $fifo = "$tmpdir/fifo";
+POSIX::mkfifo($fifo, 0600) or die "mkfifo: $!";
+my %PUT = (
+        chunked_md5 => sub {
+                my ($in, $out, $path, %opt) = @_;
+                my $dig = Digest::MD5->new;
+                print $out <<EOM;
+PUT $path HTTP/1.1\r
+Transfer-Encoding: chunked\r
+Trailer: Content-MD5\r
+\r
+EOM
+                my ($buf, $r);
+                while (1) {
+                        $r = read($in, $buf, 999 + int(rand(0xffff)));
+                        last if $r == 0;
+                        printf $out "%x\r\n", length($buf);
+                        print $out $buf, "\r\n";
+                        $dig->add($buf);
+                }
+                print $out "0\r\nContent-MD5: ", $dig->b64digest, "\r\n\r\n";
+        },
+        identity => sub {
+                my ($in, $out, $path, %opt) = @_;
+                my $clen = $opt{-s} // -s $in;
+                print $out <<EOM;
+PUT $path HTTP/1.0\r
+Content-Length: $clen\r
+\r
+EOM
+                my ($buf, $r, $len, $bs);
+                while ($clen) {
+                        $bs = 999 + int(rand(0xffff));
+                        $len = $clen > $bs ? $bs : $clen;
+                        $r = read($in, $buf, $len);
+                        die 'premature EOF' if $r == 0;
+                        print $out $buf;
+                        $clen -= $r;
+                }
+        },
+);
+
+my ($c, $status, $hdr, $bdy);
+
+# response header tests
+($status, $hdr) = do_req($srv, 'GET /rack-2-newline-headers HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid');
+my $orig_200_status = $status;
+is_deeply([ grep(/^X-R2: /, @$hdr) ],
+        [ 'X-R2: a', 'X-R2: b', 'X-R2: c' ],
+        'rack 2 LF-delimited headers supported') or diag(explain($hdr));
+
+{
+        my $val = getsockopt($srv, SOL_SOCKET, SO_KEEPALIVE);
+        is(unpack('i', $val), 1, 'SO_KEEPALIVE set on inherited socket');
+}
+
+SKIP: { # Date header check
+        my @d = grep(/^Date: /i, @$hdr);
+        is(scalar(@d), 1, 'got one date header') or diag(explain(\@d));
+        eval { require HTTP::Date } or skip "HTTP::Date missing: $@", 1;
+        $d[0] =~ s/^Date: //i or die 'BUG: did not strip date: prefix';
+        my $t = HTTP::Date::str2time($d[0]);
+        my $now = time;
+        ok($t >= ($t0 - 1) && $t > 0 && $t <= ($now + 1), 'valid date') or
+                diag(explain(["t=$t t0=$t0 now=$now", $!, \@d]));
+};
+
+
+($status, $hdr) = do_req($srv, 'GET /rack-3-array-headers HTTP/1.0');
+is_deeply([ grep(/^x-r3: /, @$hdr) ],
+        [ 'x-r3: a', 'x-r3: b', 'x-r3: c' ],
+        'rack 3 array headers supported') or diag(explain($hdr));
+
+SKIP: {
+        eval { require JSON::PP } or skip "JSON::PP missing: $@", 1;
+        ($status, $hdr, my $json) = do_req $srv, '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');
+}
+
+# cf. <CAO47=rJa=zRcLn_Xm4v2cHPr6c0UswaFC_omYFEH+baSxHOWKQ@mail.gmail.com>
+($status, $hdr) = do_req($srv, 'GET /nil-header-value HTTP/1.0');
+is_deeply([grep(/^X-Nil:/, @$hdr)], ['X-Nil: '],
+        'nil header value accepted for broken apps') or diag(explain($hdr));
+
+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);
+
+my $ck_early_hints = sub {
+        my ($note) = @_;
+        $c = unix_start($u1, 'GET /early_hints_rack2 HTTP/1.0');
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 2 value');
+        is_deeply(['link: r', 'link: 2'], $hdr, 'rack 2 hints match '.$note);
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards');
+        is(readline($c), 'String', 'early hints used a String for rack 2');
+
+        $c = unix_start($u1, 'GET /early_hints_rack3 HTTP/1.0');
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 103\b!, 'got 103 for rack 3');
+        is_deeply(['link: r', 'link: 3'], $hdr, 'rack 3 hints match '.$note);
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 200\b!, 'got 200 afterwards');
+        is(readline($c), 'Array', 'early hints used a String for rack 3');
+};
+$ck_early_hints->('ccc off'); # we'll retest later
+
+if ('TODO: ensure Rack::Utils::HTTP_STATUS_CODES is available') {
+        ($status, $hdr) = do_req $srv, 'POST /tweak-status-code HTTP/1.0';
+        like($status, qr!\AHTTP/1\.[01] 200 HI\b!, 'status tweaked');
+
+        ($status, $hdr) = do_req $srv, 'POST /restore-status-code HTTP/1.0';
+        is($status, $orig_200_status, 'original status restored');
+}
+
+SKIP: {
+        eval { require HTTP::Tiny } or skip "HTTP::Tiny missing: $@", 1;
+        my $ht = HTTP::Tiny->new;
+        my $res = $ht->get("http://$host_port/write_on_close");
+        is($res->{content}, 'Goodbye', 'write-on-close body read');
+}
+
+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');
+
+        $c = tcp_start($srv);
+        print $c 'GET /';
+        my $buf = join('', (0..9), 'ab');
+        for (0..1023) { print $c $buf }
+        print $c " HTTP/1.0\r\n\r\n";
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 414 \b!,
+                '414 on REQUEST_PATH > (12 * 1024)');
+
+        $c = tcp_start($srv);
+        print $c 'GET /hello-world?a';
+        $buf = join('', (0..9));
+        for (0..1023) { print $c $buf }
+        print $c " HTTP/1.0\r\n\r\n";
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 414 \b!,
+                '414 on QUERY_STRING > (10 * 1024)');
+
+        $c = tcp_start($srv);
+        print $c 'GET /hello-world#a';
+        $buf = join('', (0..9), 'a'..'f');
+        for (0..63) { print $c $buf }
+        print $c " HTTP/1.0\r\n\r\n";
+        ($status, $hdr) = slurp_hdr($c);
+        like($status, qr!\AHTTP/1\.[01] 414 \b!, '414 on FRAGMENT > (1024)');
+}
+
+# input tests
+my ($blob_size, $blob_hash);
+SKIP: {
+        skip 'SKIP_EXPENSIVE on', 1 if $ENV{SKIP_EXPENSIVE};
+        CORE::open(my $rh, '<', 't/random_blob') or
+                skip "t/random_blob not generated $!", 1;
+        $blob_size = -s $rh;
+        require Digest::MD5;
+        $blob_hash = Digest::MD5->new->addfile($rh)->hexdigest;
+
+        my $ck_hash = sub {
+                my ($sub, $path, %opt) = @_;
+                seek($rh, 0, SEEK_SET);
+                $c = tcp_start($srv);
+                $c->autoflush($opt{sync} // 0);
+                $PUT{$sub}->($rh, $c, $path, %opt);
+                defined($opt{overwrite}) and
+                        print { $c } ('x' x $opt{overwrite});
+                $c->flush or die $!;
+                shutdown($c, SHUT_WR);
+                ($status, $hdr) = slurp_hdr($c);
+                is(readline($c), $blob_hash, "$sub $path");
+        };
+        $ck_hash->('identity', '/rack_input', -s => $blob_size);
+        $ck_hash->('chunked_md5', '/rack_input');
+        $ck_hash->('identity', '/rack_input/size_first', -s => $blob_size);
+        $ck_hash->('identity', '/rack_input/rewind_first', -s => $blob_size);
+        $ck_hash->('chunked_md5', '/rack_input/size_first');
+        $ck_hash->('chunked_md5', '/rack_input/rewind_first');
+
+        $ck_hash->('identity', '/rack_input', -s => $blob_size, sync => 1);
+        $ck_hash->('chunked_md5', '/rack_input', sync => 1);
+
+        # ensure small overwrites don't get checksummed
+        $ck_hash->('identity', '/rack_input', -s => $blob_size,
+                        overwrite => 1); # one extra byte
+        unlike(slurp($err_log), qr/ClientShutdown/,
+                'no overreads after client SHUT_WR');
+
+        # excessive overwrite truncated
+        $c = tcp_start($srv);
+        $c->autoflush(0);
+        print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 1\r\n\r\n";
+        if (1) {
+                local $SIG{PIPE} = 'IGNORE';
+                my $buf = "\0" x 8192;
+                my $n = 0;
+                my $end = time + 5;
+                $! = 0;
+                while (print $c $buf and time < $end) { ++$n }
+                ok($!, 'overwrite truncated') or diag "n=$n err=$! ".time;
+                undef $c;
+        }
+
+        # client shutdown early
+        $c = tcp_start($srv);
+        $c->autoflush(0);
+        print $c "PUT /rack_input HTTP/1.0\r\nContent-Length: 16384\r\n\r\n";
+        if (1) {
+                local $SIG{PIPE} = 'IGNORE';
+                print $c 'too short body';
+                shutdown($c, SHUT_WR);
+                vec(my $rvec = '', fileno($c), 1) = 1;
+                select($rvec, undef, undef, 10) or BAIL_OUT "timed out";
+                my $buf = <$c>;
+                is($buf, undef, 'server aborted after client SHUT_WR');
+                undef $c;
+        }
+
+        $curl // skip 'no curl found in PATH', 1;
+
+        my ($copt, $cout);
+        my $url = "http://$host_port/rack_input";
+        my $do_curl = sub {
+                my (@arg) = @_;
+                pipe(my $cout, $copt->{1});
+                open $copt->{2}, '>', "$tmpdir/curl.err";
+                my $cpid = spawn($curl, '-sSf', @arg, $url, $copt);
+                close(delete $copt->{1});
+                is(readline($cout), $blob_hash, "curl @arg response");
+                is(waitpid($cpid, 0), $cpid, "curl @arg exited");
+                is($?, 0, "no error from curl @arg");
+                is(slurp("$tmpdir/curl.err"), '', "no stderr from curl @arg");
+        };
+
+        $do_curl->(qw(-T t/random_blob));
+
+        seek($rh, 0, SEEK_SET);
+        $copt->{0} = $rh;
+        $do_curl->('-T-');
+
+        diag 'testing Unicorn::PrereadInput...';
+        local $srv = tcp_server();
+        local $host_port = tcp_host_port($srv);
+        check_stderr;
+        truncate($errfh, 0);
+
+        my $pri = unicorn(qw(-E none t/preread_input.ru), { 3 => $srv });
+        $url = "http://$host_port/";
+
+        $do_curl->(qw(-T t/random_blob));
+        seek($rh, 0, SEEK_SET);
+        $copt->{0} = $rh;
+        $do_curl->('-T-');
+
+        my @pr_err = slurp("$tmpdir/err.log");
+        is(scalar(grep(/app dispatch:/, @pr_err)), 2, 'app dispatched twice');
+
+        # abort a chunked request by blocking curl on a FIFO:
+        $c = tcp_start($srv, "PUT / HTTP/1.1\r\nTransfer-Encoding: chunked");
+        close $c;
+        @pr_err = slurp("$tmpdir/err.log");
+        is(scalar(grep(/app dispatch:/, @pr_err)), 2,
+                        'app did not dispatch on aborted request');
+        undef $pri;
+        check_stderr;
+        diag 'Unicorn::PrereadInput middleware tests done';
+}
+
+# ... more stuff here
+
+# SIGHUP-able stuff goes here
+
+if ('check_client_connection') {
+        print $conf_fh <<EOM; # appending to existing
+check_client_connection true
+after_fork { |_,_| File.open('$fifo', 'w') { |fp| fp.write "pid=#\$\$" } }
+EOM
+        $ar->do_kill('HUP');
+        open my $fifo_fh, '<', $fifo;
+        my $wpid = readline($fifo_fh);
+        like($wpid, qr/\Apid=\d+\z/a , 'new worker ready');
+        $ck_early_hints->('ccc on');
+}
+
+if ('max_header_len internal API') {
+        undef $c;
+        my $req = 'GET / HTTP/1.0';
+        my $len = length($req."\r\n\r\n");
+        print $conf_fh <<EOM; # appending to existing
+Unicorn::HttpParser.max_header_len = $len
+EOM
+        $ar->do_kill('HUP');
+        open my $fifo_fh, '<', $fifo;
+        my $wpid = readline($fifo_fh);
+        like($wpid, qr/\Apid=\d+\z/a , 'new worker ready');
+        close $fifo_fh;
+        $wpid =~ s/\Apid=// or die;
+        ok(CORE::kill(0, $wpid), 'worker PID retrieved');
+
+        ($status, $hdr) = do_req($srv, $req);
+        like($status, qr!\AHTTP/1\.[01] 200\b!, 'minimal request succeeds');
+
+        ($status, $hdr) = do_req($srv, 'GET /xxxxxx HTTP/1.0');
+        like($status, qr!\AHTTP/1\.[01] 413\b!, 'big request fails');
+}
+
+
+undef $ar;
+
+check_stderr;
+
+undef $tmpdir;
+done_testing;
diff --git a/t/lib.perl b/t/lib.perl
new file mode 100644
index 0000000..8c842b1
--- /dev/null
+++ b/t/lib.perl
@@ -0,0 +1,309 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@80x24.org>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+package UnicornTest;
+use v5.14;
+use parent qw(Exporter);
+use autodie;
+use Test::More;
+use Socket qw(SOMAXCONN);
+use Time::HiRes qw(sleep time);
+use IO::Socket::INET;
+use IO::Socket::UNIX;
+use Carp qw(croak);
+use POSIX qw(dup2 _exit setpgid :signal_h SEEK_SET F_SETFD);
+use File::Temp 0.19 (); # 0.19 for ->newdir
+our ($tmpdir, $errfh, $err_log, $u_sock, $u_conf, $daemon_pid,
+        $pid_file, $wtest_sock, $fifo);
+our @EXPORT = qw(unicorn slurp tcp_server tcp_start unicorn
+        $tmpdir $errfh $err_log $u_sock $u_conf $daemon_pid $pid_file
+        $wtest_sock $fifo
+        SEEK_SET tcp_host_port which spawn check_stderr unix_start slurp_hdr
+        do_req stop_daemon sleep time mkfifo_die kill_until_dead write_file);
+
+my ($base) = ($0 =~ m!\b([^/]+)\.[^\.]+\z!);
+$tmpdir = File::Temp->newdir("unicorn-$base-XXXX", TMPDIR => 1);
+
+$wtest_sock = "$tmpdir/wtest.sock";
+$err_log = "$tmpdir/err.log";
+$pid_file = "$tmpdir/pid";
+$fifo = "$tmpdir/fifo";
+$u_sock = "$tmpdir/u.sock";
+$u_conf = "$tmpdir/u.conf.rb";
+open($errfh, '>>', $err_log);
+
+if (my $t = $ENV{TAIL}) {
+        my @tail = $t =~ /tail/ ? split(/\s+/, $t) : (qw(tail -F));
+        push @tail, $err_log;
+        my $pid = fork;
+        if ($pid == 0) {
+                open STDOUT, '>&', \*STDERR;
+                exec @tail;
+                die "exec(@tail): $!";
+        }
+        say "# @tail";
+        sleep 0.2;
+        UnicornTest::AutoReap->new($pid);
+}
+
+sub kill_until_dead ($;%) {
+        my ($pid, %opt) = @_;
+        my $tries = $opt{tries} // 1000;
+        my $sig = $opt{sig} // 0;
+        while (CORE::kill($sig, $pid) && --$tries) { sleep(0.01) }
+        $tries or croak "PID: $pid died after signal ($sig)";
+}
+
+sub stop_daemon (;$) {
+        my ($is_END) = @_;
+        kill('TERM', $daemon_pid);
+        kill_until_dead $daemon_pid;
+        if ($is_END && CORE::kill(0, $daemon_pid)) { # after done_testing
+                CORE::kill('KILL', $daemon_pid);
+                die "daemon_pid=$daemon_pid did not die";
+        } else {
+                ok(!CORE::kill(0, $daemon_pid), 'daemonized unicorn gone');
+                undef $daemon_pid;
+        }
+};
+
+END {
+        diag slurp($err_log) if $tmpdir;
+        stop_daemon(1) if defined $daemon_pid;
+};
+
+sub check_stderr (@) {
+        my @log = @_;
+        slurp($err_log) if !@log;
+        diag("@log") if $ENV{V};
+        my @err = grep(!/NameError.*Unicorn::Waiter/, grep(/error/i, @log));
+        @err = grep(!/failed to set accept_filter=/, @err);
+        @err = grep(!/perhaps accf_.*? needs to be loaded/, @err);
+        is_deeply(\@err, [], 'no unexpected errors in stderr');
+        is_deeply([grep(/SIGKILL/, @log)], [], 'no SIGKILL in stderr');
+}
+
+sub slurp_hdr {
+        my ($c) = @_;
+        local $/ = "\r\n\r\n"; # affects both readline+chomp
+        chomp(my $hdr = readline($c));
+        my ($status, @hdr) = split(/\r\n/, $hdr);
+        diag explain([ $status, \@hdr ]) if $ENV{V};
+        ($status, \@hdr);
+}
+
+sub unix_server (;$@) {
+        my $l = shift // $u_sock;
+        IO::Socket::UNIX->new(Listen => SOMAXCONN, Local => $l, Blocking => 0,
+                                Type => SOCK_STREAM, @_);
+}
+
+sub unix_connect ($) {
+        IO::Socket::UNIX->new(Peer => $_[0], Type => SOCK_STREAM);
+}
+
+sub tcp_server {
+        my %opt = (
+                ReuseAddr => 1,
+                Proto => 'tcp',
+                Type => SOCK_STREAM,
+                Listen => SOMAXCONN,
+                Blocking => 0,
+                @_,
+        );
+        eval {
+                die 'IPv4-only' if $ENV{TEST_IPV4_ONLY};
+                require IO::Socket::INET6;
+                IO::Socket::INET6->new(%opt, LocalAddr => '[::1]')
+        } || eval {
+                die 'IPv6-only' if $ENV{TEST_IPV6_ONLY};
+                IO::Socket::INET->new(%opt, LocalAddr => '127.0.0.1')
+        } || BAIL_OUT "failed to create TCP server: $! ($@)";
+}
+
+sub tcp_host_port {
+        my ($s) = @_;
+        my ($h, $p) = ($s->sockhost, $s->sockport);
+        my $ipv4 = $s->sockdomain == AF_INET;
+        if (wantarray) {
+                $ipv4 ? ($h, $p) : ("[$h]", $p);
+        } else {
+                $ipv4 ? "$h:$p" : "[$h]:$p";
+        }
+}
+
+sub unix_start ($@) {
+        my ($dst, @req) = @_;
+        my $s = unix_connect($dst) or BAIL_OUT "unix connect $dst: $!";
+        $s->autoflush(1);
+        print $s @req, "\r\n\r\n" if @req;
+        $s;
+}
+
+sub tcp_start ($@) {
+        my ($dst, @req) = @_;
+        my $addr = tcp_host_port($dst);
+        my $s = ref($dst)->new(
+                Proto => 'tcp',
+                Type => SOCK_STREAM,
+                PeerAddr => $addr,
+        ) or BAIL_OUT "failed to connect to $addr: $!";
+        $s->autoflush(1);
+        print $s @req, "\r\n\r\n" if @req;
+        $s;
+}
+
+sub slurp {
+        open my $fh, '<', $_[0];
+        local $/ if !wantarray;
+        readline($fh);
+}
+
+sub spawn {
+        my $env = ref($_[0]) eq 'HASH' ? shift : undef;
+        my $opt = ref($_[-1]) eq 'HASH' ? pop : {};
+        my @cmd = @_;
+        my $old = POSIX::SigSet->new;
+        my $set = POSIX::SigSet->new;
+        $set->fillset or die "sigfillset: $!";
+        sigprocmask(SIG_SETMASK, $set, $old) or die "SIG_SETMASK: $!";
+        pipe(my $r, my $w);
+        my $pid = fork;
+        if ($pid == 0) {
+                close $r;
+                $SIG{__DIE__} = sub {
+                        warn(@_);
+                        syswrite($w, my $num = $! + 0);
+                        _exit(1);
+                };
+
+                # pretend to be systemd (cf. sd_listen_fds(3))
+                my $cfd;
+                for ($cfd = 0; ($cfd < 3) || defined($opt->{$cfd}); $cfd++) {
+                        my $io = $opt->{$cfd} // next;
+                        my $pfd = fileno($io);
+                        if ($pfd == $cfd) {
+                                fcntl($io, F_SETFD, 0);
+                        } else {
+                                dup2($pfd, $cfd) // die "dup2($pfd, $cfd): $!";
+                        }
+                }
+                if (($cfd - 3) > 0) {
+                        $env->{LISTEN_PID} = $$;
+                        $env->{LISTEN_FDS} = $cfd - 3;
+                }
+
+                if (defined(my $pgid = $opt->{pgid})) {
+                        setpgid(0, $pgid) // die "setpgid(0, $pgid): $!";
+                }
+                $SIG{$_} = 'DEFAULT' for grep(!/^__/, keys %SIG);
+                if (defined(my $cd = $opt->{-C})) { chdir $cd }
+                $old->delset(POSIX::SIGCHLD) or die "sigdelset CHLD: $!";
+                sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK: ~CHLD: $!";
+                @ENV{keys %$env} = values(%$env) if $env;
+                exec { $cmd[0] } @cmd;
+                die "exec @cmd: $!";
+        }
+        close $w;
+        sigprocmask(SIG_SETMASK, $old) or die "SIG_SETMASK(old): $!";
+        if (my $cerrnum = do { local $/, <$r> }) {
+                $! = $cerrnum;
+                die "@cmd PID=$pid died: $!";
+        }
+        $pid;
+}
+
+sub which {
+        my ($file) = @_;
+        return $file if index($file, '/') >= 0;
+        for my $p (split(/:/, $ENV{PATH})) {
+                $p .= "/$file";
+                return $p if -x $p;
+        }
+        undef;
+}
+
+# returns an AutoReap object
+sub unicorn {
+        my %env;
+        if (ref($_[0]) eq 'HASH') {
+                my $e = shift;
+                %env = %$e;
+        }
+        my @args = @_;
+        push(@args, {}) if ref($args[-1]) ne 'HASH';
+        $args[-1]->{2} //= $errfh; # stderr default
+
+        state $ruby = which($ENV{RUBY} // 'ruby');
+        state $lib = File::Spec->rel2abs('lib');
+        state $ver = $ENV{TEST_RUBY_VERSION} // `$ruby -e 'print RUBY_VERSION'`;
+        state $eng = $ENV{TEST_RUBY_ENGINE} // `$ruby -e 'print RUBY_ENGINE'`;
+        state $ext = File::Spec->rel2abs("test/$eng-$ver/ext/unicorn_http");
+        state $exe = File::Spec->rel2abs("test/$eng-$ver/bin/unicorn");
+        state $rl = $ENV{RUBYLIB} ? "$lib:$ext:$ENV{RUBYLIB}" : "$lib:$ext";
+        $env{RUBYLIB} = $rl;
+        my $pid = spawn(\%env, $ruby, $exe, @args);
+        UnicornTest::AutoReap->new($pid);
+}
+
+sub do_req ($@) {
+        my ($dst, @req) = @_;
+        my $c = ref($dst) ? tcp_start($dst, @req) : unix_start($dst, @req);
+        return $c if !wantarray;
+        my ($status, $hdr);
+        # read headers iff HTTP/1.x request, HTTP/0.9 remains supported
+        my ($first) = (join('', @req) =~ m!\A([^\r\n]+)!);
+        ($status, $hdr) = slurp_hdr($c) if $first =~ m{\s*HTTP/\S+$};
+        my $bdy = do { local $/; <$c> };
+        close $c;
+        ($status, $hdr, $bdy);
+}
+
+sub mkfifo_die ($;$) {
+        POSIX::mkfifo($_[0], $_[1] // 0600) or croak "mkfifo: $!";
+}
+
+sub write_file ($$@) { # mode, filename, LIST (for print)
+        open(my $fh, shift, shift);
+        print $fh @_;
+        # return $fh for futher writes if user wants it:
+        defined(wantarray) && !wantarray ? $fh : close $fh;
+}
+
+# automatically kill + reap children when this goes out-of-scope
+package UnicornTest::AutoReap;
+use v5.14;
+use autodie;
+
+sub new {
+        my (undef, $pid) = @_;
+        bless { pid => $pid, owner => $$ }, __PACKAGE__
+}
+
+sub do_kill {
+        my ($self, $sig) = @_;
+        kill($sig // 'TERM', $self->{pid});
+}
+
+sub join {
+        my ($self, $sig) = @_;
+        my $pid = delete $self->{pid} or return;
+        kill($sig, $pid) if defined $sig;
+        my $ret = waitpid($pid, 0);
+        $ret == $pid or die "BUG: waitpid($pid) != $ret";
+}
+
+sub DESTROY {
+        my ($self) = @_;
+        return if $self->{owner} != $$;
+        $self->join('TERM');
+}
+
+package main; # inject ourselves into the t/*.t script
+UnicornTest->import;
+Test::More->import;
+# try to ensure ->DESTROY fires:
+$SIG{TERM} = sub { exit(15 + 128) };
+$SIG{INT} = sub { exit(2 + 128) };
+$SIG{PIPE} = sub { exit(13 + 128) };
+1;
diff --git a/t/listener_names.ru b/t/listener_names.ru
index edb4e6a..f52c59b 100644
--- a/t/listener_names.ru
+++ b/t/listener_names.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 names = Unicorn.listener_names.inspect # rely on preload_app=true
diff --git a/t/oob_gc.ru b/t/oob_gc.ru
index c253540..2ae58a8 100644
--- a/t/oob_gc.ru
+++ b/t/oob_gc.ru
@@ -1,4 +1,5 @@
 #\-E none
+# frozen_string_literal: false
 require 'unicorn/oob_gc'
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
@@ -7,9 +8,6 @@ $gc_started = false
 
 # Mock GC.start
 def GC.start
-  ObjectSpace.each_object(Kgio::Socket) do |x|
-    x.closed? or abort "not closed #{x}"
-  end
   $gc_started = true
 end
 run lambda { |env|
diff --git a/t/oob_gc_path.ru b/t/oob_gc_path.ru
index af8e3b9..5358222 100644
--- a/t/oob_gc_path.ru
+++ b/t/oob_gc_path.ru
@@ -1,4 +1,5 @@
 #\-E none
+# frozen_string_literal: false
 require 'unicorn/oob_gc'
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
@@ -7,9 +8,6 @@ $gc_started = false
 
 # Mock GC.start
 def GC.start
-  ObjectSpace.each_object(Kgio::Socket) do |x|
-    x.closed? or abort "not closed #{x}"
-  end
   $gc_started = true
 end
 run lambda { |env|
diff --git a/t/pid.ru b/t/pid.ru
index f5fd31f..b49b137 100644
--- a/t/pid.ru
+++ b/t/pid.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 run lambda { |env| [ 200, {}, [ "#$$\n" ] ] }
diff --git a/t/preread_input.ru b/t/preread_input.ru
index 79685c4..5f68fe9 100644
--- a/t/preread_input.ru
+++ b/t/preread_input.ru
@@ -1,17 +1,23 @@
 #\-E none
-require 'digest/sha1'
+# frozen_string_literal: false
+require 'digest/md5'
 require 'unicorn/preread_input'
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
 use Unicorn::PrereadInput
 nr = 0
 run lambda { |env|
   $stderr.write "app dispatch: #{nr += 1}\n"
   input = env["rack.input"]
-  dig = Digest::SHA1.new
-  while buf = input.read(16384)
-    dig.update(buf)
+  dig = Digest::MD5.new
+  if buf = input.read(16384)
+    begin
+      dig.update(buf)
+    end while input.read(16384, buf)
+    buf.clear # remove this call if Ruby ever gets escape analysis
   end
-
-  [ 200, {}, [ "#{dig.hexdigest}\n" ] ]
+  if env['HTTP_TRAILER'] =~ /\bContent-MD5\b/i
+    cmd5_b64 = env['HTTP_CONTENT_MD5'] or return [500, {}, ['No Content-MD5']]
+    cmd5_bin = cmd5_b64.unpack('m')[0]
+    return [500, {}, [ cmd5_b64 ] ] if cmd5_bin != dig.digest
+  end
+  [ 200, {}, [ dig.hexdigest ] ]
 }
diff --git a/t/rack-input-tests.ru b/t/rack-input-tests.ru
deleted file mode 100644
index 8c35630..0000000
--- a/t/rack-input-tests.ru
+++ /dev/null
@@ -1,21 +0,0 @@
-# SHA1 checksum generator
-require 'digest/sha1'
-use Rack::ContentLength
-cap = 16384
-app = lambda do |env|
-  /\A100-continue\z/i =~ env['HTTP_EXPECT'] and
-    return [ 100, {}, [] ]
-  digest = Digest::SHA1.new
-  input = env['rack.input']
-  input.size if env["PATH_INFO"] == "/size_first"
-  input.rewind if env["PATH_INFO"] == "/rewind_first"
-  if buf = input.read(rand(cap))
-    begin
-      raise "#{buf.size} > #{cap}" if buf.size > cap
-      digest.update(buf)
-    end while input.read(rand(cap), buf)
-  end
-
-  [ 200, {'Content-Type' => 'text/plain'}, [ digest.hexdigest << "\n" ] ]
-end
-run app
diff --git a/t/reload-bad-config.t b/t/reload-bad-config.t
new file mode 100644
index 0000000..c023b88
--- /dev/null
+++ b/t/reload-bad-config.t
@@ -0,0 +1,54 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+use v5.14; BEGIN { require './t/lib.perl' };
+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';
+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;
+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;
+
+$ar->do_kill('HUP'); # reload
+my @l;
+for (1..1000) {
+        @l = grep(/(?:done|error) reloading/, slurp($err_log)) and
+                last;
+        sleep 0.011;
+}
+diag slurp($err_log) if $ENV{V};
+ok(grep(/error reloading/, @l), 'got error reloading');
+open $fh, '>', $err_log;
+close $fh;
+
+($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+like($status, qr!\AHTTP/1\.[01] 200\b!, 'status line valid afte reload');
+is($bdy, "hello world\n", 'body matches expected after reload');
+
+check_stderr;
+undef $tmpdir; # quiet t/lib.perl END{}
+done_testing;
diff --git a/t/t0006.ru b/t/reopen-logs.ru
index c39e8f6..488da85 100644
--- a/t/t0006.ru
+++ b/t/reopen-logs.ru
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, "text/plain"
 run lambda { |env|
diff --git a/t/reopen-logs.t b/t/reopen-logs.t
new file mode 100644
index 0000000..76a4dbd
--- /dev/null
+++ b/t/reopen-logs.t
@@ -0,0 +1,39 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+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;
+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');
+is($bdy, "true\n", 'logs opened');
+
+rename($err_log, "$err_log.rot");
+rename($out_log, "$out_log.rot");
+
+$auto_reap->do_kill('USR1');
+
+my $tries = 1000;
+while (!-f $err_log && --$tries) { sleep 0.01 };
+while (!-f $out_log && --$tries) { sleep 0.01 };
+
+ok(-f $out_log, 'stdout_path recreated after USR1');
+ok(-f $err_log, 'stderr_path recreated after USR1');
+
+($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+is($bdy, "true\n", 'logs reopened with sync==true');
+
+$auto_reap->join('QUIT');
+is($?, 0, 'no error on exit');
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/t0000-http-basic.sh b/t/t0000-http-basic.sh
deleted file mode 100755
index 8ab58ac..0000000
--- a/t/t0000-http-basic.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 8 "simple HTTP connection tests"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        unicorn -D -c $unicorn_config env.ru
-        unicorn_wait_start
-}
-
-t_begin "single request" && {
-        curl -sSfv http://$listen/
-}
-
-t_begin "check stderr has no errors" && {
-        check_stderr
-}
-
-t_begin "HTTP/0.9 request should not return headers" && {
-        (
-                printf 'GET /\r\n'
-                cat $fifo > $tmp &
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-}
-
-t_begin "env.inspect should've put everything on one line" && {
-        test 1 -eq $(count_lines < $tmp)
-}
-
-t_begin "no headers in output" && {
-        if grep ^Connection: $tmp
-        then
-                die "Connection header found in $tmp"
-        elif grep ^HTTP/ $tmp
-        then
-                die "HTTP/ found in $tmp"
-        fi
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr has no errors" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0001-reload-bad-config.sh b/t/t0001-reload-bad-config.sh
deleted file mode 100755
index 55bb355..0000000
--- a/t/t0001-reload-bad-config.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 7 "reload config.ru error with preload_app true"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles ru
-
-        cat > $ru <<\EOF
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-x = { "hello" => "world" }
-run lambda { |env| [ 200, {}, [ x.inspect << "\n" ] ] }
-EOF
-        echo 'preload_app true' >> $unicorn_config
-        unicorn -D -c $unicorn_config $ru
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"world"}'
-}
-
-t_begin "introduce syntax error in rackup file" && {
-        echo '...' >> $ru
-}
-
-t_begin "reload signal succeeds" && {
-        kill -HUP $unicorn_pid
-        while ! egrep '(done|error) reloading' $r_err >/dev/null
-        do
-                sleep 1
-        done
-
-        grep 'error reloading' $r_err >/dev/null
-        > $r_err
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"world"}'
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0002-config-conflict.sh b/t/t0002-config-conflict.sh
deleted file mode 100755
index d7b2181..0000000
--- a/t/t0002-config-conflict.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 6 "config variables conflict with preload_app"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles ru rutmp
-
-        cat > $ru <<\EOF
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-config = ru = { "hello" => "world" }
-run lambda { |env| [ 200, {}, [ ru.inspect << "\n" ] ] }
-EOF
-        echo 'preload_app true' >> $unicorn_config
-        unicorn -D -c $unicorn_config $ru
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"world"}'
-}
-
-t_begin "modify rackup file" && {
-        sed -e 's/world/WORLD/' < $ru > $rutmp
-        mv $rutmp $ru
-}
-
-t_begin "reload signal succeeds" && {
-        kill -HUP $unicorn_pid
-        while ! egrep '(done|error) reloading' < $r_err >/dev/null
-        do
-                sleep 1
-        done
-
-        grep 'done reloading' $r_err >/dev/null
-}
-
-t_begin "hit with curl" && {
-        out=$(curl -sSf http://$listen/)
-        test x"$out" = x'{"hello"=>"WORLD"}'
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_done
diff --git a/t/t0002-parser-error.sh b/t/t0002-parser-error.sh
deleted file mode 100755
index 9dc1cd2..0000000
--- a/t/t0002-parser-error.sh
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 11 "parser error test"
-
-t_begin "setup and startup" && {
-        unicorn_setup
-        unicorn -D env.ru -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "send a bad request" && {
-        (
-                printf 'GET / HTTP/1/1\r\nHost: example.com\r\n\r\n'
-                cat $fifo > $tmp &
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-        test xok = x$(cat $ok)
-}
-
-dbgcat tmp
-
-t_begin "response should be a 400" && {
-        grep -F 'HTTP/1.1 400 Bad Request' $tmp
-}
-
-t_begin "send a huge Request URI (REQUEST_PATH > (12 * 1024))" && {
-        rm -f $tmp
-        cat $fifo > $tmp &
-        (
-                set -e
-                trap 'echo ok > $ok' EXIT
-                printf 'GET /'
-                for i in $(awk </dev/null 'BEGIN{for(i=0;i<1024;i++) print i}')
-                do
-                        printf '0123456789ab'
-                done
-                printf ' HTTP/1.1\r\nHost: example.com\r\n\r\n'
-        ) | socat - TCP:$listen > $fifo || :
-        test xok = x$(cat $ok)
-        wait
-}
-
-t_begin "response should be a 414 (REQUEST_PATH)" && {
-        grep -F 'HTTP/1.1 414 ' $tmp
-}
-
-t_begin "send a huge Request URI (QUERY_STRING > (10 * 1024))" && {
-        rm -f $tmp
-        cat $fifo > $tmp &
-        (
-                set -e
-                trap 'echo ok > $ok' EXIT
-                printf 'GET /hello-world?a'
-                for i in $(awk </dev/null 'BEGIN{for(i=0;i<1024;i++) print i}')
-                do
-                        printf '0123456789'
-                done
-                printf ' HTTP/1.1\r\nHost: example.com\r\n\r\n'
-        ) | socat - TCP:$listen > $fifo || :
-        test xok = x$(cat $ok)
-        wait
-}
-
-t_begin "response should be a 414 (QUERY_STRING)" && {
-        grep -F 'HTTP/1.1 414 ' $tmp
-}
-
-t_begin "send a huge Request URI (FRAGMENT > 1024)" && {
-        rm -f $tmp
-        cat $fifo > $tmp &
-        (
-                set -e
-                trap 'echo ok > $ok' EXIT
-                printf 'GET /hello-world#a'
-                for i in $(awk </dev/null 'BEGIN{for(i=0;i<64;i++) print i}')
-                do
-                        printf '0123456789abcdef'
-                done
-                printf ' HTTP/1.1\r\nHost: example.com\r\n\r\n'
-        ) | socat - TCP:$listen > $fifo || :
-        test xok = x$(cat $ok)
-        wait
-}
-
-t_begin "response should be a 414 (FRAGMENT)" && {
-        grep -F 'HTTP/1.1 414 ' $tmp
-}
-
-t_begin "server stderr should be clean" && check_stderr
-
-t_begin "term signal sent" && kill $unicorn_pid
-
-t_done
diff --git a/t/t0003-working_directory.sh b/t/t0003-working_directory.sh
deleted file mode 100755
index 79988d8..0000000
--- a/t/t0003-working_directory.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 4 "config.ru inside alt working_directory"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles unicorn_config_tmp
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        cat > $t_pfx.app/config.ru <<EOF
-#\--daemonize --host $host --port $port
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-run lambda { |env| [ 200, {}, [ "#{\$master_ppid}\\n" ] ] }
-EOF
-        # we have --host/--port in config.ru instead
-        grep -v ^listen $unicorn_config > $unicorn_config_tmp
-
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config_tmp
-
-        # allows ppid to be 1 in before_fork
-        echo "preload_app true" >> $unicorn_config_tmp
-        cat >> $unicorn_config_tmp <<\EOF
-before_fork do |server,worker|
-  $master_ppid = Process.ppid # should be zero to detect daemonization
-end
-EOF
-
-        mv $unicorn_config_tmp $unicorn_config
-
-        # rely on --daemonize switch, no & or -D
-        unicorn -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        body=$(curl -sSf http://$listen/)
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "response body ppid == 1 (daemonized)" && {
-        test "$body" -eq 1
-}
-
-t_done
diff --git a/t/t0004-heartbeat-timeout.sh b/t/t0004-heartbeat-timeout.sh
deleted file mode 100755
index 2965283..0000000
--- a/t/t0004-heartbeat-timeout.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 11 "heartbeat/timeout test"
-
-t_begin "setup and startup" && {
-        unicorn_setup
-        echo timeout 3 >> $unicorn_config
-        echo preload_app true >> $unicorn_config
-        unicorn -D heartbeat-timeout.ru -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "read worker PID" && {
-        worker_pid=$(curl -sSf http://$listen/)
-        t_info "worker_pid=$worker_pid"
-}
-
-t_begin "sleep for a bit, ensure worker PID does not change" && {
-        sleep 4
-        test $(curl -sSf http://$listen/) -eq $worker_pid
-}
-
-t_begin "block the worker process to force it to die" && {
-        rm $ok
-        t0=$(unix_time)
-        err="$(curl -sSf http://$listen/block-forever 2>&1 || > $ok)"
-        t1=$(unix_time)
-        elapsed=$(($t1 - $t0))
-        t_info "elapsed=$elapsed err=$err"
-        test x"$err" != x"Should never get here"
-        test x"$err" != x"$worker_pid"
-}
-
-t_begin "ensure worker was killed" && {
-        test -e $ok
-        test 1 -eq $(grep timeout $r_err | grep killing | count_lines)
-}
-
-t_begin "ensure timeout took at least 3 seconds" && {
-        test $elapsed -ge 3
-}
-
-t_begin "we get a fresh new worker process" && {
-        new_worker_pid=$(curl -sSf http://$listen/)
-        test $new_worker_pid -ne $worker_pid
-}
-
-t_begin "truncate the server error log" && {
-        > $r_err
-}
-
-t_begin "SIGSTOP and SIGCONT on unicorn master does not kill worker" && {
-        kill -STOP $unicorn_pid
-        sleep 4
-        kill -CONT $unicorn_pid
-        sleep 2
-        test $new_worker_pid -eq $(curl -sSf http://$listen/)
-}
-
-t_begin "stop server" && {
-        kill -QUIT $unicorn_pid
-}
-
-t_begin "check stderr" && check_stderr
-
-dbgcat r_err
-
-t_done
diff --git a/t/t0004-working_directory_broken.sh b/t/t0004-working_directory_broken.sh
deleted file mode 100755
index ca9d382..0000000
--- a/t/t0004-working_directory_broken.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 3 "config.ru is missing inside alt working_directory"
-
-t_begin "setup" && {
-        unicorn_setup
-        rtmpfiles unicorn_config_tmp ok
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config_tmp
-}
-
-t_begin "fails to start up w/o config.ru" && {
-        unicorn -c $unicorn_config_tmp || echo ok > $ok
-}
-
-t_begin "fallback code was run" && {
-        test x"$(cat $ok)" = xok
-}
-
-t_done
diff --git a/t/t0005-working_directory_app.rb.sh b/t/t0005-working_directory_app.rb.sh
deleted file mode 100755
index 0fbab4f..0000000
--- a/t/t0005-working_directory_app.rb.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 4 "fooapp.rb inside alt working_directory"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        cat > $t_pfx.app/fooapp.rb <<\EOF
-class Fooapp
-  def self.call(env)
-    # Rack::Lint in 1.5.0 requires headers to be a hash
-    h = [%w(Content-Type text/plain), %w(Content-Length 2)]
-    h = Rack::Utils::HeaderHash.new(h)
-    [ 200, h, %w(HI) ]
-  end
-end
-EOF
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config
-        cd /
-        unicorn -D -c $unicorn_config -I. fooapp.rb
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        body=$(curl -sSf http://$listen/)
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "response body expected" && {
-        test x"$body" = xHI
-}
-
-t_done
diff --git a/t/t0006-reopen-logs.sh b/t/t0006-reopen-logs.sh
deleted file mode 100755
index a6e7a17..0000000
--- a/t/t0006-reopen-logs.sh
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 15 "reopen rotated logs"
-
-t_begin "setup and startup" && {
-        rtmpfiles curl_out curl_err r_rot
-        unicorn_setup
-        unicorn -D t0006.ru -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "ensure server is responsive" && {
-        test xtrue = x$(curl -sSf http://$listen/ 2> $curl_err)
-}
-
-t_begin "ensure stderr log is clean" && check_stderr
-
-t_begin "external log rotation" && {
-        rm -f $r_rot
-        mv $r_err $r_rot
-}
-
-t_begin "send reopen log signal (USR1)" && {
-        kill -USR1 $unicorn_pid
-}
-
-t_begin "wait for rotated log to reappear" && {
-        nr=60
-        while ! test -f $r_err && test $nr -ge 0
-        do
-                sleep 1
-                nr=$(( $nr - 1 ))
-        done
-}
-
-t_begin "ensure server is still responsive" && {
-        test xtrue = x$(curl -sSf http://$listen/ 2> $curl_err)
-}
-
-t_begin "wait for worker to reopen logs" && {
-        nr=60
-        re="worker=.* done reopening logs"
-        while ! grep "$re" < $r_err >/dev/null && test $nr -ge 0
-        do
-                sleep 1
-                nr=$(( $nr - 1 ))
-        done
-}
-
-dbgcat r_rot
-dbgcat r_err
-
-t_begin "ensure no errors from curl" && {
-        test ! -s $curl_err
-}
-
-t_begin "current server stderr is clean" && check_stderr
-
-t_begin "rotated stderr is clean" && {
-        check_stderr $r_rot
-}
-
-t_begin "server is now writing logs to new stderr" && {
-        before_rot=$(count_bytes < $r_rot)
-        before_err=$(count_bytes < $r_err)
-        test xtrue = x$(curl -sSf http://$listen/ 2> $curl_err)
-        after_rot=$(count_bytes < $r_rot)
-        after_err=$(count_bytes < $r_err)
-        test $after_rot -eq $before_rot
-        test $after_err -gt $before_err
-}
-
-t_begin "stop server" && {
-        kill $unicorn_pid
-}
-
-dbgcat r_err
-
-t_begin "current server stderr is clean" && check_stderr
-t_begin "rotated stderr is clean" && check_stderr $r_rot
-
-t_done
diff --git a/t/t0007-working_directory_no_embed_cli.sh b/t/t0007-working_directory_no_embed_cli.sh
deleted file mode 100755
index 77d6707..0000000
--- a/t/t0007-working_directory_no_embed_cli.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-
-t_plan 4 "config.ru inside alt working_directory (no embedded switches)"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rm -rf $t_pfx.app
-        mkdir $t_pfx.app
-
-        cat > $t_pfx.app/config.ru <<EOF
-use Rack::ContentLength
-use Rack::ContentType, "text/plain"
-run lambda { |env| [ 200, {}, [ "#{\$master_ppid}\\n" ] ] }
-EOF
-        # the whole point of this exercise
-        echo "working_directory '$t_pfx.app'" >> $unicorn_config
-
-        # allows ppid to be 1 in before_fork
-        echo "preload_app true" >> $unicorn_config
-        cat >> $unicorn_config <<\EOF
-before_fork do |server,worker|
-  $master_ppid = Process.ppid # should be zero to detect daemonization
-end
-EOF
-
-        cd /
-        unicorn -D -c $unicorn_config
-        unicorn_wait_start
-}
-
-t_begin "hit with curl" && {
-        body=$(curl -sSf http://$listen/)
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "response body ppid == 1 (daemonized)" && {
-        test "$body" -eq 1
-}
-
-t_done
diff --git a/t/t0008-back_out_of_upgrade.sh b/t/t0008-back_out_of_upgrade.sh
deleted file mode 100755
index 96d4057..0000000
--- a/t/t0008-back_out_of_upgrade.sh
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 13 "backout of USR2 upgrade"
-
-worker_wait_start () {
-        test xSTART = x"$(cat $fifo)"
-        unicorn_pid=$(cat $pid)
-}
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rm -f $pid.oldbin
-
-cat >> $unicorn_config <<EOF
-after_fork do |server, worker|
-  # test script will block while reading from $fifo,
-  # so notify the script on the first worker we spawn
-  # by opening the FIFO
-  if worker.nr == 0
-    File.open("$fifo", "wb") { |fp| fp.syswrite "START" }
-  end
-end
-EOF
-        unicorn -D -c $unicorn_config pid.ru
-        worker_wait_start
-        orig_master_pid=$unicorn_pid
-}
-
-t_begin "read original worker pid" && {
-        orig_worker_pid=$(curl -sSf http://$listen/)
-        test -n "$orig_worker_pid" && kill -0 $orig_worker_pid
-}
-
-t_begin "upgrade to new master" && {
-        kill -USR2 $orig_master_pid
-}
-
-t_begin "kill old worker" && {
-        kill -WINCH $orig_master_pid
-}
-
-t_begin "wait for new worker to start" && {
-        worker_wait_start
-        test $unicorn_pid -ne $orig_master_pid
-        new_master_pid=$unicorn_pid
-}
-
-t_begin "old master pid is stashed in $pid.oldbin" && {
-        test -s "$pid.oldbin"
-        test $orig_master_pid -eq $(cat $pid.oldbin)
-}
-
-t_begin "ensure old worker is no longer running" && {
-        i=0
-        while kill -0 $orig_worker_pid 2>/dev/null
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "capture pid of new worker" && {
-        new_worker_pid=$(curl -sSf http://$listen/)
-}
-
-t_begin "reload old master process" && {
-        kill -HUP $orig_master_pid
-        worker_wait_start
-}
-
-t_begin "gracefully kill new master and ensure it dies" && {
-        kill -QUIT $new_master_pid
-        i=0
-        while kill -0 $new_worker_pid 2>/dev/null
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "ensure $pid.oldbin does not exist" && {
-        i=0
-        while test -s $pid.oldbin
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-        while ! test -s $pid
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "ensure $pid is correct" && {
-        cur_master_pid=$(cat $pid)
-        test $orig_master_pid -eq $cur_master_pid
-}
-
-t_begin "killing succeeds" && {
-        kill $orig_master_pid
-}
-
-dbgcat r_err
-
-t_done
diff --git a/t/t0009-winch_ttin.sh b/t/t0009-winch_ttin.sh
deleted file mode 100755
index 6e56e30..0000000
--- a/t/t0009-winch_ttin.sh
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 8 "SIGTTIN succeeds after SIGWINCH"
-
-t_begin "setup and start" && {
-        unicorn_setup
-cat >> $unicorn_config <<EOF
-after_fork do |server, worker|
-  # test script will block while reading from $fifo,
-  File.open("$fifo", "wb") { |fp| fp.syswrite worker.nr.to_s }
-end
-EOF
-        unicorn -D -c $unicorn_config pid.ru
-        unicorn_wait_start
-        test 0 -eq $(cat $fifo) || die "worker.nr != 0"
-}
-
-t_begin "read worker pid" && {
-        orig_worker_pid=$(curl -sSf http://$listen/)
-        test -n "$orig_worker_pid" && kill -0 $orig_worker_pid
-}
-
-t_begin "stop all workers" && {
-        kill -WINCH $unicorn_pid
-}
-
-# we have to do this next step before delivering TTIN
-# signals aren't guaranteed to delivered in order
-t_begin "wait for worker to die" && {
-        i=0
-        while kill -0 $orig_worker_pid 2>/dev/null
-        do
-                i=$(( $i + 1 ))
-                test $i -lt 600 || die "timed out"
-                sleep 1
-        done
-}
-
-t_begin "start one worker back up" && {
-        kill -TTIN $unicorn_pid
-}
-
-t_begin "wait for new worker to start" && {
-        test 0 -eq $(cat $fifo) || die "worker.nr != 0"
-        new_worker_pid=$(curl -sSf http://$listen/)
-        test -n "$new_worker_pid" && kill -0 $new_worker_pid
-        test $orig_worker_pid -ne $new_worker_pid || \
-           die "worker wasn't replaced"
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && check_stderr
-
-dbgcat r_err
-
-t_done
diff --git a/t/t0011-active-unix-socket.sh b/t/t0011-active-unix-socket.sh
deleted file mode 100755
index fae0b6c..0000000
--- a/t/t0011-active-unix-socket.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 11 "existing UNIX domain socket check"
-
-read_pid_unix () {
-        x=$(printf 'GET / HTTP/1.0\r\n\r\n' | \
-            socat - UNIX:$unix_socket | \
-            tail -1)
-        test -n "$x"
-        y="$(expr "$x" : '\([0-9][0-9]*\)')"
-        test x"$x" = x"$y"
-        test -n "$y"
-        echo "$y"
-}
-
-t_begin "setup and start" && {
-        rtmpfiles unix_socket unix_config
-        rm -f $unix_socket
-        unicorn_setup
-        grep -v ^listen < $unicorn_config > $unix_config
-        echo "listen '$unix_socket'" >> $unix_config
-        unicorn -D -c $unix_config pid.ru
-        unicorn_wait_start
-        orig_master_pid=$unicorn_pid
-}
-
-t_begin "get pid of worker" && {
-        worker_pid=$(read_pid_unix)
-        t_info "worker_pid=$worker_pid"
-}
-
-t_begin "fails to start with existing pid file" && {
-        rm -f $ok
-        unicorn -D -c $unix_config pid.ru || echo ok > $ok
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "worker pid unchanged" && {
-        test x"$(read_pid_unix)" = x$worker_pid
-        > $r_err
-}
-
-t_begin "fails to start with listening UNIX domain socket bound" && {
-        rm $ok $pid
-        unicorn -D -c $unix_config pid.ru || echo ok > $ok
-        test x"$(cat $ok)" = xok
-        > $r_err
-}
-
-t_begin "worker pid unchanged (again)" && {
-        test x"$(read_pid_unix)" = x$worker_pid
-}
-
-t_begin "nuking the existing Unicorn succeeds" && {
-        kill -9 $unicorn_pid
-        while kill -0 $unicorn_pid
-        do
-                sleep 1
-        done
-        check_stderr
-}
-
-t_begin "succeeds in starting with leftover UNIX domain socket bound" && {
-        test -S $unix_socket
-        unicorn -D -c $unix_config pid.ru
-        unicorn_wait_start
-}
-
-t_begin "worker pid changed" && {
-        test x"$(read_pid_unix)" != x$worker_pid
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "no errors" && check_stderr
-
-t_done
diff --git a/t/t0013.ru b/t/t0013.ru
index 48a3a34..e425093 100644
--- a/t/t0013.ru
+++ b/t/t0013.ru
@@ -1,4 +1,5 @@
 #\ -E none
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, 'text/plain'
 app = lambda do |env|
diff --git a/t/t0014.ru b/t/t0014.ru
index b0bd2b7..686d214 100644
--- a/t/t0014.ru
+++ b/t/t0014.ru
@@ -1,4 +1,5 @@
 #\ -E none
+# frozen_string_literal: false
 use Rack::ContentLength
 use Rack::ContentType, 'text/plain'
 app = lambda do |env|
diff --git a/t/t0018-write-on-close.sh b/t/t0018-write-on-close.sh
deleted file mode 100755
index 3afefea..0000000
--- a/t/t0018-write-on-close.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 4 "write-on-close tests for funky response-bodies"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        unicorn -D -c $unicorn_config write-on-close.ru
-        unicorn_wait_start
-}
-
-t_begin "write-on-close response body succeeds" && {
-        test xGoodbye = x"$(curl -sSf http://$listen/)"
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0019-max_header_len.sh b/t/t0019-max_header_len.sh
deleted file mode 100755
index 6a355b4..0000000
--- a/t/t0019-max_header_len.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 5 "max_header_len setting (only intended for Rainbows!)"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        req='GET / HTTP/1.0\r\n\r\n'
-        len=$(printf "$req" | count_bytes)
-        echo Unicorn::HttpParser.max_header_len = $len >> $unicorn_config
-        unicorn -D -c $unicorn_config env.ru
-        unicorn_wait_start
-}
-
-t_begin "minimal request succeeds" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf "$req"
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-        test xok = x$(cat $ok)
-
-        fgrep "HTTP/1.1 200 OK" $tmp
-}
-
-t_begin "big request fails" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'GET /xxxxxx HTTP/1.0\r\n\r\n'
-                wait
-                echo ok > $ok
-        ) | socat - TCP:$listen > $fifo
-        test xok = x$(cat $ok)
-        fgrep "HTTP/1.1 413" $tmp
-}
-
-dbgcat tmp
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0100-rack-input-tests.sh b/t/t0100-rack-input-tests.sh
deleted file mode 100755
index ee7a437..0000000
--- a/t/t0100-rack-input-tests.sh
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-test -r random_blob || die "random_blob required, run with 'make $0'"
-
-t_plan 10 "rack.input read tests"
-
-t_begin "setup and startup" && {
-        rtmpfiles curl_out curl_err
-        unicorn_setup
-        unicorn -E none -D rack-input-tests.ru -c $unicorn_config
-        blob_sha1=$(rsha1 < random_blob)
-        blob_size=$(count_bytes < random_blob)
-        t_info "blob_sha1=$blob_sha1"
-        unicorn_wait_start
-}
-
-t_begin "corked identity request" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT / HTTP/1.0\r\n'
-                printf 'Content-Length: %d\r\n\r\n' $blob_size
-                cat random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked chunked request" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                content-md5-put < random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked identity request (input#size first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /size_first HTTP/1.0\r\n'
-                printf 'Content-Length: %d\r\n\r\n' $blob_size
-                cat random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked identity request (input#rewind first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /rewind_first HTTP/1.0\r\n'
-                printf 'Content-Length: %d\r\n\r\n' $blob_size
-                cat random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked chunked request (input#size first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /size_first HTTP/1.1\r\n'
-                printf 'Host: example.com\r\n'
-                printf 'Transfer-Encoding: chunked\r\n'
-                printf 'Trailer: Content-MD5\r\n'
-                printf '\r\n'
-                content-md5-put --no-headers < random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "corked chunked request (input#rewind first)" && {
-        rm -f $tmp
-        (
-                cat $fifo > $tmp &
-                printf 'PUT /rewind_first HTTP/1.1\r\n'
-                printf 'Host: example.com\r\n'
-                printf 'Transfer-Encoding: chunked\r\n'
-                printf 'Trailer: Content-MD5\r\n'
-                printf '\r\n'
-                content-md5-put --no-headers < random_blob
-                wait
-                echo ok > $ok
-        ) | ( sleep 1 && socat - TCP4:$listen > $fifo )
-        test 1 -eq $(grep $blob_sha1 $tmp |count_lines)
-        test x"$(cat $ok)" = xok
-}
-
-t_begin "regular request" && {
-        curl -sSf -T random_blob http://$listen/ > $curl_out 2> $curl_err
-        test x$blob_sha1 = x$(cat $curl_out)
-        test ! -s $curl_err
-}
-
-t_begin "chunked request" && {
-        curl -sSf -T- < random_blob http://$listen/ > $curl_out 2> $curl_err
-        test x$blob_sha1 = x$(cat $curl_out)
-        test ! -s $curl_err
-}
-
-dbgcat r_err
-
-t_begin "shutdown" && {
-        kill $unicorn_pid
-}
-
-t_done
diff --git a/t/t0116-client_body_buffer_size.sh b/t/t0116-client_body_buffer_size.sh
deleted file mode 100755
index c9e17c7..0000000
--- a/t/t0116-client_body_buffer_size.sh
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 12 "client_body_buffer_size settings"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        rtmpfiles unicorn_config_tmp one_meg
-        dd if=/dev/zero bs=1M count=1 of=$one_meg
-        cat >> $unicorn_config <<EOF
-after_fork do |server, worker|
-  File.open("$fifo", "wb") { |fp| fp.syswrite "START" }
-end
-EOF
-        cat $unicorn_config > $unicorn_config_tmp
-        echo client_body_buffer_size 0 >> $unicorn_config
-        unicorn -D -c $unicorn_config t0116.ru
-        unicorn_wait_start
-        fs_class=Unicorn::TmpIO
-        mem_class=StringIO
-
-        test x"$(cat $fifo)" = xSTART
-}
-
-t_begin "class for a zero-byte file should be StringIO" && {
-        > $tmp
-        test xStringIO = x"$(curl -T $tmp -sSf http://$listen/input_class)"
-}
-
-t_begin "class for a 1 byte file should be filesystem-backed" && {
-        echo > $tmp
-        test x$fs_class = x"$(curl -T $tmp -sSf http://$listen/tmp_class)"
-}
-
-t_begin "reload with default client_body_buffer_size" && {
-        mv $unicorn_config_tmp $unicorn_config
-        kill -HUP $unicorn_pid
-        test x"$(cat $fifo)" = xSTART
-}
-
-t_begin "class for a 1 byte file should be memory-backed" && {
-        echo > $tmp
-        test x$mem_class = x"$(curl -T $tmp -sSf http://$listen/tmp_class)"
-}
-
-t_begin "class for a random blob file should be filesystem-backed" && {
-        resp="$(curl -T random_blob -sSf http://$listen/tmp_class)"
-        test x$fs_class = x"$resp"
-}
-
-t_begin "one megabyte file should be filesystem-backed" && {
-        resp="$(curl -T $one_meg -sSf http://$listen/tmp_class)"
-        test x$fs_class = x"$resp"
-}
-
-t_begin "reload with a big client_body_buffer_size" && {
-        echo "client_body_buffer_size(1024 * 1024)" >> $unicorn_config
-        kill -HUP $unicorn_pid
-        test x"$(cat $fifo)" = xSTART
-}
-
-t_begin "one megabyte file should be memory-backed" && {
-        resp="$(curl -T $one_meg -sSf http://$listen/tmp_class)"
-        test x$mem_class = x"$resp"
-}
-
-t_begin "one megabyte + 1 byte file should be filesystem-backed" && {
-        echo >> $one_meg
-        resp="$(curl -T $one_meg -sSf http://$listen/tmp_class)"
-        test x$fs_class = x"$resp"
-}
-
-t_begin "killing succeeds" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr" && {
-        check_stderr
-}
-
-t_done
diff --git a/t/t0200-rack-hijack.sh b/t/t0200-rack-hijack.sh
deleted file mode 100755
index fee0791..0000000
--- a/t/t0200-rack-hijack.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 9 "rack.hijack tests (Rack 1.5+ (Rack::VERSION >= [ 1,2]))"
-
-t_begin "setup and start" && {
-        unicorn_setup
-        unicorn -D -c $unicorn_config hijack.ru
-        unicorn_wait_start
-}
-
-t_begin "normal env reused between requests" && {
-        env_a="$(curl -sSf http://$listen/normal_env_id)"
-        b="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_a" = x"$b"
-}
-
-t_begin "check request hijack" && {
-        test "xrequest.hijacked" = x"$(curl -sSfv http://$listen/hijack_req)"
-}
-
-t_begin "env changed after request hijack" && {
-        env_b="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_a" != x"$env_b"
-}
-
-t_begin "check response hijack" && {
-        test "xresponse.hijacked" = x"$(curl -sSfv http://$listen/hijack_res)"
-}
-
-t_begin "env changed after response hijack" && {
-        env_c="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_b" != x"$env_c"
-}
-
-t_begin "env continues to be reused between requests" && {
-        b="$(curl -sSf http://$listen/normal_env_id)"
-        test x"$env_c" = x"$b"
-}
-
-t_begin "killing succeeds after hijack" && {
-        kill $unicorn_pid
-}
-
-t_begin "check stderr for hijacked body close" && {
-        check_stderr
-        grep 'closed DieIfUsed 1\>' $r_err
-        grep 'closed DieIfUsed 2\>' $r_err
-        ! grep 'closed DieIfUsed 3\>' $r_err
-}
-
-t_done
diff --git a/t/t0300-no-default-middleware.sh b/t/t0300-no-default-middleware.sh
index 779dc02..00feacc 100644
--- a/t/t0300-no-default-middleware.sh
+++ b/t/t0300-no-default-middleware.sh
@@ -9,7 +9,7 @@ t_begin "setup and start" && {
 }
 
 t_begin "check exit status with Rack::Lint not present" && {
-        test 42 -eq "$(curl -sf -o/dev/null -w'%{http_code}' http://$listen/)"
+        test 500 -ne "$(curl -sf -o/dev/null -w'%{http_code}' http://$listen/)"
 }
 
 t_begin "killing succeeds" && {
diff --git a/t/t0301.ru b/t/t0301.ru
index 1ae8ea7..54929b1 100644
--- a/t/t0301.ru
+++ b/t/t0301.ru
@@ -1,4 +1,5 @@
 #\-N --debug
+# frozen_string_literal: false
 run(lambda do |env|
   case env['PATH_INFO']
   when '/vars'
@@ -6,8 +7,8 @@ run(lambda do |env|
         "lint=#{caller.grep(%r{rack/lint\.rb})[0].split(':')[0]}\n"
   end
   h = {
-    'Content-Length' => b.size.to_s,
-    'Content-Type' => 'text/plain',
+    'content-length' => b.size.to_s,
+    'content-type' => 'text/plain',
   }
   [ 200, h, [ b ] ]
 end)
diff --git a/t/t9000-preread-input.sh b/t/t9000-preread-input.sh
deleted file mode 100755
index d6c73ab..0000000
--- a/t/t9000-preread-input.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-. ./test-lib.sh
-t_plan 9 "PrereadInput middleware tests"
-
-t_begin "setup and start" && {
-        random_blob_sha1=$(rsha1 < random_blob)
-        unicorn_setup
-        unicorn  -D -c $unicorn_config preread_input.ru
-        unicorn_wait_start
-}
-
-t_begin "single identity request" && {
-        curl -sSf -T random_blob http://$listen/ > $tmp
-}
-
-t_begin "sha1 matches" && {
-        test x"$(cat $tmp)" = x"$random_blob_sha1"
-}
-
-t_begin "single chunked request" && {
-        curl -sSf -T- < random_blob http://$listen/ > $tmp
-}
-
-t_begin "sha1 matches" && {
-        test x"$(cat $tmp)" = x"$random_blob_sha1"
-}
-
-t_begin "app only dispatched twice" && {
-        test 2 -eq "$(grep 'app dispatch:' < $r_err | count_lines )"
-}
-
-t_begin "aborted chunked request" && {
-        rm -f $tmp
-        curl -sSf -T- < $fifo http://$listen/ > $tmp &
-        curl_pid=$!
-        kill -9 $curl_pid
-        wait
-}
-
-t_begin "app only dispatched twice" && {
-        test 2 -eq "$(grep 'app dispatch:' < $r_err | count_lines )"
-}
-
-t_begin "killing succeeds" && {
-        kill -QUIT $unicorn_pid
-}
-
-t_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 7f97958..8613144 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -94,7 +94,8 @@ check_stderr () {
         set +u
         _r_err=${1-${r_err}}
         set -u
-        if grep -v $T $_r_err | grep -i Error
+        if grep -v $T $_r_err | grep -i Error | \
+                grep -v NameError.*Unicorn::Waiter
         then
                 die "Errors found in $_r_err"
         elif grep SIGKILL $_r_err
@@ -122,7 +123,3 @@ unicorn_wait_start () {
         # no need to play tricks with FIFOs since we got "ready_pipe" now
         unicorn_pid=$(cat $pid)
 }
-
-rsha1 () {
-        sha1sum.rb
-}
diff --git a/t/winch_ttin.t b/t/winch_ttin.t
new file mode 100644
index 0000000..c507959
--- /dev/null
+++ b/t/winch_ttin.t
@@ -0,0 +1,67 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+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;
+pid "$tmpdir/pid"
+listen "$u_sock"
+stderr_path "$err_log"
+after_fork do |server, worker|
+  # test script will block while reading from $fifo,
+  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";
+chomp(my $pid = <$fh>);
+ok(kill(0, $pid), 'daemonized PID works');
+my $quit = sub { kill('QUIT', $pid) if $pid; $pid = undef };
+END { $quit->() };
+
+open $fh, '<', $fifo;
+my $worker_nr = <$fh>;
+close $fh;
+is($worker_nr, '0', 'initial worker spawned');
+
+my ($status, $hdr, $worker_pid) = do_req($u_sock, 'GET /pid HTTP/1.0');
+like($status, qr/ 200\b/, 'got 200 response');
+like($worker_pid, qr/\A[0-9]+\n\z/s, 'PID in response');
+chomp $worker_pid;
+ok(kill(0, $worker_pid), 'worker_pid is valid');
+
+ok(kill('WINCH', $pid), 'SIGWINCH can be sent');
+
+my $tries = 1000;
+while (CORE::kill(0, $worker_pid) && --$tries) { sleep 0.01 }
+ok(!CORE::kill(0, $worker_pid), 'worker not running');
+
+ok(kill('TTIN', $pid), 'SIGTTIN to restart worker');
+
+open $fh, '<', $fifo;
+$worker_nr = <$fh>;
+close $fh;
+is($worker_nr, '0', 'worker restarted');
+
+($status, $hdr, my $new_worker_pid) = do_req($u_sock, 'GET /pid HTTP/1.0');
+like($status, qr/ 200\b/, 'got 200 response');
+like($new_worker_pid, qr/\A[0-9]+\n\z/, 'got new worker PID');
+chomp $new_worker_pid;
+ok(kill(0, $new_worker_pid), 'got a valid worker PID');
+isnt($worker_pid, $new_worker_pid, 'worker PID changed');
+
+$quit->();
+
+check_stderr;
+undef $tmpdir;
+done_testing;
diff --git a/t/working_directory.t b/t/working_directory.t
new file mode 100644
index 0000000..f9254eb
--- /dev/null
+++ b/t/working_directory.t
@@ -0,0 +1,94 @@
+#!perl -w
+# Copyright (C) unicorn hackers <unicorn-public@yhbt.net>
+# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
+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;
+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
+use Rack::ContentType, 'text/plain'
+run lambda { |env| [ 200, {}, [ "#{$master_ppid}\n" ] ] }
+EOM
+
+open $fh, '>', $ru;
+print $fh <<EOM;
+#\\--daemonize --listen $u_sock
+$common_ru
+EOM
+close $fh;
+
+unicorn('-c', $u_conf)->join; # will daemonize
+chomp($daemon_pid = slurp($pid_file));
+
+my ($status, $hdr, $bdy) = do_req($u_sock, 'GET / HTTP/1.0');
+is($bdy, "1\n", 'got expected $master_ppid');
+
+stop_daemon;
+check_stderr;
+
+if ('test without CLI switches in config.ru') {
+        truncate $err_log, 0;
+        open $fh, '>', $ru;
+        print $fh $common_ru;
+        close $fh;
+
+        unicorn('-D', '-l', $u_sock, '-c', $u_conf)->join; # will daemonize
+        chomp($daemon_pid = slurp($pid_file));
+
+        ($status, $hdr, $bdy) = do_req($u_sock, 'GET / HTTP/1.0');
+        is($bdy, "1\n", 'got expected $master_ppid');
+
+        stop_daemon;
+        check_stderr;
+}
+
+if ('ensures broken working_directory (missing config.ru) is OK') {
+        truncate $err_log, 0;
+        unlink $ru;
+
+        my $auto_reap = unicorn('-c', $u_conf);
+        $auto_reap->join;
+        isnt($?, 0, 'exited with error due to missing config.ru');
+
+        like(slurp($err_log), qr/rackup file \Q(config.ru)\E not readable/,
+                'noted unreadability of config.ru in stderr');
+}
+
+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;
+class Fooapp
+  def self.call(env)
+    b = "dir=#{Dir.pwd}"
+    h = { 'content-type' => 'text/plain', 'content-length' => b.bytesize.to_s }
+    [ 200, h, [ b ] ]
+  end
+end
+EOM
+        close $fh;
+        my $srv = tcp_server;
+        my $auto_reap = unicorn(qw(-c), $u_conf, qw(-I. fooapp.rb),
+                                { -C => '/', 3 => $srv });
+        ($status, $hdr, $bdy) = do_req($srv, 'GET / HTTP/1.0');
+        is($bdy, "dir=$tmpdir/alt",
+                'fooapp.rb (w/o config.ru) w/ working_directory');
+        $auto_reap->join('TERM');
+        is($?, 0, 'fooapp.rb process exited');
+        check_stderr;
+}
+
+undef $tmpdir;
+done_testing;
diff --git a/t/write-on-close.ru b/t/write-on-close.ru
deleted file mode 100644
index 54a2f2e..0000000
--- a/t/write-on-close.ru
+++ /dev/null
@@ -1,11 +0,0 @@
-class WriteOnClose
-  def each(&block)
-    @callback = block
-  end
-
-  def close
-    @callback.call "7\r\nGoodbye\r\n0\r\n\r\n"
-  end
-end
-use Rack::ContentType, "text/plain"
-run(lambda { |_| [ 200, [%w(Transfer-Encoding chunked)], WriteOnClose.new ] })