diff options
Diffstat (limited to 'test')
-rwxr-xr-x | test/aggregate.rb | 1 | ||||
-rw-r--r-- | test/benchmark/README | 18 | ||||
-rw-r--r-- | test/benchmark/dd.ru | 1 | ||||
-rw-r--r-- | test/benchmark/ddstream.ru | 51 | ||||
-rw-r--r-- | test/benchmark/readinput.ru | 41 | ||||
-rw-r--r-- | test/benchmark/stack.ru | 1 | ||||
-rwxr-xr-x | test/benchmark/uconnect.perl | 66 | ||||
-rw-r--r-- | test/exec/test_exec.rb | 127 | ||||
-rw-r--r-- | test/test_helper.rb | 69 | ||||
-rw-r--r-- | test/unit/test_ccc.rb | 10 | ||||
-rw-r--r-- | test/unit/test_configurator.rb | 1 | ||||
-rw-r--r-- | test/unit/test_droplet.rb | 1 | ||||
-rw-r--r-- | test/unit/test_http_parser.rb | 1 | ||||
-rw-r--r-- | test/unit/test_http_parser_ng.rb | 82 | ||||
-rw-r--r-- | test/unit/test_request.rb | 50 | ||||
-rw-r--r-- | test/unit/test_response.rb | 111 | ||||
-rw-r--r-- | test/unit/test_server.rb | 109 | ||||
-rw-r--r-- | test/unit/test_signals.rb | 27 | ||||
-rw-r--r-- | test/unit/test_socket_helper.rb | 75 | ||||
-rw-r--r-- | test/unit/test_stream_input.rb | 28 | ||||
-rw-r--r-- | test/unit/test_tee_input.rb | 22 | ||||
-rw-r--r-- | test/unit/test_upload.rb | 306 | ||||
-rw-r--r-- | test/unit/test_util.rb | 10 | ||||
-rw-r--r-- | test/unit/test_waiter.rb | 35 |
24 files changed, 497 insertions, 746 deletions
diff --git a/test/aggregate.rb b/test/aggregate.rb index 5eebbe5..0f32b2f 100755 --- a/test/aggregate.rb +++ b/test/aggregate.rb @@ -1,5 +1,6 @@ #!/usr/bin/ruby -n # -*- encoding: binary -*- +# frozen_string_literal: false BEGIN { $tests = $assertions = $failures = $errors = 0 } diff --git a/test/benchmark/README b/test/benchmark/README index 1d3cdd0..cd929f3 100644 --- a/test/benchmark/README +++ b/test/benchmark/README @@ -42,9 +42,19 @@ The benchmark client is usually httperf. Another gentle reminder: performance with slow networks/clients is NOT our problem. That is the job of nginx (or similar). +== ddstream.ru + +Standalone Rack app intended to show how BAD we are at slow clients. +See usage in comments. + +== readinput.ru + +Standalone Rack app intended to show how bad we are with slow uploaders. +See usage in comments. + == Contributors -This directory is maintained independently in the "benchmark" branch -based against v0.1.0. Only changes to this directory (test/benchmarks) -are committed to this branch although the master branch may merge this -branch occassionaly. +This directory is intended to remain stable. Do not make changes +to benchmarking code which can change performance and invalidate +results across revisions. Instead, write new benchmarks and update +coments/documentation as necessary. diff --git a/test/benchmark/dd.ru b/test/benchmark/dd.ru index 111fa2e..5bd2739 100644 --- a/test/benchmark/dd.ru +++ b/test/benchmark/dd.ru @@ -1,3 +1,4 @@ +# frozen_string_literal: false # This benchmark is the simplest test of the I/O facilities in # unicorn. It is meant to return a fixed-sized blob to test # the performance of things in Unicorn, _NOT_ the app. diff --git a/test/benchmark/ddstream.ru b/test/benchmark/ddstream.ru new file mode 100644 index 0000000..fd40ced --- /dev/null +++ b/test/benchmark/ddstream.ru @@ -0,0 +1,51 @@ +# frozen_string_literal: false +# This app is intended to test large HTTP responses with or without +# a fully-buffering reverse proxy such as nginx. Without a fully-buffering +# reverse proxy, unicorn will be unresponsive when client count exceeds +# worker_processes. +# +# To demonstrate how bad unicorn is at slowly reading clients: +# +# # in one terminal, start unicorn with one worker: +# unicorn -E none -l 127.0.0.1:8080 test/benchmark/ddstream.ru +# +# # in a different terminal, start more slow curl processes than +# # unicorn workers and watch time outputs +# curl --limit-rate 8K --trace-time -vsN http://127.0.0.1:8080/ >/dev/null & +# curl --limit-rate 8K --trace-time -vsN http://127.0.0.1:8080/ >/dev/null & +# wait +# +# The last client won't see a response until the first one is done reading +# +# nginx note: do not change the default "proxy_buffering" behavior. +# Setting "proxy_buffering off" prevents nginx from protecting unicorn. + +# totally standalone rack app to stream a giant response +class BigResponse + def initialize(bs, count) + @buf = "#{bs.to_s(16)}\r\n#{' ' * bs}\r\n" + @count = count + @res = [ 200, + { 'Transfer-Encoding' => -'chunked', 'Content-Type' => 'text/plain' }, + self + ] + end + + # rack response body iterator + def each + (1..@count).each { yield @buf } + yield -"0\r\n\r\n" + end + + # rack app entry endpoint + def call(_env) + @res + end +end + +# default to a giant (128M) response because kernel socket buffers +# can be ridiculously large on some systems +bs = ENV['bs'] ? ENV['bs'].to_i : 65536 +count = ENV['count'] ? ENV['count'].to_i : 2048 +warn "serving response with bs=#{bs} count=#{count} (#{bs*count} bytes)" +run BigResponse.new(bs, count) diff --git a/test/benchmark/readinput.ru b/test/benchmark/readinput.ru new file mode 100644 index 0000000..95c0226 --- /dev/null +++ b/test/benchmark/readinput.ru @@ -0,0 +1,41 @@ +# frozen_string_literal: false +# This app is intended to test large HTTP requests with or without +# a fully-buffering reverse proxy such as nginx. Without a fully-buffering +# reverse proxy, unicorn will be unresponsive when client count exceeds +# worker_processes. + +DOC = <<DOC +To demonstrate how bad unicorn is at slowly uploading clients: + + # in one terminal, start unicorn with one worker: + unicorn -E none -l 127.0.0.1:8080 test/benchmark/readinput.ru + + # in a different terminal, upload 45M from multiple curl processes: + dd if=/dev/zero bs=45M count=1 | curl -T- -HExpect: --limit-rate 1M \ + --trace-time -v http://127.0.0.1:8080/ & + dd if=/dev/zero bs=45M count=1 | curl -T- -HExpect: --limit-rate 1M \ + --trace-time -v http://127.0.0.1:8080/ & + wait + +# The last client won't see a response until the first one is done uploading +# You also won't be able to make GET requests to view this documentation +# while clients are uploading. You can also view the stderr debug output +# of unicorn (see logging code in #{__FILE__}). +DOC + +run(lambda do |env| + input = env['rack.input'] + buf = ''.b + + # default logger contains timestamps, rely on that so users can + # see what the server is doing + l = env['rack.logger'] + + l.debug('BEGIN reading input ...') if l + :nop while input.read(16384, buf) + l.debug('DONE reading input ...') if l + + buf.clear + [ 200, [ %W(Content-Length #{DOC.size}), %w(Content-Type text/plain) ], + [ DOC ] ] +end) diff --git a/test/benchmark/stack.ru b/test/benchmark/stack.ru index fc9193f..17a565b 100644 --- a/test/benchmark/stack.ru +++ b/test/benchmark/stack.ru @@ -1,3 +1,4 @@ +# frozen_string_literal: false run(lambda { |env| body = "#{caller.size}\n" h = { diff --git a/test/benchmark/uconnect.perl b/test/benchmark/uconnect.perl new file mode 100755 index 0000000..230445e --- /dev/null +++ b/test/benchmark/uconnect.perl @@ -0,0 +1,66 @@ +#!/usr/bin/perl -w +# Benchmark script to spawn some processes and hammer a local unicorn +# to test accept loop performance. This only does Unix sockets. +# There's plenty of TCP benchmarking tools out there, and TCP port reuse +# has predictability problems since unicorn can't do persistent connections. +# Written in Perl for the same reason: predictability. +# Ruby GC is not as predictable as Perl refcounting. +use strict; +use Socket qw(AF_UNIX SOCK_STREAM sockaddr_un); +use POSIX qw(:sys_wait_h); +use Getopt::Std; +# -c / -n switches stolen from ab(1) +my $usage = "$0 [-c CONCURRENCY] [-n NUM_REQUESTS] SOCKET_PATH\n"; +our $opt_c = 2; +our $opt_n = 1000; +getopts('c:n:') or die $usage; +my $unix_path = shift or die $usage; +use constant REQ => "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; +use constant REQ_LEN => length(REQ); +use constant BUFSIZ => 8192; +$^F = 99; # don't waste syscall time with FD_CLOEXEC + +my %workers; # pid => worker num +die "-n $opt_n not evenly divisible by -c $opt_c\n" if $opt_n % $opt_c; +my $n_per_worker = $opt_n / $opt_c; +my $addr = sockaddr_un($unix_path); + +for my $num (1..$opt_c) { + defined(my $pid = fork) or die "fork failed: $!\n"; + if ($pid) { + $workers{$pid} = $num; + } else { + work($n_per_worker); + } +} + +reap_worker(0) while scalar keys %workers; +exit; + +sub work { + my ($n) = @_; + my ($buf, $x); + for (1..$n) { + socket(S, AF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; + connect(S, $addr) or die "connect: $!"; + defined($x = syswrite(S, REQ)) or die "write: $!"; + $x == REQ_LEN or die "short write: $x != ".REQ_LEN."\n"; + do { + $x = sysread(S, $buf, BUFSIZ); + unless (defined $x) { + next if $!{EINTR}; + die "sysread: $!\n"; + } + } until ($x == 0); + } + exit 0; +} + +sub reap_worker { + my ($flags) = @_; + my $pid = waitpid(-1, $flags); + return if !defined $pid || $pid <= 0; + my $p = delete $workers{$pid} || '(unknown)'; + warn("$pid [$p] exited with $?\n") if $?; + $p; +} diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb index 8a6b43e..807f724 100644 --- a/test/exec/test_exec.rb +++ b/test/exec/test_exec.rb @@ -1,6 +1,6 @@ # -*- encoding: binary -*- - -# Copyright (c) 2009 Eric Wong +# frozen_string_literal: false +# Don't add to this file, new tests are in Perl 5. See t/README FLOCK_PATH = File.expand_path(__FILE__) require './test/test_helper' @@ -25,28 +25,29 @@ class ExecTest < Test::Unit::TestCase HI = <<-EOS use Rack::ContentLength -run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] } +run proc { |env| [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ] } EOS SHOW_RACK_ENV = <<-EOS use Rack::ContentLength run proc { |env| - [ 200, { 'Content-Type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ] + [ 200, { 'content-type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ] } EOS HELLO = <<-EOS class Hello def call(env) - [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] + [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ] end end EOS COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP) + HEAVY_WORKERS = 2 HEAVY_CFG = <<-EOS -worker_processes 4 +worker_processes #{HEAVY_WORKERS} timeout 30 logger Logger.new('#{COMMON_TMP.path}') before_fork do |server, worker| @@ -61,9 +62,9 @@ run lambda { |env| a = ::File.stat(pwd) b = ::File.stat(Dir.pwd) if (a.ino == b.ino && a.dev == b.dev) - [ 200, { 'Content-Type' => 'text/plain' }, [ pwd ] ] + [ 200, { 'content-type' => 'text/plain' }, [ pwd ] ] else - [ 404, { 'Content-Type' => 'text/plain' }, [] ] + [ 404, { 'content-type' => 'text/plain' }, [] ] end } EOS @@ -96,59 +97,6 @@ run lambda { |env| end end - def test_sd_listen_fds_emulation - # [ruby-core:69895] [Bug #11336] fixed by r51576 - return if RUBY_VERSION.to_f < 2.3 - - File.open("config.ru", "wb") { |fp| fp.write(HI) } - sock = TCPServer.new(@addr, @port) - - [ %W(-l #@addr:#@port), nil ].each do |l| - sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0) - - pid = xfork do - redirect_test_io do - # pretend to be systemd - ENV['LISTEN_PID'] = "#$$" - ENV['LISTEN_FDS'] = '1' - - # 3 = SD_LISTEN_FDS_START - args = [ $unicorn_bin ] - args.concat(l) if l - args << { 3 => sock } - exec(*args) - end - end - res = hit(["http://#@addr:#@port/"]) - assert_equal [ "HI\n" ], res - assert_shutdown(pid) - assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool, - 'unicorn should always set SO_KEEPALIVE on inherited sockets' - end - ensure - sock.close if sock - end - - def test_inherit_listener_unspecified - File.open("config.ru", "wb") { |fp| fp.write(HI) } - sock = TCPServer.new(@addr, @port) - sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0) - - pid = xfork do - redirect_test_io do - ENV['UNICORN_FD'] = sock.fileno.to_s - exec($unicorn_bin, sock.fileno => sock.fileno) - end - end - res = hit(["http://#@addr:#@port/"]) - assert_equal [ "HI\n" ], res - assert_shutdown(pid) - assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool, - 'unicorn should always set SO_KEEPALIVE on inherited sockets' - ensure - sock.close if sock - end - def test_working_directory_rel_path_config_file other = Tempfile.new('unicorn.wd') File.unlink(other.path) @@ -254,7 +202,13 @@ after_fork do |server, worker| end EOF pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } } - File.open("#{other.path}/fifo", "rb").close + begin + fifo = File.open("#{other.path}/fifo", "rb") + rescue Errno::EINTR + # OpenBSD raises Errno::EINTR when opening + return if RUBY_PLATFORM =~ /openbsd/ + end + fifo.close assert ! File.exist?("stderr_log_here") assert ! File.exist?("stdout_log_here") @@ -291,16 +245,6 @@ EOF end end - def test_basic - File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } - pid = fork do - redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") } - end - results = retry_hit(["http://#{@addr}:#{@port}/"]) - assert_equal String, results[0].class - assert_shutdown(pid) - end - def test_rack_env_unset File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) } pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } } @@ -556,7 +500,7 @@ EOF def test_unicorn_config_per_worker_listen port2 = unused_port pid_spit = 'use Rack::ContentLength;' \ - 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }' + 'run proc { |e| [ 200, {"content-type"=>"text/plain"}, ["#$$\\n"] ] }' File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) } tmp = Tempfile.new('test.socket') File.unlink(tmp.path) @@ -573,7 +517,7 @@ EOF assert_equal String, results[0].class worker_pid = results[0].to_i assert_not_equal pid, worker_pid - s = UNIXSocket.new(tmp.path) + s = unix_socket(tmp.path) s.syswrite("GET / HTTP/1.0\r\n\r\n") results = '' loop { results << s.sysread(4096) } rescue nil @@ -606,6 +550,7 @@ EOF def test_weird_config_settings File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } ucfg = Tempfile.new('unicorn_test_config') + proc_total = HEAVY_WORKERS + 1 # + 1 for master ucfg.syswrite(HEAVY_CFG) pid = xfork do redirect_test_io do @@ -616,9 +561,9 @@ EOF results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class wait_master_ready(COMMON_TMP.path) - wait_workers_ready(COMMON_TMP.path, 4) + wait_workers_ready(COMMON_TMP.path, HEAVY_WORKERS) bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/) - assert_equal 4, bf.size + assert_equal HEAVY_WORKERS, bf.size rotate = Tempfile.new('unicorn_rotate') File.rename(COMMON_TMP.path, rotate.path) @@ -630,20 +575,20 @@ EOF tries = DEFAULT_TRIES log = File.readlines(rotate.path) while (tries -= 1) > 0 && - log.grep(/reopening logs\.\.\./).size < 5 + log.grep(/reopening logs\.\.\./).size < proc_total sleep DEFAULT_RES log = File.readlines(rotate.path) end - assert_equal 5, log.grep(/reopening logs\.\.\./).size + assert_equal proc_total, log.grep(/reopening logs\.\.\./).size assert_equal 0, log.grep(/done reopening logs/).size tries = DEFAULT_TRIES log = File.readlines(COMMON_TMP.path) - while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5 + while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < proc_total sleep DEFAULT_RES log = File.readlines(COMMON_TMP.path) end - assert_equal 5, log.grep(/done reopening logs/).size + assert_equal proc_total, log.grep(/done reopening logs/).size assert_equal 0, log.grep(/reopening logs\.\.\./).size Process.kill(:QUIT, pid) @@ -663,20 +608,6 @@ EOF assert_shutdown(pid) end - def test_config_ru_alt_path - config_path = "#{@tmpdir}/foo.ru" - File.open(config_path, "wb") { |fp| fp.syswrite(HI) } - pid = fork do - redirect_test_io do - Dir.chdir("/") - exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path) - end - end - results = retry_hit(["http://#{@addr}:#{@port}/"]) - assert_equal String, results[0].class - assert_shutdown(pid) - end - def test_load_module libdir = "#{@tmpdir}/lib" FileUtils.mkpath([ libdir ]) @@ -730,7 +661,7 @@ EOF wait_for_file(sock_path) assert File.socket?(sock_path) - sock = UNIXSocket.new(sock_path) + sock = unix_socket(sock_path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) @@ -740,7 +671,7 @@ EOF wait_for_file(sock_path) assert File.socket?(sock_path) - sock = UNIXSocket.new(sock_path) + sock = unix_socket(sock_path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) @@ -775,7 +706,7 @@ EOF assert_equal pid, File.read(pid_file).to_i assert File.socket?(sock_path), "socket created" - sock = UNIXSocket.new(sock_path) + sock = unix_socket(sock_path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) @@ -801,7 +732,7 @@ EOF wait_for_file(new_sock_path) assert File.socket?(new_sock_path), "socket exists" @sockets.each do |path| - sock = UNIXSocket.new(path) + sock = unix_socket(path) sock.syswrite("GET / HTTP/1.0\r\n\r\n") results = sock.sysread(4096) assert_equal String, results.class diff --git a/test/test_helper.rb b/test/test_helper.rb index c21f75d..0bf3c90 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or @@ -28,22 +29,43 @@ require 'tempfile' require 'fileutils' require 'logger' require 'unicorn' +require 'io/nonblock' if ENV['DEBUG'] require 'ruby-debug' Debugger.start end +unless RUBY_VERSION < '3.1' + warn "Unicorn was only tested against MRI up to 3.0.\n" \ + "It might not properly work with #{RUBY_VERSION}" +end + def redirect_test_io orig_err = STDERR.dup orig_out = STDOUT.dup - STDERR.reopen("test_stderr.#{$$}.log", "a") - STDOUT.reopen("test_stdout.#{$$}.log", "a") + rdr_pid = $$ + new_out = File.open("test_stdout.#$$.log", "a") + new_err = File.open("test_stderr.#$$.log", "a") + new_out.sync = new_err.sync = true + + if tail = ENV['TAIL'] # "tail -F" if GNU, "tail -f" otherwise + require 'shellwords' + cmd = tail.shellsplit + cmd << new_out.path + cmd << new_err.path + pid = Process.spawn(*cmd, { 1 => 2, :pgroup => true }) + sleep 0.1 # wait for tail(1) to startup + end + STDERR.reopen(new_err) + STDOUT.reopen(new_out) STDERR.sync = STDOUT.sync = true at_exit do - File.unlink("test_stderr.#{$$}.log") rescue nil - File.unlink("test_stdout.#{$$}.log") rescue nil + if rdr_pid == $$ + File.unlink(new_out.path) rescue nil + File.unlink(new_err.path) rescue nil + end end begin @@ -51,6 +73,7 @@ def redirect_test_io ensure STDERR.reopen(orig_err) STDOUT.reopen(orig_out) + Process.kill(:TERM, pid) if pid end end @@ -265,34 +288,20 @@ def wait_for_death(pid) raise "PID:#{pid} never died!" end -# executes +cmd+ and chunks its STDOUT -def chunked_spawn(stdout, *cmd) - fork { - crd, cwr = IO.pipe - crd.binmode - cwr.binmode - crd.sync = cwr.sync = true - - pid = fork { - STDOUT.reopen(cwr) - crd.close - cwr.close - exec(*cmd) - } - cwr.close - begin - buf = crd.readpartial(16384) - stdout.write("#{'%x' % buf.size}\r\n#{buf}") - rescue EOFError - stdout.write("0\r\n") - pid, status = Process.waitpid(pid) - exit status.exitstatus - end while true - } -end - def reset_sig_handlers %w(WINCH QUIT INT TERM USR1 USR2 HUP TTIN TTOU CHLD).each do |sig| trap(sig, "DEFAULT") end end + +def tcp_socket(*args) + sock = TCPSocket.new(*args) + sock.nonblock = false + sock +end + +def unix_socket(*args) + sock = UNIXSocket.new(*args) + sock.nonblock = false + sock +end diff --git a/test/unit/test_ccc.rb b/test/unit/test_ccc.rb index 3be1439..a0a2bff 100644 --- a/test/unit/test_ccc.rb +++ b/test/unit/test_ccc.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: false require 'socket' require 'unicorn' require 'io/wait' require 'tempfile' require 'test/unit' +require './test/test_helper' class TestCccTCPI < Test::Unit::TestCase def test_ccc_tcpi @@ -28,7 +30,7 @@ class TestCccTCPI < Test::Unit::TestCase # will wake up when writer closes sleep_pipe[0].read if env['PATH_INFO'] == '/sleep' - [ 200, [ %w(Content-Length 0), %w(Content-Type text/plain) ], [] ] + [ 200, {'content-length'=>'0', 'content-type'=>'text/plain'}, [] ] end ENV['UNICORN_FD'] = srv.fileno.to_s opts = { @@ -42,7 +44,7 @@ class TestCccTCPI < Test::Unit::TestCase wr.close # make sure the server is running, at least - client = TCPSocket.new(host, port) + client = tcp_socket(host, port) client.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") assert client.wait(10), 'never got response from server' res = client.read @@ -51,13 +53,13 @@ class TestCccTCPI < Test::Unit::TestCase client.close # start a slow request... - sleeper = TCPSocket.new(host, port) + sleeper = tcp_socket(host, port) sleeper.write("GET /sleep HTTP/1.1\r\nHost: example.com\r\n\r\n") # and a bunch of aborted ones nr = 100 nr.times do |i| - client = TCPSocket.new(host, port) + client = tcp_socket(host, port) client.write("GET /collections/#{rand(10000)} HTTP/1.1\r\n" \ "Host: example.com\r\n\r\n") client.close diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb index 1298f0e..1a89aca 100644 --- a/test/unit/test_configurator.rb +++ b/test/unit/test_configurator.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require 'test/unit' require 'tempfile' diff --git a/test/unit/test_droplet.rb b/test/unit/test_droplet.rb index 81ad82b..4b2d2d0 100644 --- a/test/unit/test_droplet.rb +++ b/test/unit/test_droplet.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require 'test/unit' require 'unicorn' diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb index 697af44..adcc84f 100644 --- a/test/unit/test_http_parser.rb +++ b/test/unit/test_http_parser.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or diff --git a/test/unit/test_http_parser_ng.rb b/test/unit/test_http_parser_ng.rb index d186f5a..fd47246 100644 --- a/test/unit/test_http_parser_ng.rb +++ b/test/unit/test_http_parser_ng.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require './test/test_helper' require 'digest/md5' @@ -11,6 +12,20 @@ class HttpParserNgTest < Test::Unit::TestCase @parser = HttpParser.new end + # RFC 7230 allows gzip/deflate/compress Transfer-Encoding, + # but "chunked" must be last if used + def test_is_chunked + [ 'chunked,chunked', 'chunked,gzip', 'chunked,gzip,chunked' ].each do |x| + assert_raise(HttpParserError) { HttpParser.is_chunked?(x) } + end + [ 'gzip, chunked', 'gzip,chunked', 'gzip ,chunked' ].each do |x| + assert HttpParser.is_chunked?(x) + end + [ 'gzip', 'xhunked', 'xchunked' ].each do |x| + assert !HttpParser.is_chunked?(x) + end + end + def test_parser_max_len assert_raises(RangeError) do HttpParser.max_header_len = 0xffffffff + 1 @@ -566,6 +581,73 @@ class HttpParserNgTest < Test::Unit::TestCase end end + def test_duplicate_content_length + str = "PUT / HTTP/1.1\r\n" \ + "Content-Length: 1\r\n" \ + "Content-Length: 9\r\n" \ + "\r\n" + assert_raises(HttpParserError) { @parser.headers({}, str) } + end + + def test_chunked_overrides_content_length + order = [ 'Transfer-Encoding: chunked', 'Content-Length: 666' ] + %w(a b).each do |x| + str = "PUT /#{x} HTTP/1.1\r\n" \ + "#{order.join("\r\n")}" \ + "\r\n\r\na\r\nhelloworld\r\n0\r\n\r\n" + order.reverse! + env = @parser.headers({}, str) + assert_nil @parser.content_length + assert_equal 'chunked', env['HTTP_TRANSFER_ENCODING'] + assert_equal '666', env['CONTENT_LENGTH'], + 'Content-Length logged so the app can log a possible client bug/attack' + @parser.filter_body(dst = '', str) + assert_equal 'helloworld', dst + @parser.parse # handle the non-existent trailer + assert @parser.next? + end + end + + def test_chunked_order_good + str = "PUT /x HTTP/1.1\r\n" \ + "Transfer-Encoding: gzip\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" + env = @parser.headers({}, str) + assert_equal 'gzip,chunked', env['HTTP_TRANSFER_ENCODING'] + assert_nil @parser.content_length + + @parser.clear + str = "PUT /x HTTP/1.1\r\n" \ + "Transfer-Encoding: gzip, chunked\r\n" \ + "\r\n" + env = @parser.headers({}, str) + assert_equal 'gzip, chunked', env['HTTP_TRANSFER_ENCODING'] + assert_nil @parser.content_length + end + + def test_chunked_order_bad + str = "PUT /x HTTP/1.1\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "Transfer-Encoding: gzip\r\n" \ + "\r\n" + assert_raise(HttpParserError) { @parser.headers({}, str) } + end + + def test_double_chunked + str = "PUT /x HTTP/1.1\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" + assert_raise(HttpParserError) { @parser.headers({}, str) } + + @parser.clear + str = "PUT /x HTTP/1.1\r\n" \ + "Transfer-Encoding: chunked,chunked\r\n" \ + "\r\n" + assert_raise(HttpParserError) { @parser.headers({}, str) } + end + def test_backtrace_is_empty begin @parser.headers({}, "AAADFSFDSFD\r\n\r\n") diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb index 6cb0268..9d1b350 100644 --- a/test/unit/test_request.rb +++ b/test/unit/test_request.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or @@ -10,19 +11,14 @@ include Unicorn class RequestTest < Test::Unit::TestCase - class MockRequest < StringIO - alias_method :readpartial, :sysread - alias_method :kgio_read!, :sysread - alias_method :read_nonblock, :sysread - def kgio_addr - '127.0.0.1' - end - end + MockRequest = Class.new(StringIO) + + AI = Addrinfo.new(Socket.sockaddr_un('/unicorn/sucks')) def setup @request = HttpRequest.new @app = lambda do |env| - [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ] + [ 200, { 'content-length' => '0', 'content-type' => 'text/plain' }, [] ] end @lint = Rack::Lint.new(@app) end @@ -30,7 +26,7 @@ class RequestTest < Test::Unit::TestCase def test_options client = MockRequest.new("OPTIONS * HTTP/1.1\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal '', env['REQUEST_PATH'] assert_equal '', env['PATH_INFO'] assert_equal '*', env['REQUEST_URI'] @@ -40,7 +36,7 @@ class RequestTest < Test::Unit::TestCase def test_absolute_uri_with_query client = MockRequest.new("GET http://e:3/x?y=z HTTP/1.1\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal '/x', env['REQUEST_PATH'] assert_equal '/x', env['PATH_INFO'] assert_equal 'y=z', env['QUERY_STRING'] @@ -50,7 +46,7 @@ class RequestTest < Test::Unit::TestCase def test_absolute_uri_with_fragment client = MockRequest.new("GET http://e:3/x#frag HTTP/1.1\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal '/x', env['REQUEST_PATH'] assert_equal '/x', env['PATH_INFO'] assert_equal '', env['QUERY_STRING'] @@ -61,7 +57,7 @@ class RequestTest < Test::Unit::TestCase def test_absolute_uri_with_query_and_fragment client = MockRequest.new("GET http://e:3/x?a=b#frag HTTP/1.1\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal '/x', env['REQUEST_PATH'] assert_equal '/x', env['PATH_INFO'] assert_equal 'a=b', env['QUERY_STRING'] @@ -73,7 +69,7 @@ class RequestTest < Test::Unit::TestCase %w(ssh+http://e/ ftp://e/x http+ssh://e/x).each do |abs_uri| client = MockRequest.new("GET #{abs_uri} HTTP/1.1\r\n" \ "Host: foo\r\n\r\n") - assert_raises(HttpParserError) { @request.read(client) } + assert_raises(HttpParserError) { @request.read_headers(client, AI) } end end @@ -81,7 +77,7 @@ class RequestTest < Test::Unit::TestCase client = MockRequest.new("GET / HTTP/1.1\r\n" \ "X-Forwarded-Proto: https\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal "https", env['rack.url_scheme'] assert_kind_of Array, @lint.call(env) end @@ -90,7 +86,7 @@ class RequestTest < Test::Unit::TestCase client = MockRequest.new("GET / HTTP/1.1\r\n" \ "X-Forwarded-Proto: http\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal "http", env['rack.url_scheme'] assert_kind_of Array, @lint.call(env) end @@ -99,14 +95,14 @@ class RequestTest < Test::Unit::TestCase client = MockRequest.new("GET / HTTP/1.1\r\n" \ "X-Forwarded-Proto: ftp\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal "http", env['rack.url_scheme'] assert_kind_of Array, @lint.call(env) end def test_rack_lint_get client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal "http", env['rack.url_scheme'] assert_equal '127.0.0.1', env['REMOTE_ADDR'] assert_kind_of Array, @lint.call(env) @@ -114,7 +110,7 @@ class RequestTest < Test::Unit::TestCase def test_no_content_stringio client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal StringIO, env['rack.input'].class end @@ -122,7 +118,7 @@ class RequestTest < Test::Unit::TestCase client = MockRequest.new("PUT / HTTP/1.1\r\n" \ "Content-Length: 0\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal StringIO, env['rack.input'].class end @@ -130,7 +126,7 @@ class RequestTest < Test::Unit::TestCase client = MockRequest.new("PUT / HTTP/1.1\r\n" \ "Content-Length: 1\r\n" \ "Host: foo\r\n\r\n") - env = @request.read(client) + env = @request.read_headers(client, AI) assert_equal Unicorn::TeeInput, env['rack.input'].class end @@ -141,7 +137,7 @@ class RequestTest < Test::Unit::TestCase "Content-Length: 5\r\n" \ "\r\n" \ "abcde") - env = @request.read(client) + env = @request.read_headers(client, AI) assert ! env.include?(:http_body) assert_kind_of Array, @lint.call(env) end @@ -152,14 +148,6 @@ class RequestTest < Test::Unit::TestCase buf = (' ' * bs).freeze length = bs * count client = Tempfile.new('big_put') - def client.kgio_addr; '127.0.0.1'; end - def client.kgio_read(*args) - readpartial(*args) - rescue EOFError - end - def client.kgio_read!(*args) - readpartial(*args) - end client.syswrite( "PUT / HTTP/1.1\r\n" \ "Host: foo\r\n" \ @@ -167,7 +155,7 @@ class RequestTest < Test::Unit::TestCase "\r\n") count.times { assert_equal bs, client.syswrite(buf) } assert_equal 0, client.sysseek(0) - env = @request.read(client) + env = @request.read_headers(client, AI) assert ! env.include?(:http_body) assert_equal length, env['rack.input'].size count.times { diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb deleted file mode 100644 index fbe433f..0000000 --- a/test/unit/test_response.rb +++ /dev/null @@ -1,111 +0,0 @@ -# -*- encoding: binary -*- - -# Copyright (c) 2005 Zed A. Shaw -# You can redistribute it and/or modify it under the same terms as Ruby 1.8 or -# the GPLv2+ (GPLv3+ preferred) -# -# Additional work donated by contributors. See git history -# for more information. - -require './test/test_helper' -require 'time' - -include Unicorn - -class ResponseTest < Test::Unit::TestCase - include Unicorn::HttpResponse - - def test_httpdate - before = Time.now.to_i - 1 - str = httpdate - assert_kind_of(String, str) - middle = Time.parse(str).to_i - after = Time.now.to_i - assert before <= middle - assert middle <= after - end - - def test_response_headers - out = StringIO.new - http_response_write(out, 200, {"X-Whatever" => "stuff"}, ["cool"]) - assert ! out.closed? - - assert out.length > 0, "output didn't have data" - end - - # ref: <CAO47=rJa=zRcLn_Xm4v2cHPr6c0UswaFC_omYFEH+baSxHOWKQ@mail.gmail.com> - def test_response_header_broken_nil - out = StringIO.new - http_response_write(out, 200, {"Nil" => nil}, %w(hysterical raisin)) - assert ! out.closed? - - assert_match %r{^Nil: \r\n}sm, out.string, 'nil accepted' - end - - def test_response_string_status - out = StringIO.new - http_response_write(out,'200', {}, []) - assert ! out.closed? - assert out.length > 0, "output didn't have data" - end - - def test_response_200 - io = StringIO.new - http_response_write(io, 200, {}, []) - assert ! io.closed? - assert io.length > 0, "output didn't have data" - end - - def test_response_with_default_reason - code = 400 - io = StringIO.new - http_response_write(io, code, {}, []) - assert ! io.closed? - lines = io.string.split(/\r\n/) - assert_match(/.* Bad Request$/, lines.first, - "wrong default reason phrase") - end - - def test_rack_multivalue_headers - out = StringIO.new - http_response_write(out,200, {"X-Whatever" => "stuff\nbleh"}, []) - assert ! out.closed? - assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string) - end - - # Even though Rack explicitly forbids "Status" in the header hash, - # some broken clients still rely on it - def test_status_header_added - out = StringIO.new - http_response_write(out,200, {"X-Whatever" => "stuff"}, []) - assert ! out.closed? - end - - def test_unknown_status_pass_through - out = StringIO.new - http_response_write(out,"666 I AM THE BEAST", {}, [] ) - assert ! out.closed? - headers = out.string.split(/\r\n\r\n/).first.split(/\r\n/) - assert %r{\AHTTP/\d\.\d 666 I AM THE BEAST\z}.match(headers[0]) - end - - def test_modified_rack_http_status_codes_late - r, w = IO.pipe - pid = fork do - r.close - # Users may want to globally override the status text associated - # with an HTTP status code in their app. - Rack::Utils::HTTP_STATUS_CODES[200] = "HI" - http_response_write(w, 200, {}, []) - w.close - end - w.close - assert_equal "HTTP/1.1 200 HI\r\n", r.gets - r.read # just drain the pipe - pid, status = Process.waitpid2(pid) - assert status.success?, status.inspect - ensure - r.close - w.close unless w.closed? - end -end diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb index 8096955..5a2252f 100644 --- a/test/unit/test_server.rb +++ b/test/unit/test_server.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or @@ -16,13 +17,30 @@ class TestHandler def call(env) while env['rack.input'].read(4096) end - [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']] + [200, { 'content-type' => 'text/plain' }, ['hello!\n']] rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n") raise e end end +class TestRackAfterReply + def initialize + @called = false + end + + def call(env) + while env['rack.input'].read(4096) + end + + env["rack.after_reply"] << -> { @called = true } + + [200, { 'content-type' => 'text/plain' }, ["after_reply_called: #{@called}"]] + rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e + $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n") + raise e + end +end class WebServerTest < Test::Unit::TestCase @@ -53,7 +71,7 @@ class WebServerTest < Test::Unit::TestCase tmp.sysseek(0) tmp.truncate(0) tmp.syswrite($$) - lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "#$$\n" ] ] } + lambda { |env| [ 200, { 'content-type' => 'text/plain' }, [ "#$$\n" ] ] } } redirect_test_io do @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] ) @@ -84,18 +102,30 @@ class WebServerTest < Test::Unit::TestCase tmp.close! end - def test_broken_app + def test_after_reply teardown - app = lambda { |env| raise RuntimeError, "hello" } - # [200, {}, []] } + redirect_test_io do - @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] ) + @server = HttpServer.new(TestRackAfterReply.new, + :listeners => [ "127.0.0.1:#@port"]) @server.start end - sock = TCPSocket.new('127.0.0.1', @port) + + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") - assert_match %r{\AHTTP/1.[01] 500\b}, sock.sysread(4096) - assert_nil sock.close + + responses = sock.read(4096) + assert_match %r{\AHTTP/1.[01] 200\b}, responses + assert_match %r{^after_reply_called: false}, responses + + sock = tcp_socket('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + + responses = sock.read(4096) + assert_match %r{\AHTTP/1.[01] 200\b}, responses + assert_match %r{^after_reply_called: true}, responses + + sock.close end def test_simple_server @@ -103,62 +133,9 @@ class WebServerTest < Test::Unit::TestCase assert_equal 'hello!\n', results[0], "Handler didn't really run" end - def test_client_shutdown_writes - bs = 15609315 * rand - sock = TCPSocket.new('127.0.0.1', @port) - sock.syswrite("PUT /hello HTTP/1.1\r\n") - sock.syswrite("Host: example.com\r\n") - sock.syswrite("Transfer-Encoding: chunked\r\n") - sock.syswrite("Trailer: X-Foo\r\n") - sock.syswrite("\r\n") - sock.syswrite("%x\r\n" % [ bs ]) - sock.syswrite("F" * bs) - sock.syswrite("\r\n0\r\nX-") - "Foo: bar\r\n\r\n".each_byte do |x| - sock.syswrite x.chr - sleep 0.05 - end - # we wrote the entire request before shutting down, server should - # continue to process our request and never hit EOFError on our sock - sock.shutdown(Socket::SHUT_WR) - buf = sock.read - assert_equal 'hello!\n', buf.split(/\r\n\r\n/).last - next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/")) - assert_equal 'hello!\n', next_client - lines = File.readlines("test_stderr.#$$.log") - assert lines.grep(/^Unicorn::ClientShutdown: /).empty? - assert_nil sock.close - end - - def test_client_shutdown_write_truncates - bs = 15609315 * rand - sock = TCPSocket.new('127.0.0.1', @port) - sock.syswrite("PUT /hello HTTP/1.1\r\n") - sock.syswrite("Host: example.com\r\n") - sock.syswrite("Transfer-Encoding: chunked\r\n") - sock.syswrite("Trailer: X-Foo\r\n") - sock.syswrite("\r\n") - sock.syswrite("%x\r\n" % [ bs ]) - sock.syswrite("F" * (bs / 2.0)) - - # shutdown prematurely, this will force the server to abort - # processing on us even during app dispatch - sock.shutdown(Socket::SHUT_WR) - IO.select([sock], nil, nil, 60) or raise "Timed out" - buf = sock.read - assert_equal "", buf - next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/")) - assert_equal 'hello!\n', next_client - lines = File.readlines("test_stderr.#$$.log") - lines = lines.grep(/^Unicorn::ClientShutdown: bytes_read=\d+/) - assert_equal 1, lines.size - assert_match %r{\AUnicorn::ClientShutdown: bytes_read=\d+ true$}, lines[0] - assert_nil sock.close - end - def test_client_malformed_body bs = 15653984 - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("PUT /hello HTTP/1.1\r\n") sock.syswrite("Host: example.com\r\n") sock.syswrite("Transfer-Encoding: chunked\r\n") @@ -180,7 +157,7 @@ class WebServerTest < Test::Unit::TestCase def do_test(string, chunk, close_after=nil, shutdown_delay=0) # Do not use instance variables here, because it needs to be thread safe - socket = TCPSocket.new("127.0.0.1", @port); + socket = tcp_socket("127.0.0.1", @port); request = StringIO.new(string) chunks_out = 0 @@ -225,14 +202,14 @@ class WebServerTest < Test::Unit::TestCase end def test_bad_client_400 - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\nHost: foo\rbar\r\n\r\n") assert_match %r{\AHTTP/1.[01] 400\b}, sock.sysread(4096) assert_nil sock.close end def test_http_0_9 - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET /hello\r\n") assert_match 'hello!\n', sock.sysread(4096) assert_nil sock.close diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb index 4d9fdc5..49ff3c7 100644 --- a/test/unit/test_signals.rb +++ b/test/unit/test_signals.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or @@ -47,16 +48,16 @@ class SignalsTest < Test::Unit::TestCase def test_worker_dies_on_dead_master pid = fork { - app = lambda { |env| [ 200, {'X-Pid' => "#$$" }, [] ] } + app = lambda { |env| [ 200, {'x-pid' => "#$$" }, [] ] } opts = @server_opts.merge(:timeout => 3) redirect_test_io { HttpServer.new(app, opts).start.join } } wait_workers_ready("test_stderr.#{pid}.log", 1) - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = sock.readpartial(4096) assert_nil sock.close - buf =~ /\bX-Pid: (\d+)\b/ or raise Exception + buf =~ /\bx-pid: (\d+)\b/ or raise Exception child = $1.to_i wait_master_ready("test_stderr.#{pid}.log") wait_workers_ready("test_stderr.#{pid}.log", 1) @@ -79,7 +80,7 @@ class SignalsTest < Test::Unit::TestCase } wr.close wait_workers_ready("test_stderr.#{pid}.log", 1) - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = rd.readpartial(1) wait_master_ready("test_stderr.#{pid}.log") @@ -102,7 +103,7 @@ class SignalsTest < Test::Unit::TestCase } t0 = Time.now wait_workers_ready("test_stderr.#{pid}.log", 1) - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = nil @@ -120,17 +121,17 @@ class SignalsTest < Test::Unit::TestCase def test_response_write app = lambda { |env| - [ 200, { 'Content-Type' => 'text/plain', 'X-Pid' => Process.pid.to_s }, + [ 200, { 'content-type' => 'text/plain', 'x-pid' => Process.pid.to_s }, Dd.new(@bs, @count) ] } redirect_test_io { @server = HttpServer.new(app, @server_opts).start } wait_workers_ready("test_stderr.#{$$}.log", 1) - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") buf = '' header_len = pid = nil buf = sock.sysread(16384, buf) - pid = buf[/\r\nX-Pid: (\d+)\r\n/, 1].to_i + pid = buf[/\r\nx-pid: (\d+)\r\n/, 1].to_i header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size assert pid > 0, "pid not positive: #{pid.inspect}" read = buf.size @@ -158,18 +159,18 @@ class SignalsTest < Test::Unit::TestCase app = lambda { |env| while env['rack.input'].read(4096) end - [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ] + [ 200, {'content-type'=>'text/plain', 'x-pid'=>Process.pid.to_s}, [] ] } redirect_test_io { @server = HttpServer.new(app, @server_opts).start } wait_workers_ready("test_stderr.#{$$}.log", 1) - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") - pid = sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i + pid = sock.sysread(4096)[/\r\nx-pid: (\d+)\r\n/, 1].to_i assert_nil sock.close assert pid > 0, "pid not positive: #{pid.inspect}" - sock = TCPSocket.new('127.0.0.1', @port) + sock = tcp_socket('127.0.0.1', @port) sock.syswrite("PUT / HTTP/1.0\r\n") sock.syswrite("Content-Length: #{@bs * @count}\r\n\r\n") 1000.times { Process.kill(:HUP, pid) } @@ -182,7 +183,7 @@ class SignalsTest < Test::Unit::TestCase redirect_test_io { @server.stop(true) } # can't check for == since pending signals get merged assert size_before < @tmp.stat.size - assert_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i + assert_equal pid, sock.sysread(4096)[/\r\nx-pid: (\d+)\r\n/, 1].to_i assert_nil sock.close end end diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb index fbc7bb9..4363474 100644 --- a/test/unit/test_socket_helper.rb +++ b/test/unit/test_socket_helper.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require './test/test_helper' require 'tempfile' @@ -24,7 +25,8 @@ class TestSocketHelper < Test::Unit::TestCase port = unused_port @test_addr @tcp_listener_name = "#@test_addr:#{port}" @tcp_listener = bind_listen(@tcp_listener_name) - assert TCPServer === @tcp_listener + assert Socket === @tcp_listener + assert @tcp_listener.local_address.ip? assert_equal @tcp_listener_name, sock_name(@tcp_listener) end @@ -38,10 +40,10 @@ class TestSocketHelper < Test::Unit::TestCase { :backlog => 16, :rcvbuf => 4096, :sndbuf => 4096 } ].each do |opts| tcp_listener = bind_listen(tcp_listener_name, opts) - assert TCPServer === tcp_listener + assert tcp_listener.local_address.ip? tcp_listener.close unix_listener = bind_listen(unix_listener_name, opts) - assert UNIXServer === unix_listener + assert unix_listener.local_address.unix? unix_listener.close end end @@ -52,11 +54,13 @@ class TestSocketHelper < Test::Unit::TestCase @unix_listener_path = tmp.path File.unlink(@unix_listener_path) @unix_listener = bind_listen(@unix_listener_path) - assert UNIXServer === @unix_listener + assert Socket === @unix_listener + assert @unix_listener.local_address.unix? assert_equal @unix_listener_path, sock_name(@unix_listener) assert File.readable?(@unix_listener_path), "not readable" assert File.writable?(@unix_listener_path), "not writable" assert_equal 0777, File.umask + assert_equal @unix_listener, bind_listen(@unix_listener) ensure File.umask(old_umask) end @@ -67,7 +71,6 @@ class TestSocketHelper < Test::Unit::TestCase @unix_listener_path = tmp.path File.unlink(@unix_listener_path) @unix_listener = bind_listen(@unix_listener_path, :umask => 077) - assert UNIXServer === @unix_listener assert_equal @unix_listener_path, sock_name(@unix_listener) assert_equal 0140700, File.stat(@unix_listener_path).mode assert_equal 0777, File.umask @@ -75,28 +78,6 @@ class TestSocketHelper < Test::Unit::TestCase File.umask(old_umask) end - def test_bind_listen_unix_idempotent - test_bind_listen_unix - a = bind_listen(@unix_listener) - assert_equal a.fileno, @unix_listener.fileno - unix_server = server_cast(@unix_listener) - assert UNIXServer === unix_server - a = bind_listen(unix_server) - assert_equal a.fileno, unix_server.fileno - assert_equal a.fileno, @unix_listener.fileno - end - - def test_bind_listen_tcp_idempotent - test_bind_listen_tcp - a = bind_listen(@tcp_listener) - assert_equal a.fileno, @tcp_listener.fileno - tcp_server = server_cast(@tcp_listener) - assert TCPServer === tcp_server - a = bind_listen(tcp_server) - assert_equal a.fileno, tcp_server.fileno - assert_equal a.fileno, @tcp_listener.fileno - end - def test_bind_listen_unix_rebind test_bind_listen_unix new_listener = nil @@ -107,48 +88,26 @@ class TestSocketHelper < Test::Unit::TestCase File.unlink(@unix_listener_path) new_listener = bind_listen(@unix_listener_path) - assert UNIXServer === new_listener assert new_listener.fileno != @unix_listener.fileno assert_equal sock_name(new_listener), sock_name(@unix_listener) assert_equal @unix_listener_path, sock_name(new_listener) pid = fork do - client = server_cast(new_listener).accept - client.syswrite('abcde') - exit 0 + begin + client, _ = new_listener.accept + client.syswrite('abcde') + exit 0 + rescue => e + warn "#{e.message} (#{e.class})" + exit 1 + end end - s = UNIXSocket.new(@unix_listener_path) + s = unix_socket(@unix_listener_path) IO.select([s]) assert_equal 'abcde', s.sysread(5) pid, status = Process.waitpid2(pid) assert status.success? end - def test_server_cast - test_bind_listen_unix - test_bind_listen_tcp - unix_listener_socket = Socket.for_fd(@unix_listener.fileno) - assert Socket === unix_listener_socket - @unix_server = server_cast(unix_listener_socket) - assert_equal @unix_listener.fileno, @unix_server.fileno - assert UNIXServer === @unix_server - assert_equal(@unix_server.path, @unix_listener.path, - "##{@unix_server.path} != #{@unix_listener.path}") - assert File.socket?(@unix_server.path) - assert_equal @unix_listener_path, sock_name(@unix_server) - - tcp_listener_socket = Socket.for_fd(@tcp_listener.fileno) - assert Socket === tcp_listener_socket - @tcp_server = server_cast(tcp_listener_socket) - assert_equal @tcp_listener.fileno, @tcp_server.fileno - assert TCPServer === @tcp_server - assert_equal @tcp_listener_name, sock_name(@tcp_server) - end - - def test_sock_name - test_server_cast - sock_name(@unix_server) - end - def test_tcp_defer_accept_default return unless defined?(TCP_DEFER_ACCEPT) port = unused_port @test_addr diff --git a/test/unit/test_stream_input.rb b/test/unit/test_stream_input.rb index 1a07ec3..7ee98e4 100644 --- a/test/unit/test_stream_input.rb +++ b/test/unit/test_stream_input.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require 'test/unit' require 'digest/sha1' @@ -6,16 +7,16 @@ require 'unicorn' class TestStreamInput < Test::Unit::TestCase def setup - @rs = $/ + @rs = "\n" + $/ == "\n" or abort %q{test broken if \$/ != "\\n"} @env = {} - @rd, @wr = Kgio::UNIXSocket.pair + @rd, @wr = UNIXSocket.pair @rd.sync = @wr.sync = true @start_pid = $$ end def teardown return if $$ != @start_pid - $/ = @rs @rd.close rescue nil @wr.close rescue nil Process.waitall @@ -54,11 +55,18 @@ class TestStreamInput < Test::Unit::TestCase end def test_gets_empty_rs - $/ = nil r = init_request("a\nb\n\n") si = Unicorn::StreamInput.new(@rd, r) - assert_equal "a\nb\n\n", si.gets - assert_nil si.gets + pid = fork do # to avoid $/ warning (hopefully) + $/ = nil + @rd.close + @wr.write(si.gets) + @wr.close + end + @wr.close + assert_equal "a\nb\n\n", @rd.read + pid, status = Process.waitpid2(pid) + assert_predicate status, :success? end def test_read_with_equal_len @@ -90,21 +98,21 @@ class TestStreamInput < Test::Unit::TestCase end def test_gets_long - r = init_request("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size) + r = init_request("hello", 5 + (4096 * 4 * 3) + "#{@rs}foo#{@rs}".size) si = Unicorn::StreamInput.new(@rd, r) status = line = nil pid = fork { @rd.close 3.times { @wr.write("ffff" * 4096) } - @wr.write "#$/foo#$/" + @wr.write "#{@rs}foo#{@rs}" @wr.close } @wr.close line = si.gets assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size) - assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line) + assert_equal("hello" << ("ffff" * 4096 * 3) << "#{@rs}", line) line = si.gets - assert_equal "foo#$/", line + assert_equal "foo#{@rs}", line assert_nil si.gets pid, status = Process.waitpid2(pid) assert status.success? diff --git a/test/unit/test_tee_input.rb b/test/unit/test_tee_input.rb index 4647e66..8f05c77 100644 --- a/test/unit/test_tee_input.rb +++ b/test/unit/test_tee_input.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require 'test/unit' require 'digest/sha1' @@ -9,17 +10,16 @@ class TeeInput < Unicorn::TeeInput end class TestTeeInput < Test::Unit::TestCase - def setup - @rs = $/ - @rd, @wr = Kgio::UNIXSocket.pair + @rd, @wr = UNIXSocket.pair @rd.sync = @wr.sync = true @start_pid = $$ + @rs = "\n" + $/ == "\n" or abort %q{test broken if \$/ != "\\n"} end def teardown return if $$ != @start_pid - $/ = @rs @rd.close rescue nil @wr.close rescue nil begin @@ -37,38 +37,38 @@ class TestTeeInput < Test::Unit::TestCase end def test_gets_long - r = init_request("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size) + r = init_request("hello", 5 + (4096 * 4 * 3) + "#{@rs}foo#{@rs}".size) ti = TeeInput.new(@rd, r) status = line = nil pid = fork { @rd.close 3.times { @wr.write("ffff" * 4096) } - @wr.write "#$/foo#$/" + @wr.write "#{@rs}foo#{@rs}" @wr.close } @wr.close line = ti.gets assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size) - assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line) + assert_equal("hello" << ("ffff" * 4096 * 3) << "#{@rs}", line) line = ti.gets - assert_equal "foo#$/", line + assert_equal "foo#{@rs}", line assert_nil ti.gets pid, status = Process.waitpid2(pid) assert status.success? end def test_gets_short - r = init_request("hello", 5 + "#$/foo".size) + r = init_request("hello", 5 + "#{@rs}foo".size) ti = TeeInput.new(@rd, r) status = line = nil pid = fork { @rd.close - @wr.write "#$/foo" + @wr.write "#{@rs}foo" @wr.close } @wr.close line = ti.gets - assert_equal("hello#$/", line) + assert_equal("hello#{@rs}", line) line = ti.gets assert_equal "foo", line assert_nil ti.gets diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb deleted file mode 100644 index 5de02e4..0000000 --- a/test/unit/test_upload.rb +++ /dev/null @@ -1,306 +0,0 @@ -# -*- encoding: binary -*- - -# Copyright (c) 2009 Eric Wong -require './test/test_helper' -require 'digest/md5' - -include Unicorn - -class UploadTest < Test::Unit::TestCase - - def setup - @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' - @port = unused_port - @hdr = {'Content-Type' => 'text/plain', 'Content-Length' => '0'} - @bs = 4096 - @count = 256 - @server = nil - - # we want random binary data to test 1.9 encoding-aware IO craziness - @random = File.open('/dev/urandom','rb') - @sha1 = Digest::SHA1.new - @sha1_app = lambda do |env| - input = env['rack.input'] - resp = {} - - @sha1.reset - while buf = input.read(@bs) - @sha1.update(buf) - end - resp[:sha1] = @sha1.hexdigest - - # rewind and read again - input.rewind - @sha1.reset - while buf = input.read(@bs) - @sha1.update(buf) - end - - if resp[:sha1] == @sha1.hexdigest - resp[:sysread_read_byte_match] = true - end - - if expect_size = env['HTTP_X_EXPECT_SIZE'] - if expect_size.to_i == input.size - resp[:expect_size_match] = true - end - end - resp[:size] = input.size - resp[:content_md5] = env['HTTP_CONTENT_MD5'] - - [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ] - end - end - - def teardown - redirect_test_io { @server.stop(false) } if @server - @random.close - reset_sig_handlers - end - - def test_put - start_server(@sha1_app) - sock = TCPSocket.new(@addr, @port) - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times do |i| - buf = @random.sysread(@bs) - @sha1.update(buf) - sock.syswrite(buf) - end - read = sock.read.split(/\r\n/) - assert_equal "HTTP/1.1 200 OK", read[0] - resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) - assert_equal length, resp[:size] - assert_equal @sha1.hexdigest, resp[:sha1] - end - - def test_put_content_md5 - md5 = Digest::MD5.new - start_server(@sha1_app) - sock = TCPSocket.new(@addr, @port) - sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \ - "Trailer: Content-MD5\r\n\r\n") - @count.times do |i| - buf = @random.sysread(@bs) - @sha1.update(buf) - md5.update(buf) - sock.syswrite("#{'%x' % buf.size}\r\n") - sock.syswrite(buf << "\r\n") - end - sock.syswrite("0\r\n") - - content_md5 = [ md5.digest! ].pack('m').strip.freeze - sock.syswrite("Content-MD5: #{content_md5}\r\n\r\n") - read = sock.read.split(/\r\n/) - assert_equal "HTTP/1.1 200 OK", read[0] - resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) - assert_equal length, resp[:size] - assert_equal @sha1.hexdigest, resp[:sha1] - assert_equal content_md5, resp[:content_md5] - end - - def test_put_trickle_small - @count, @bs = 2, 128 - start_server(@sha1_app) - assert_equal 256, length - sock = TCPSocket.new(@addr, @port) - hdr = "PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n" - @count.times do - buf = @random.sysread(@bs) - @sha1.update(buf) - hdr << buf - sock.syswrite(hdr) - hdr = '' - sleep 0.6 - end - read = sock.read.split(/\r\n/) - assert_equal "HTTP/1.1 200 OK", read[0] - resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) - assert_equal length, resp[:size] - assert_equal @sha1.hexdigest, resp[:sha1] - end - - def test_put_keepalive_truncates_small_overwrite - start_server(@sha1_app) - sock = TCPSocket.new(@addr, @port) - to_upload = length + 1 - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{to_upload}\r\n\r\n") - @count.times do - buf = @random.sysread(@bs) - @sha1.update(buf) - sock.syswrite(buf) - end - sock.syswrite('12345') # write 4 bytes more than we expected - @sha1.update('1') - - buf = sock.readpartial(4096) - while buf !~ /\r\n\r\n/ - buf << sock.readpartial(4096) - end - read = buf.split(/\r\n/) - assert_equal "HTTP/1.1 200 OK", read[0] - resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) - assert_equal to_upload, resp[:size] - assert_equal @sha1.hexdigest, resp[:sha1] - end - - def test_put_excessive_overwrite_closed - tmp = Tempfile.new('overwrite_check') - tmp.sync = true - start_server(lambda { |env| - nr = 0 - while buf = env['rack.input'].read(65536) - nr += buf.size - end - tmp.write(nr.to_s) - [ 200, @hdr, [] ] - }) - sock = TCPSocket.new(@addr, @port) - buf = ' ' * @bs - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - - @count.times { sock.syswrite(buf) } - assert_raise(Errno::ECONNRESET, Errno::EPIPE) do - ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) } - end - sock.gets - tmp.rewind - assert_equal length, tmp.read.to_i - end - - # Despite reading numerous articles and inspecting the 1.9.1-p0 C - # source, Eric Wong will never trust that we're always handling - # encoding-aware IO objects correctly. Thus this test uses shell - # utilities that should always operate on files/sockets on a - # byte-level. - def test_uncomfortable_with_onenine_encodings - # POSIX doesn't require all of these to be present on a system - which('curl') or return - which('sha1sum') or return - which('dd') or return - - start_server(@sha1_app) - - tmp = Tempfile.new('dd_dest') - assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", - "bs=#{@bs}", "count=#{@count}"), - "dd #@random to #{tmp}") - sha1_re = %r!\b([a-f0-9]{40})\b! - sha1_out = `sha1sum #{tmp.path}` - assert $?.success?, 'sha1sum ran OK' - - assert_match(sha1_re, sha1_out) - sha1 = sha1_re.match(sha1_out)[1] - resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` - assert $?.success?, 'curl ran OK' - assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/sysread_read_byte_match/, resp) - - # small StringIO path - assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", - "bs=1024", "count=1"), - "dd #@random to #{tmp}") - sha1_re = %r!\b([a-f0-9]{40})\b! - sha1_out = `sha1sum #{tmp.path}` - assert $?.success?, 'sha1sum ran OK' - - assert_match(sha1_re, sha1_out) - sha1 = sha1_re.match(sha1_out)[1] - resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` - assert $?.success?, 'curl ran OK' - assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/sysread_read_byte_match/, resp) - end - - def test_chunked_upload_via_curl - # POSIX doesn't require all of these to be present on a system - which('curl') or return - which('sha1sum') or return - which('dd') or return - - start_server(@sha1_app) - - tmp = Tempfile.new('dd_dest') - assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", - "bs=#{@bs}", "count=#{@count}"), - "dd #@random to #{tmp}") - sha1_re = %r!\b([a-f0-9]{40})\b! - sha1_out = `sha1sum #{tmp.path}` - assert $?.success?, 'sha1sum ran OK' - - assert_match(sha1_re, sha1_out) - sha1 = sha1_re.match(sha1_out)[1] - cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \ - -isSf --no-buffer -T- " \ - "http://#@addr:#@port/" - resp = Tempfile.new('resp') - resp.sync = true - - rd, wr = IO.pipe - wr.sync = rd.sync = true - pid = fork { - STDIN.reopen(rd) - rd.close - wr.close - STDOUT.reopen(resp) - exec cmd - } - rd.close - - tmp.rewind - @count.times { |i| - wr.write(tmp.read(@bs)) - sleep(rand / 10) if 0 == i % 8 - } - wr.close - pid, status = Process.waitpid2(pid) - - resp.rewind - resp = resp.read - assert status.success?, 'curl ran OK' - assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/sysread_read_byte_match/, resp) - assert_match(/expect_size_match/, resp) - end - - def test_curl_chunked_small - # POSIX doesn't require all of these to be present on a system - which('curl') or return - which('sha1sum') or return - which('dd') or return - - start_server(@sha1_app) - - tmp = Tempfile.new('dd_dest') - # small StringIO path - assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", - "bs=1024", "count=1"), - "dd #@random to #{tmp}") - sha1_re = %r!\b([a-f0-9]{40})\b! - sha1_out = `sha1sum #{tmp.path}` - assert $?.success?, 'sha1sum ran OK' - - assert_match(sha1_re, sha1_out) - sha1 = sha1_re.match(sha1_out)[1] - resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \ - -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}` - assert $?.success?, 'curl ran OK' - assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/sysread_read_byte_match/, resp) - assert_match(/expect_size_match/, resp) - end - - private - - def length - @bs * @count - end - - def start_server(app) - redirect_test_io do - @server = HttpServer.new(app, :listeners => [ "#{@addr}:#{@port}" ] ) - @server.start - end - end - -end diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb index 9d5d4ef..ce53b86 100644 --- a/test/unit/test_util.rb +++ b/test/unit/test_util.rb @@ -1,4 +1,5 @@ # -*- encoding: binary -*- +# frozen_string_literal: false require './test/test_helper' require 'tempfile' @@ -51,7 +52,7 @@ class TestUtil < Test::Unit::TestCase def test_reopen_logs_renamed_with_encoding tmp = Tempfile.new('') tmp_path = tmp.path.dup.freeze - Encoding.list.each { |encoding| + Encoding.list.sample(5).each { |encoding| File.open(tmp_path, "a:#{encoding.to_s}") { |fp| fp.sync = true assert_equal encoding, fp.external_encoding @@ -74,8 +75,9 @@ class TestUtil < Test::Unit::TestCase def test_reopen_logs_renamed_with_internal_encoding tmp = Tempfile.new('') tmp_path = tmp.path.dup.freeze - Encoding.list.each { |ext| - Encoding.list.each { |int| + full = Encoding.list + full.sample(2).each { |ext| + full.sample(2).each { |int| next if ext == int File.open(tmp_path, "a:#{ext.to_s}:#{int.to_s}") { |fp| fp.sync = true @@ -114,7 +116,7 @@ class TestUtil < Test::Unit::TestCase f_getpipe_sz = 1032 IO.pipe do |a, b| a_sz = a.fcntl(f_getpipe_sz) - b_sz = b.fcntl(f_getpipe_sz) + b.fcntl(f_getpipe_sz) assert_kind_of Integer, a_sz r_sz = r.fcntl(f_getpipe_sz) assert_equal Raindrops::PAGE_SIZE, r_sz diff --git a/test/unit/test_waiter.rb b/test/unit/test_waiter.rb new file mode 100644 index 0000000..a20994b --- /dev/null +++ b/test/unit/test_waiter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: false +require 'test/unit' +require 'unicorn' +require 'unicorn/select_waiter' +class TestSelectWaiter < Test::Unit::TestCase + + def test_select_timeout # n.b. this is level-triggered + sw = Unicorn::SelectWaiter.new + IO.pipe do |r,w| + sw.get_readers(ready = [], [r], 0) + assert_equal [], ready + w.syswrite '.' + sw.get_readers(ready, [r], 1000) + assert_equal [r], ready + sw.get_readers(ready, [r], 0) + assert_equal [r], ready + end + end + + def test_linux # ugh, also level-triggered, unlikely to change + IO.pipe do |r,w| + wtr = Unicorn::Waiter.prep_readers([r]) + wtr.get_readers(ready = [], [r], 0) + assert_equal [], ready + w.syswrite '.' + wtr.get_readers(ready = [], [r], 1000) + assert_equal [r], ready + wtr.get_readers(ready = [], [r], 1000) + assert_equal [r], ready, 'still ready (level-triggered :<)' + assert_nil wtr.close + end + rescue SystemCallError => e + warn "#{e.message} (#{e.class})" + end if Unicorn.const_defined?(:Waiter) +end |