diff options
Diffstat (limited to 't')
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 @@ -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| @@ -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| @@ -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 @@ -1,4 +1,5 @@ #\ -E none +# frozen_string_literal: false use Rack::ContentLength use Rack::ContentType, 'text/plain' app = lambda do |env| @@ -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" && { @@ -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 ] }) |