From ab067831e707b191d6dfdcd01de1f1d85fc90d05 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 18 Oct 2013 10:28:18 +0000 Subject: initial commit --- test/covshow.rb | 29 ++++ test/helper.rb | 115 +++++++++++++ test/server_helper.rb | 64 +++++++ test/test_bin.rb | 98 +++++++++++ test/test_config.rb | 56 +++++++ test/test_output_buffering.rb | 288 +++++++++++++++++++++++++++++++ test/test_queue.rb | 59 +++++++ test/test_rack.rb | 26 +++ test/test_serve_static.rb | 42 +++++ test/test_server.rb | 382 ++++++++++++++++++++++++++++++++++++++++++ test/test_stream_file.rb | 30 ++++ test/test_wbuf.rb | 136 +++++++++++++++ 12 files changed, 1325 insertions(+) create mode 100644 test/covshow.rb create mode 100644 test/helper.rb create mode 100644 test/server_helper.rb create mode 100644 test/test_bin.rb create mode 100644 test/test_config.rb create mode 100644 test/test_output_buffering.rb create mode 100644 test/test_queue.rb create mode 100644 test/test_rack.rb create mode 100644 test/test_serve_static.rb create mode 100644 test/test_server.rb create mode 100644 test/test_stream_file.rb create mode 100644 test/test_wbuf.rb (limited to 'test') diff --git a/test/covshow.rb b/test/covshow.rb new file mode 100644 index 0000000..2fd48c6 --- /dev/null +++ b/test/covshow.rb @@ -0,0 +1,29 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +# +# this works with the __covmerge method in test/helper.rb +# run this file after all tests are run + +# load the merged dump data +res = Marshal.load(IO.binread("coverage.dump")) + +# Dirty little text formatter. I tried simplecov but the default +# HTML+JS is unusable without a GUI (I hate GUIs :P) and it would've +# taken me longer to search the Internets to find a plain-text +# formatter I like... +res.keys.sort.each do |filename| + cov = res[filename] + puts "==> #{filename} <==" + File.readlines(filename).each_with_index do |line, i| + n = cov[i] + if n == 0 # BAD + print(" *** 0 #{line}") + elsif n + printf("% 7u %s", n, line) + elsif line =~ /\S/ # probably a line with just "end" in it + print(" #{line}") + else # blank line + print "\n" # don't output trailing whitespace on blank lines + end + end +end diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 0000000..ab9a04f --- /dev/null +++ b/test/helper.rb @@ -0,0 +1,115 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +$stdout.sync = $stderr.sync = Thread.abort_on_exception = true +require 'thread' + +# Global Test Lock, to protect: +# Process.wait*, Dir.chdir, ENV, trap, require, etc... +GTL = Mutex.new + +# fork-aware coverage data gatherer, see also test/covshow.rb +if ENV["COVERAGE"] + require "coverage" + COVMATCH = %r{/lib/yahns\b.*rb\z} + COVTMP = File.open("coverage.dump", IO::CREAT|IO::RDWR) + COVTMP.binmode + COVTMP.sync = true + + def __covmerge + res = Coverage.result + + # we own this file (at least until somebody tries to use NFS :x) + COVTMP.flock(File::LOCK_EX) + + COVTMP.rewind + prev = COVTMP.read + prev = prev.empty? ? {} : Marshal.load(prev) + res.each do |filename, counts| + # filter out stuff that's not in our project + COVMATCH =~ filename or next + + merge = prev[filename] || [] + merge = merge + counts.each_with_index do |count, i| + count or next + merge[i] = (merge[i] || 0) + count + end + prev[filename] = merge + end + COVTMP.rewind + COVTMP.truncate(0) + COVTMP.write(Marshal.dump(prev)) + COVTMP.flock(File::LOCK_UN) + end + + Coverage.start + at_exit { at_exit { __covmerge } } +end + +gem 'minitest' +require 'minitest/autorun' +require "tempfile" + +Testcase = begin + Minitest::Test # minitest 5 +rescue NameError + Minitest::Unit::TestCase # minitest 4 +end + +FIFOS = [] +def tmpfifo + tmp = Tempfile.new(%w(yahns-test .fifo)) + path = tmp.path + tmp.close! + assert system(*%W(mkfifo #{path})), "mkfifo #{path}" + + GTL.synchronize do + if FIFOS.empty? + at_exit do + FIFOS.each { |(pid,_path)| File.unlink(_path) if $$ == pid } + end + end + FIFOS << [ $$, path ] + end + path +end + +require 'tmpdir' +class Dir + require 'fileutils' + def Dir.mktmpdir + begin + d = "#{Dir.tmpdir}/#$$.#{rand}" + Dir.mkdir(d) + rescue Errno::EEXIST + end while true + begin + yield d + ensure + FileUtils.remove_entry(d) + end + end +end unless Dir.respond_to?(:mktmpdir) + +def tmpfile(*args) + tmp = Tempfile.new(*args) + tmp.sync = true + tmp.binmode + tmp +end + +require 'io/wait' +# needed for Rubinius 2.0.0, we only use IO#nread in tests +class IO + # this ignores buffers + def nread + buf = "\0" * 8 + ioctl(0x541B, buf) + buf.unpack("l_")[0] + end +end if ! IO.method_defined?(:nread) && RUBY_PLATFORM =~ /linux/ + +require 'yahns' + +# needed for parallel (MT) tests) +require 'yahns/rack' diff --git a/test/server_helper.rb b/test/server_helper.rb new file mode 100644 index 0000000..78d2f94 --- /dev/null +++ b/test/server_helper.rb @@ -0,0 +1,64 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'helper' +require 'timeout' +require 'socket' +require 'net/http' + +module ServerHelper + def check_err(err = @err) + err = File.open(err.path, "r") if err.respond_to?(:path) + err.rewind + lines = err.readlines.delete_if { |l| l =~ /INFO/ } + assert lines.empty?, lines.join("\n") + err.close + end + + def poke_until_dead(pid) + Timeout.timeout(10) do + begin + Process.kill(0, pid) + sleep(0.01) + rescue Errno::ESRCH + break + end while true + end + assert_raises(Errno::ESRCH) { Process.kill(0, pid) } + end + + def quit_wait(pid) + pid or return + Process.kill(:QUIT, pid) + _, status = Timeout.timeout(10) { Process.waitpid2(pid) } + assert status.success?, status.inspect + rescue Timeout::Error + if RUBY_PLATFORM =~ /linux/ + system("lsof -p #{pid}") + warn "#{pid} failed to die, waiting for user to inspect" + sleep + end + raise + end + + def get_tcp_client(host, port, tries = 500) + begin + c = TCPSocket.new(host, port) + return c + rescue Errno::ECONNREFUSED + raise if tries < 0 + tries -= 1 + end while sleep(0.01) + end + + def server_helper_teardown + @srv.close unless @srv.closed? + @ru.close! if @ru + check_err + end + + def server_helper_setup + @srv = TCPServer.new(ENV["TEST_HOST"] || "127.0.0.1", 0) + @err = tmpfile(%w(srv .err)) + @ru = nil + end +end diff --git a/test/test_bin.rb b/test/test_bin.rb new file mode 100644 index 0000000..4a47a93 --- /dev/null +++ b/test/test_bin.rb @@ -0,0 +1,98 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'server_helper' +class TestBin < Testcase + parallelize_me! + include ServerHelper + alias teardown server_helper_teardown + + def setup + server_helper_setup + @cmd = %W(ruby -I lib bin/yahns) + end + + def test_bin_daemon_noworker_inherit + bin_daemon(false, true) + end + + def test_bin_daemon_worker_inherit + bin_daemon(true, true) + end + + def test_bin_daemon_noworker_bind + bin_daemon(false, false) + end + + def test_bin_daemon_worker_bind + bin_daemon(true, false) + end + + def bin_daemon(worker, inherit) + @srv.close unless inherit + @pid = tmpfile(%w(test_bin_daemon .pid)) + @ru = tmpfile(%w(test_bin_daemon .ru)) + @ru.write("require 'rack/lobster'; run Rack::Lobster.new\n") + cfg = tmpfile(%w(test_bin_daemon_conf .rb)) + pid = tmpfile(%w(daemon .pid)) + cfg.puts "pid '#{@pid.path}'" + cfg.puts "stderr_path '#{@err.path}'" + cfg.puts "worker_processes 1" if worker + cfg.puts "app(:rack, '#{@ru.path}', preload: false) do" + cfg.puts " listen ENV['YAHNS_TEST_LISTEN']" + cfg.puts "end" + @cmd.concat(%W(-D -c #{cfg.path})) + addr = IO.pipe + pid = fork do + if inherit + @cmd << { @srv.fileno => @srv } + ENV["YAHNS_FD"] = @srv.fileno.to_s + else + @srv = TCPServer.new(ENV["TEST_HOST"] || "127.0.0.1", 0) + end + host, port = @srv.addr[3], @srv.addr[1] + listen = ENV["YAHNS_TEST_LISTEN"] = "#{host}:#{port}" + addr[1].write(listen) + addr[1].close + addr[0].close + exec(*@cmd) + end + addr[1].close + listen = Timeout.timeout(10) { addr[0].read } + addr[0].close + host, port = listen.split(/:/, 2) + port = port.to_i + assert_operator port, :>, 0 + + unless inherit + # daemon_pipe guarantees socket will be usable after this: + Timeout.timeout(10) do # Ruby startup is slow! + _, status = Process.waitpid2(pid) + assert status.success?, status.inspect + end + end + + Net::HTTP.start(host, port) do |http| + req = Net::HTTP::Get.new("/") + res = http.request(req) + assert_equal 200, res.code.to_i + assert_equal "keep-alive", res["Connection"] + end + rescue => e + warn "#{e.message} (#{e.class})" + e.backtrace.each { |l| warn "#{l}" } + raise + ensure + cfg.close! if cfg + pid = File.read(@pid.path) + pid = pid.to_i + assert_operator pid, :>, 0 + Process.kill(:QUIT, pid) + if inherit + _, status = Timeout.timeout(10) { Process.waitpid2(pid) } + assert status.success?, status.inspect + else + poke_until_dead pid + end + @pid.close! if @pid + end +end diff --git a/test/test_config.rb b/test/test_config.rb new file mode 100644 index 0000000..2afcecb --- /dev/null +++ b/test/test_config.rb @@ -0,0 +1,56 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'helper' +require 'rack/lobster' + +class TestConfig < Testcase + parallelize_me! + + def test_initialize + cfg = Yahns::Config.new + assert_instance_of Yahns::Config, cfg + end + + def test_multi_conf_example + tmpdir = Dir.mktmpdir + + # modify the example config file for testing + path = "examples/yahns_multi.conf.rb" + cfgs = File.read(path) + cfgs.gsub!(%r{/path/to/}, "#{tmpdir}/") + conf = File.open("#{tmpdir}/yahns_multi.conf.rb", "w") + conf.sync = true + conf.write(cfgs) + File.open("#{tmpdir}/another.ru", "w") do |fp| + fp.puts("run Rack::Lobster.new\n") + end + FileUtils.mkpath("#{tmpdir}/another") + + cfg = GTL.synchronize { Yahns::Config.new(conf.path) } + assert_instance_of Yahns::Config, cfg + ensure + FileUtils.rm_rf(tmpdir) if tmpdir + end + + def test_rack_basic_conf_example + tmpdir = Dir.mktmpdir + + # modify the example config file for testing + path = "examples/yahns_rack_basic.conf.rb" + cfgs = File.read(path) + cfgs.gsub!(%r{/path/to/}, "#{tmpdir}/") + Dir.mkdir("#{tmpdir}/my_app") + Dir.mkdir("#{tmpdir}/my_logs") + Dir.mkdir("#{tmpdir}/my_pids") + conf = File.open("#{tmpdir}/yahns_rack_basic.conf.rb", "w") + conf.sync = true + conf.write(cfgs) + File.open("#{tmpdir}/my_app/config.ru", "w") do |fp| + fp.puts("run Rack::Lobster.new\n") + end + cfg = GTL.synchronize { Yahns::Config.new(conf.path) } + assert_instance_of Yahns::Config, cfg + ensure + FileUtils.rm_rf(tmpdir) if tmpdir + end +end diff --git a/test/test_output_buffering.rb b/test/test_output_buffering.rb new file mode 100644 index 0000000..6fe22ba --- /dev/null +++ b/test/test_output_buffering.rb @@ -0,0 +1,288 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'server_helper' +require 'digest/md5' +require 'rack/file' + +class TestOutputBuffering < Testcase + parallelize_me! + include ServerHelper + alias setup server_helper_setup + alias teardown server_helper_teardown + + GPLv3 = File.read("COPYING") + RAND = IO.binread("/dev/urandom", 666) * 119 + dig = Digest::MD5.new + NR = 1337 + MD5 = Thread.new do + NR.times { dig << RAND } + dig.hexdigest + end + + class BigBody + def each + NR.times { yield RAND } + end + end + + def test_output_buffer_false_curl + output_buffer(false, :curl) + end + + def test_output_buffer_false_http09 + output_buffer(false, :http09) + end + + def test_output_buffer_true_curl + output_buffer(true, :curl) + end + + def test_output_buffer_true_http09 + output_buffer(true, :http09) + end + + def output_buffer(btype, check_type, delay = 4) + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + len = (RAND.size * NR).to_s + cfg.instance_eval do + ru = lambda do |e| + [ 200, {'Content-Length'=>len}, BigBody.new ] + end + GTL.synchronize do + app(:rack, ru) do + listen "#{host}:#{port}" + output_buffering btype + end + end + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + + case check_type + when :curl + # curl is faster for piping gigantic wads of data than Net::HTTP + sh_sleep = delay ? "sleep #{delay} && " : "" + md5 = `curl -sSf http://#{host}:#{port}/ | (#{sh_sleep} md5sum)` + assert $?.success?, $?.inspect + (md5 =~ /([a-f0-9]{32})/i) or raise "bad md5: #{md5.inspect}" + md5 = $1 + assert_equal MD5.value, md5 + when :http09 + # HTTP/0.9 + c = TCPSocket.new(host, port) + c.write("GET /\r\n\r\n") + md5in = IO.pipe + md5out = IO.pipe + sleep(delay) if delay + md5pid = Process.spawn("md5sum", :in => md5in[0], :out => md5out[1]) + md5in[0].close + md5out[1].close + assert_equal(NR * RAND.size, IO.copy_stream(c, md5in[1])) + c.close + md5in[1].close + _, status = Timeout.timeout(10) { Process.waitpid2(md5pid) } + assert status.success?, status.inspect + md5 = md5out[0].read + (md5 =~ /([a-f0-9]{32})/i) or raise "bad md5: #{md5.inspect}" + md5 = $1 + assert_equal MD5.value, md5 + md5out[0].close + else + raise "TESTBUG" + end + rescue => e + Yahns::Log.exception(Logger.new($stderr), "test", e) + raise + ensure + quit_wait(pid) + end + + class BigHeader + A = "A" * 65536 + def initialize(h) + @h = h + end + def each + NR.times do |n| + yield("X-#{n}", A) + end + @h.each { |k,v| yield(k,v) } + end + end + + def test_big_header + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + ru = lambda do |e| + case e["PATH_INFO"] + when "/COPYING" + Rack::File.new(Dir.pwd).call(e) + gplv3 = File.open("COPYING") + def gplv3.each + raise "SHOULD NOT BE CALLED" + end + size = gplv3.stat.size + len = size.to_s + ranges = Rack::Utils.byte_ranges(e, size) + status = 200 + h = { "Content-Type" => "text/plain", "Content-Length" => len } + if ranges && ranges.size == 1 + status = 206 + range = ranges[0] + h["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}" + size = range.end - range.begin + 1 + len.replace(size.to_s) + end + [ status , BigHeader.new(h), gplv3 ] + when "/" + h = { "Content-Type" => "text/plain", "Content-Length" => "4" } + [ 200, BigHeader.new(h), ["BIG\n"] ] + else + raise "WTF" + end + end + GTL.synchronize do + app(:rack, ru) do + listen "#{host}:#{port}" + end + end + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + threads = [] + + # start with just a big header + threads << Thread.new do + c = TCPSocket.new(host, port) + c.write "GET / HTTP/1.0\r\n\r\n" + begin + sleep 1 + end while c.nread == 0 + nr = 0 + last = nil + c.each_line do |line| + case line + when %r{\AX-} then nr += 1 + else + last = line + end + end + assert_equal NR, nr + assert_equal "BIG\n", last + c.close + end + + threads << Thread.new do + c = TCPSocket.new(host, port) + c.write "GET /COPYING HTTP/1.0\r\n\r\n" + begin + sleep 1 + end while c.nread == 0 + nr = 0 + c.each_line do |line| + case line + when %r{\AX-} then nr += 1 + else + break if line == "\r\n" + end + end + assert_equal NR, nr + assert_equal GPLv3, c.read + c.close + end + + threads << Thread.new do + c = TCPSocket.new(host, port) + c.write "GET /COPYING HTTP/1.0\r\nRange: bytes=5-46\r\n\r\n" + begin + sleep 1 + end while c.nread == 0 + nr = 0 + c.each_line do |line| + case line + when %r{\AX-} then nr += 1 + else + break if line == "\r\n" + end + end + assert_equal NR, nr + assert_equal GPLv3[5..46], c.read + c.close + end + threads.each do |t| + assert_equal t, t.join(30) + assert_nil t.value + end + ensure + quit_wait(pid) + end + + def test_client_timeout + err = @err + apperr = tmpfile(%w(app .err)) + cfg = Yahns::Config.new + size = RAND.size * NR + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + ru = lambda do |e| + if e["PATH_INFO"] == "/bh" + h = { "Content-Type" => "text/plain", "Content-Length" => "4" } + [ 200, BigHeader.new(h), ["BIG\n"] ] + else + [ 200, {'Content-Length' => size.to_s }, BigBody.new ] + end + end + GTL.synchronize do + app(:rack, ru) do + listen "#{host}:#{port}" + output_buffering false + client_timeout 3 + logger(Logger.new(apperr.path)) + end + end + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + threads = [] + threads << Thread.new do + c = get_tcp_client(host, port) + c.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + sleep(5) # wait for timeout + assert_operator c.nread, :>, 0 + c + end + + threads << Thread.new do + c = get_tcp_client(host, port) + c.write("GET /bh HTTP/1.1\r\nHost: example.com\r\n\r\n") + sleep(5) # wait for timeout + assert_operator c.nread, :>, 0 + c + end + threads.each { |t| t.join(10) } + assert_operator size, :>, threads[0].value.read.size + assert_operator size, :>, threads[1].value.read.size + msg = File.readlines(apperr.path) + msg = msg.grep(/timeout on :wait_writable after 3s$/) + assert_equal 2, msg.size + ensure + quit_wait(pid) + end +end if `which curl 2>/dev/null`.strip =~ /curl/ && + `which md5sum 2>/dev/null`.strip =~ /md5sum/ diff --git a/test/test_queue.rb b/test/test_queue.rb new file mode 100644 index 0000000..6d61aef --- /dev/null +++ b/test/test_queue.rb @@ -0,0 +1,59 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'helper' +require 'timeout' +require 'stringio' + +class TestQueue < Testcase + parallelize_me! + + def setup + @q = Yahns::Queue.new + @err = StringIO.new + @logger = Logger.new(@err) + @q.fdmap = @fdmap = Yahns::Fdmap.new(@logger, 0.5) + assert @q.close_on_exec? + end + + def test_queue + r, w = IO.pipe + assert_equal 0, @fdmap.size + @q.queue_add(r, Yahns::Queue::QEV_RD) + assert_equal 1, @fdmap.size + def r.yahns_step + begin + case read_nonblock(11) + when "delete" + return :delete + end + rescue Errno::EAGAIN + return :wait_readable + rescue EOFError + return nil + end while true + end + w.write('.') + Timeout.timeout(10) do + Thread.pass until r.nread > 0 + @q.spawn_worker_threads(@logger, 1, 1) + Thread.pass until r.nread == 0 + + w.write("delete") + Thread.pass until r.nread == 0 + Thread.pass until @fdmap.size == 0 + + # should not raise + @q.queue_add(r, Yahns::Queue::QEV_RD) + assert_equal 1, @fdmap.size + w.close + Thread.pass until @fdmap.size == 0 + end + assert r.closed? + ensure + [ r, w ].each { |io| io.close unless io.closed? } + end + + def teardown + @q.close + end +end diff --git a/test/test_rack.rb b/test/test_rack.rb new file mode 100644 index 0000000..bd0d5b5 --- /dev/null +++ b/test/test_rack.rb @@ -0,0 +1,26 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'helper' +require 'rack/lobster' +require 'yahns/rack' +class TestRack < Testcase + parallelize_me! + + def test_rack + tmp = tmpfile(%W(config .ru)) + tmp.write "run Rack::Lobster.new\n" + rapp = GTL.synchronize { Yahns::Rack.new(tmp.path) } + assert_kind_of Rack::Lobster, GTL.synchronize { rapp.app_after_fork } + defaults = rapp.app_defaults + assert_kind_of Hash, defaults + end + + def test_rack_preload + tmp = tmpfile(%W(config .ru)) + tmp.write "run Rack::Lobster.new\n" + rapp = GTL.synchronize { Yahns::Rack.new(tmp.path, preload: true) } + assert_kind_of Rack::Lobster, rapp.instance_variable_get(:@app) + defaults = rapp.app_defaults + assert_kind_of Hash, defaults + end +end diff --git a/test/test_serve_static.rb b/test/test_serve_static.rb new file mode 100644 index 0000000..b9856e9 --- /dev/null +++ b/test/test_serve_static.rb @@ -0,0 +1,42 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'server_helper' +require 'rack/file' + +class TestServeStatic < Testcase + parallelize_me! + include ServerHelper + alias setup server_helper_setup + alias teardown server_helper_teardown + + def test_serve_static + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + GTL.synchronize do + app(:rack, Rack::File.new(Dir.pwd)) { listen "#{host}:#{port}" } + end + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + gplv3 = File.read("COPYING") + Net::HTTP.start(host, port) do |http| + res = http.request(Net::HTTP::Get.new("/COPYING")) + assert_equal gplv3, res.body + + req = Net::HTTP::Get.new("/COPYING", "Range" => "bytes=5-46") + res = http.request(req) + assert_equal gplv3[5..46], res.body + end + rescue => e + Yahns::Log.exception(Logger.new($stderr), "test", e) + raise + ensure + quit_wait(pid) + end +end diff --git a/test/test_server.rb b/test/test_server.rb new file mode 100644 index 0000000..5c86268 --- /dev/null +++ b/test/test_server.rb @@ -0,0 +1,382 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'server_helper' + +class TestServer < Testcase + parallelize_me! + include ServerHelper + + alias setup server_helper_setup + alias teardown server_helper_teardown + + def test_single_process + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + ru = lambda { |_| [ 200, {'Content-Length'=>'2'}, ['HI'] ] } + GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } } + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + run_client(host, port) { |res| assert_equal "HI", res.body } + c = get_tcp_client(host, port) + + # test pipelining + r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + c.write(r + r) + buf = "" + Timeout.timeout(10) do + until buf =~ /HI.+HI/m + buf << c.readpartial(4096) + end + end + + # trickle pipelining + c.write(r + "GET ") + buf = "" + Timeout.timeout(10) do + until buf =~ /HI\z/ + buf << c.readpartial(4096) + end + end + c.write("/ HTTP/1.1\r\nHost: example.com\r\n\r\n") + Timeout.timeout(10) do + until buf =~ /HI.+HI/m + buf << c.readpartial(4096) + end + end + rescue => e + Yahns::Log.exception(Logger.new($stderr), "test", e) + raise + ensure + c.close if c + quit_wait(pid) + end + + def test_input_body_true; input_body(true); end + def test_input_body_false; input_body(false); end + def test_input_body_lazy; input_body(:lazy); end + + def input_body(btype) + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + ru = lambda {|e|[ 200, {'Content-Length'=>'2'},[e["rack.input"].read]]} + GTL.synchronize do + app(:rack, ru) do + listen "#{host}:#{port}" + input_buffering btype + end + end + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + c = get_tcp_client(host, port) + buf = "PUT / HTTP/1.0\r\nContent-Length: 2\r\n\r\nHI" + c.write(buf) + IO.select([c], nil, nil, 5) + rv = c.read(666) + head, body = rv.split(/\r\n\r\n/) + assert_match(%r{^Content-Length: 2\r\n}, head) + assert_equal "HI", body, "#{rv.inspect} - #{btype.inspect}" + c.close + + # pipelined oneshot + buf = "PUT / HTTP/1.1\r\nContent-Length: 2\r\n\r\nHI" + c = get_tcp_client(host, port) + c.write(buf + buf) + buf = "" + Timeout.timeout(10) do + until buf =~ /HI.+HI/m + buf << c.readpartial(4096) + end + end + assert buf.gsub!(/Date:[^\r\n]+\r\n/, ""), "kill differing Date" + rv = buf.sub!(/\A(HTTP.+?\r\n\r\nHI)/m, "") + first = $1 + assert rv + assert_equal first, buf + + # pipelined trickle + buf = "PUT / HTTP/1.1\r\nContent-Length: 5\r\n\r\nHIBYE" + (buf + buf).each_byte do |b| + c.write(b.chr) + sleep(0.01) if b.chr == ":" + Thread.pass + end + buf = "" + Timeout.timeout(10) do + until buf =~ /HIBYE.+HIBYE/m + buf << c.readpartial(4096) + end + end + assert buf.gsub!(/Date:[^\r\n]+\r\n/, ""), "kill differing Date" + rv = buf.sub!(/\A(HTTP.+?\r\n\r\nHIBYE)/m, "") + first = $1 + assert rv + assert_equal first, buf + rescue => e + Yahns::Log.exception(Logger.new($stderr), "test", e) + raise + ensure + c.close if c + quit_wait(pid) + end + + def test_trailer_true; trailer(true); end + def test_trailer_false; trailer(false); end + def test_trailer_lazy; trailer(:lazy); end + def test_slow_trailer_true; trailer(true, 0.02); end + def test_slow_trailer_false; trailer(false, 0.02); end + def test_slow_trailer_lazy; trailer(:lazy, 0.02); end + + def trailer(btype, delay = false) + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + ru = lambda do |e| + body = e["rack.input"].read + s = e["HTTP_XBT"] + "\n" + body + [ 200, {'Content-Length'=>s.size.to_s}, [ s ] ] + end + GTL.synchronize do + app(:rack, ru) do + listen "#{host}:#{port}" + input_buffering btype + end + end + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + c = get_tcp_client(host, port) + buf = "PUT / HTTP/1.0\r\nTrailer:xbt\r\nTransfer-Encoding: chunked\r\n\r\n" + c.write(buf) + xbt = btype.to_s + sleep(delay) if delay + c.write(sprintf("%x\r\n", xbt.size)) + sleep(delay) if delay + c.write(xbt) + sleep(delay) if delay + c.write("\r\n") + sleep(delay) if delay + c.write("0\r\nXBT: ") + sleep(delay) if delay + c.write("#{xbt}\r\n\r\n") + IO.select([c], nil, nil, 5000) or raise "timed out" + rv = c.read(666) + _, body = rv.split(/\r\n\r\n/) + a, b = body.split(/\n/) + assert_equal xbt, a + assert_equal xbt, b + ensure + c.close if c + quit_wait(pid) + end + + def test_check_client_connection + msgs = %w(ZZ zz) + err = @err + cfg = Yahns::Config.new + bpipe = IO.pipe + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + ru = lambda { |e| + case e['PATH_INFO'] + when '/sleep' + a = Object.new + a.instance_variable_set(:@bpipe, bpipe) + a.instance_variable_set(:@msgs, msgs) + def a.each + @msgs.each do |msg| + yield @bpipe[0].read(msg.size) + end + end + when '/cccfail' + # we should not get here if check_client_connection worked + abort "CCCFAIL" + else + a = %w(HI) + end + [ 200, {'Content-Length'=>'2'}, a ] + } + GTL.synchronize { + app(:rack, ru) { + listen "#{host}:#{port}" + check_client_connection true + # needed to avoid concurrency with check_client_connection + queue { worker_threads 1 } + output_buffering false + } + } + logger(Logger.new(err.path)) + end + srv = Yahns::Server.new(cfg) + + # ensure we set worker_threads correctly + eggs = srv.instance_variable_get(:@config).qeggs + assert_equal 1, eggs.size + assert_equal 1, eggs[:default].instance_variable_get(:@worker_threads) + + pid = fork do + bpipe[1].close + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + bpipe[0].close + a = get_tcp_client(host, port) + b = get_tcp_client(host, port) + a.write("GET /sleep HTTP/1.0\r\n\r\n") + r = IO.select([a], nil, nil, 4) + assert r, "nothing ready" + assert_equal a, r[0][0] + buf = a.read(8) + assert_equal "HTTP/1.1", buf + + # hope the kernel sees this before it sees the bpipe ping-ponging below + b.write("GET /cccfail HTTP/1.0\r\n\r\n") + b.shutdown + b.close + + # ping-pong a bit to stall the server + msgs.each do |msg| + bpipe[1].write(msg) + Timeout.timeout(10) { buf << a.readpartial(10) until buf =~ /#{msg}/ } + end + bpipe[1].close + assert_equal msgs.join, buf.split(/\r\n\r\n/)[1] + + # do things still work? + run_client(host, port) { |res| assert_equal "HI", res.body } + ensure + quit_wait(pid) + end + + def test_mp + pid, host, port = new_mp_server + wpid = nil + run_client(host, port) do |res| + wpid ||= res.body.to_i + end + ensure + quit_wait(pid) + if wpid + assert_raises(Errno::ESRCH) { Process.kill(:KILL, wpid) } + assert_raises(Errno::ECHILD) { Process.waitpid2(wpid) } + end + end + + # Linux blocking accept() has fair behavior between multiple tasks + def test_mp_balance + skip("linux-only test") unless RUBY_PLATFORM =~ /linux/ + pid, host, port = new_mp_server(2) + seen = {} + + # wait for both processes to spin up + Timeout.timeout(10) do + run_client(host, port) { |res| seen[res.body] = 1 } until seen.size == 2 + end + + prev = nil + req = Net::HTTP::Get.new("/") + # we should bounce new connections between 2 processes + 4.times do + Net::HTTP.start(host, port) do |http| + res = http.request(req) + assert_equal 200, res.code.to_i + assert_equal "keep-alive", res["Connection"] + refute_equal prev, res.body, "same PID accepted twice" + prev = res.body.dup + seen[prev] += 1 + 666.times { Thread.pass } # have the other acceptor to wake up + end + end + assert_equal 2, seen.size + ensure + quit_wait(pid) + end + + def test_mp_worker_die + pid, host, port = new_mp_server + wpid1 = wpid2 = nil + run_client(host, port) do |res| + wpid1 ||= res.body.to_i + end + Process.kill(:QUIT, wpid1) + poke_until_dead(wpid1) + run_client(host, port) do |res| + wpid2 ||= res.body.to_i + end + refute_equal wpid2, wpid1 + ensure + quit_wait(pid) + assert_raises(Errno::ESRCH) { Process.kill(:KILL, wpid2) } if wpid2 + end + + def test_mp_dead_parent + pid, host, port = new_mp_server + wpid = nil + run_client(host, port) do |res| + wpid ||= res.body.to_i + end + Process.kill(:KILL, pid) + _, status = Process.waitpid2(pid) + assert status.signaled?, status.inspect + poke_until_dead(wpid) + end + + def run_client(host, port) + c = get_tcp_client(host, port) + Net::HTTP.start(host, port) do |http| + res = http.request(Net::HTTP::Get.new("/")) + assert_equal 200, res.code.to_i + assert_equal "keep-alive", res["Connection"] + yield res + res = http.request(Net::HTTP::Get.new("/")) + assert_equal 200, res.code.to_i + assert_equal "keep-alive", res["Connection"] + yield res + end + c.write "GET / HTTP/1.0\r\n\r\n" + res = Timeout.timeout(10) { c.read } + head, _ = res.split(/\r\n\r\n/) + head = head.split(/\r\n/) + assert_equal "HTTP/1.1 200 OK", head[0] + assert_equal "Connection: close", head[-1] + c.close + end + + def new_mp_server(nr = 1) + ru = @ru = tmpfile(%w(config .ru)) + @ru.puts('a = $$.to_s') + @ru.puts('run lambda { |_| [ 200, {"Content-Length"=>a.size.to_s},[a]]}') + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + worker_processes 2 + GTL.synchronize { app(:rack, ru.path) { listen "#{host}:#{port}" } } + logger(Logger.new(File.open(err.path, "a"))) + end + srv = Yahns::Server.new(cfg) + pid = fork do + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + [ pid, host, port ] + end +end diff --git a/test/test_stream_file.rb b/test/test_stream_file.rb new file mode 100644 index 0000000..6574a97 --- /dev/null +++ b/test/test_stream_file.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'helper' +require 'timeout' + +class TestStreamFile < Testcase + parallelize_me! + DevFD = Struct.new(:to_path) + + def test_stream_file + fp = File.open("COPYING") + sf = Yahns::StreamFile.new(fp, true, 0, fp.stat.size) + refute sf.respond_to?(:close) + sf.wbuf_close(nil) + assert fp.closed? + end + + def test_fd + fp = File.open("COPYING") + obj = DevFD.new("/dev/fd/#{fp.fileno}") + sf = Yahns::StreamFile.new(obj, true, 0, fp.stat.size) + io = sf.instance_variable_get :@tmpio + assert_instance_of IO, io.to_io + assert_equal fp.fileno, io.fileno + refute sf.respond_to?(:close) + sf.wbuf_close(nil) + refute fp.closed? + refute io.closed? + end +end diff --git a/test/test_wbuf.rb b/test/test_wbuf.rb new file mode 100644 index 0000000..dc6bc24 --- /dev/null +++ b/test/test_wbuf.rb @@ -0,0 +1,136 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'helper' +require 'timeout' + +class TestWbuf < Testcase + parallelize_me! + + def test_wbuf + buf = "*" * (16384 * 2) + nr = 1000 + [ true, false ].each do |persist| + wbuf = Yahns::Wbuf.new([], persist) + a, b = UNIXSocket.pair + assert_nil wbuf.wbuf_write(a, "HIHI") + assert_equal "HIHI", b.read(4) + nr.times { wbuf.wbuf_write(a, buf) } + assert_equal :wait_writable, wbuf.wbuf_flush(a) + done = IO.pipe + thr = Thread.new do + rv = [] + until rv[-1] == persist + IO.select(nil, [a]) + tmp = wbuf.wbuf_flush(a) + rv << tmp + end + done[1].syswrite '.' + rv + end + + wait = true + begin + if wait + r = IO.select([b,done[0]], nil, nil, 5) + end + b.read_nonblock((rand * 1024) + 666, buf) + wait = (r[0] & done).empty? + rescue Errno::EAGAIN + break + end while true + + assert_equal thr, thr.join(5) + rv = thr.value + assert_equal persist, rv.pop + assert(rv.all? { |x| x == :wait_writable }) + a.close + b.close + done.each { |io| io.close } + end + end + + def test_wbuf_blocked + a, b = UNIXSocket.pair + buf = "." * 4096 + 4.times do + begin + a.write_nonblock(buf) + rescue Errno::EAGAIN + break + end while true + end + wbuf = Yahns::Wbuf.new([], true) + assert_equal :wait_writable, wbuf.wbuf_write(a, buf) + assert_equal :wait_writable, wbuf.wbuf_flush(a) + + # drain the buffer + Timeout.timeout(10) { b.read(b.nread) until b.nread == 0 } + + # b.nread will increase after this + assert_nil wbuf.wbuf_write(a, "HI") + nr = b.nread + assert_operator nr, :>, 0 + assert_equal b, IO.select([b], nil, nil, 5)[0][0] + b.read(nr - 2) if nr > 2 + assert_equal b, IO.select([b], nil, nil, 5)[0][0] + assert_equal "HI", b.read(2) + begin + wbuf.wbuf_flush(a) + assert false + rescue => e + end + assert_match(%r{BUG: EOF on tmpio}, e.message) + ensure + a.close + b.close + end + + def test_wbuf_flush_close + pipe = IO.pipe + persist = true + wbuf = Yahns::Wbuf.new(pipe[0], persist) + refute wbuf.respond_to?(:close) # we don't want this for HttpResponse body + sp = UNIXSocket.pair + rv = nil + + buf = ("*" * 16384) << "\n" + thr = Thread.new do + 1000.times { pipe[1].write(buf) } + pipe[1].close + end + + pipe[0].each { |chunk| rv = wbuf.wbuf_write(sp[1], chunk) } + assert_equal thr, thr.join(5) + assert_equal :wait_writable, rv + + done = IO.pipe + thr = Thread.new do + rv = [] + until rv[-1] == persist + IO.select(nil, [sp[1]]) + rv << wbuf.wbuf_flush(sp[1]) + end + done[1].syswrite '.' + rv + end + + wait = true + begin + if wait + r = IO.select([sp[0],done[0]], nil, nil, 5) + end + sp[0].read_nonblock(16384, buf) + wait = (r[0] & done).empty? + rescue Errno::EAGAIN + break + end while true + + assert_equal thr, thr.join(5) + rv = thr.value + assert_equal true, rv.pop + assert rv.all? { |x| x == :wait_writable } + assert pipe[0].closed? + sp.each(&:close) + done.each(&:close) + end +end -- cgit v1.2.3-24-ge0c7