diff options
Diffstat (limited to 'test')
67 files changed, 1838 insertions, 331 deletions
diff --git a/test/aggregate.rb b/test/aggregate.rb index 1c2cc5c..5eebbe5 100755 --- a/test/aggregate.rb +++ b/test/aggregate.rb @@ -1,4 +1,6 @@ #!/usr/bin/ruby -n +# -*- encoding: binary -*- + BEGIN { $tests = $assertions = $failures = $errors = 0 } $_ =~ /(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors/ or next diff --git a/test/benchmark/README b/test/benchmark/README index b63b8a3..1d3cdd0 100644 --- a/test/benchmark/README +++ b/test/benchmark/README @@ -42,11 +42,6 @@ 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). -== request.rb, response.rb, big_request.rb - -These are micro-benchmarks designed to test internal components -of Unicorn. It assumes the internal Unicorn API is mostly stable. - == Contributors This directory is maintained independently in the "benchmark" branch diff --git a/test/benchmark/big_request.rb b/test/benchmark/big_request.rb deleted file mode 100644 index a250c62..0000000 --- a/test/benchmark/big_request.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'benchmark' -require 'tempfile' -require 'unicorn' -nr = ENV['nr'] ? ENV['nr'].to_i : 100 -bs = ENV['bs'] ? ENV['bs'].to_i : (1024 * 1024) -count = ENV['count'] ? ENV['count'].to_i : 4 -length = bs * count -slice = (' ' * bs).freeze - -big = Tempfile.new('') - -def big.unicorn_peeraddr # old versions of Unicorn used this - '127.0.0.1' -end - -big.syswrite( -"PUT /hello/world/puturl?abcd=efg&hi#anchor HTTP/1.0\r\n" \ -"Host: localhost\r\n" \ -"Accept: */*\r\n" \ -"Content-Length: #{length}\r\n" \ -"User-Agent: test-user-agent 0.1.0 (Mozilla compatible) 5.0 asdfadfasda\r\n" \ -"\r\n") -count.times { big.syswrite(slice) } -big.sysseek(0) -big.fsync - -include Unicorn -request = HttpRequest.new(Logger.new($stderr)) -unless request.respond_to?(:reset) - def request.reset - # no-op - end -end - -Benchmark.bmbm do |x| - x.report("big") do - for i in 1..nr - request.read(big) - request.reset - big.sysseek(0) - end - end -end - diff --git a/test/benchmark/request.rb b/test/benchmark/request.rb deleted file mode 100644 index fc7822c..0000000 --- a/test/benchmark/request.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'benchmark' -require 'unicorn' -nr = ENV['nr'] ? ENV['nr'].to_i : 100000 - -class TestClient - def initialize(response) - @response = (response.join("\r\n") << "\r\n\r\n").freeze - end - def sysread(len, buf) - buf.replace(@response) - end - - alias readpartial sysread - - # old versions of Unicorn used this - def unicorn_peeraddr - '127.0.0.1' - end -end - -small = TestClient.new([ - 'GET / HTTP/1.0', - 'Host: localhost', - 'Accept: */*', - 'User-Agent: test-user-agent 0.1.0' -]) - -medium = TestClient.new([ - 'GET /hello/world/geturl?abcd=efg&hi#anchor HTTP/1.0', - 'Host: localhost', - 'Accept: */*', - 'User-Agent: test-user-agent 0.1.0 (Mozilla compatible) 5.0 asdfadfasda' -]) - -include Unicorn -request = HttpRequest.new(Logger.new($stderr)) -unless request.respond_to?(:reset) - def request.reset - # no-op - end -end - -Benchmark.bmbm do |x| - x.report("small") do - for i in 1..nr - request.read(small) - request.reset - end - end - x.report("medium") do - for i in 1..nr - request.read(medium) - request.reset - end - end -end diff --git a/test/benchmark/response.rb b/test/benchmark/response.rb deleted file mode 100644 index cb7397b..0000000 --- a/test/benchmark/response.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'benchmark' -require 'unicorn' - -class NullWriter - def syswrite(buf); buf.size; end - alias write syswrite - def close; end -end - -include Unicorn - -socket = NullWriter.new -bs = ENV['bs'] ? ENV['bs'].to_i : 4096 -count = ENV['count'] ? ENV['count'].to_i : 1 -slice = (' ' * bs).freeze -body = (1..count).map { slice }.freeze -hdr = { - 'Content-Length' => (bs * count).to_s.freeze, - 'Content-Type' => 'text/plain'.freeze -}.freeze -response = [ 200, hdr, body ].freeze - -nr = ENV['nr'] ? ENV['nr'].to_i : 100000 -Benchmark.bmbm do |x| - x.report do - for i in 1..nr - HttpResponse.write(socket.dup, response) - end - end -end diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb index 014b270..24ba856 100644 --- a/test/exec/test_exec.rb +++ b/test/exec/test_exec.rb @@ -1,4 +1,7 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong +FLOCK_PATH = File.expand_path(__FILE__) require 'test/test_helper' do_test = true @@ -25,6 +28,13 @@ use Rack::ContentLength 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'] ] ] +} + EOS + HELLO = <<-EOS class Hello def call(env) @@ -72,11 +82,148 @@ end end end + def test_working_directory_rel_path_config_file + other = Tempfile.new('unicorn.wd') + File.unlink(other.path) + Dir.mkdir(other.path) + File.open("config.ru", "wb") do |fp| + fp.syswrite <<EOF +use Rack::ContentLength +run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ Dir.pwd ] ] } +EOF + end + FileUtils.cp("config.ru", other.path + "/config.ru") + Dir.chdir(@tmpdir) + + tmp = File.open('unicorn.config', 'wb') + tmp.syswrite <<EOF +working_directory '#@tmpdir' +listen '#@addr:#@port' +EOF + pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } } + wait_workers_ready("test_stderr.#{pid}.log", 1) + results = hit(["http://#@addr:#@port/"]) + assert_equal @tmpdir, results.first + File.truncate("test_stderr.#{pid}.log", 0) + + tmp.sysseek(0) + tmp.truncate(0) + tmp.syswrite <<EOF +working_directory '#{other.path}' +listen '#@addr:#@port' +EOF + + Process.kill(:HUP, pid) + lines = [] + re = /config_file=(.+) would not be accessible in working_directory=(.+)/ + until lines.grep(re) + sleep 0.1 + lines = File.readlines("test_stderr.#{pid}.log") + end + + File.truncate("test_stderr.#{pid}.log", 0) + FileUtils.cp('unicorn.config', other.path + "/unicorn.config") + Process.kill(:HUP, pid) + wait_workers_ready("test_stderr.#{pid}.log", 1) + results = hit(["http://#@addr:#@port/"]) + assert_equal other.path, results.first + + Process.kill(:QUIT, pid) + ensure + FileUtils.rmtree(other.path) + end + + def test_working_directory + other = Tempfile.new('unicorn.wd') + File.unlink(other.path) + Dir.mkdir(other.path) + File.open("config.ru", "wb") do |fp| + fp.syswrite <<EOF +use Rack::ContentLength +run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ Dir.pwd ] ] } +EOF + end + FileUtils.cp("config.ru", other.path + "/config.ru") + tmp = Tempfile.new('unicorn.config') + tmp.syswrite <<EOF +working_directory '#@tmpdir' +listen '#@addr:#@port' +EOF + pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } } + wait_workers_ready("test_stderr.#{pid}.log", 1) + results = hit(["http://#@addr:#@port/"]) + assert_equal @tmpdir, results.first + File.truncate("test_stderr.#{pid}.log", 0) + + tmp.sysseek(0) + tmp.truncate(0) + tmp.syswrite <<EOF +working_directory '#{other.path}' +listen '#@addr:#@port' +EOF + + Process.kill(:HUP, pid) + wait_workers_ready("test_stderr.#{pid}.log", 1) + results = hit(["http://#@addr:#@port/"]) + assert_equal other.path, results.first + + Process.kill(:QUIT, pid) + ensure + FileUtils.rmtree(other.path) + end + + def test_working_directory_controls_relative_paths + other = Tempfile.new('unicorn.wd') + File.unlink(other.path) + Dir.mkdir(other.path) + File.open("config.ru", "wb") do |fp| + fp.syswrite <<EOF +use Rack::ContentLength +run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ Dir.pwd ] ] } +EOF + end + FileUtils.cp("config.ru", other.path + "/config.ru") + system('mkfifo', "#{other.path}/fifo") + tmp = Tempfile.new('unicorn.config') + tmp.syswrite <<EOF +pid "pid_file_here" +stderr_path "stderr_log_here" +stdout_path "stdout_log_here" +working_directory '#{other.path}' +listen '#@addr:#@port' +after_fork do |server, worker| + File.open("fifo", "wb").close +end +EOF + pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } } + File.open("#{other.path}/fifo", "rb").close + + assert ! File.exist?("stderr_log_here") + assert ! File.exist?("stdout_log_here") + assert ! File.exist?("pid_file_here") + + assert ! File.exist?("#@tmpdir/stderr_log_here") + assert ! File.exist?("#@tmpdir/stdout_log_here") + assert ! File.exist?("#@tmpdir/pid_file_here") + + assert File.exist?("#{other.path}/pid_file_here") + assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here") + assert File.exist?("#{other.path}/stderr_log_here") + assert File.exist?("#{other.path}/stdout_log_here") + wait_master_ready("#{other.path}/stderr_log_here") + + Process.kill(:QUIT, pid) + ensure + FileUtils.rmtree(other.path) + end + + def test_exit_signals %w(INT TERM QUIT).each do |sig| File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } } wait_master_ready("test_stderr.#{pid}.log") + wait_workers_ready("test_stderr.#{pid}.log", 1) status = nil assert_nothing_raised do Process.kill(sig, pid) @@ -98,6 +245,46 @@ end 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") } } + results = retry_hit(["http://#{@addr}:#{@port}/"]) + assert_equal "development", results.first + assert_shutdown(pid) + end + + def test_rack_env_cli_set + File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) } + pid = fork { + redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") } + } + results = retry_hit(["http://#{@addr}:#{@port}/"]) + assert_equal "asdf", results.first + assert_shutdown(pid) + end + + def test_rack_env_ENV_set + File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) } + pid = fork { + ENV["RACK_ENV"] = "foobar" + redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } + } + results = retry_hit(["http://#{@addr}:#{@port}/"]) + assert_equal "foobar", results.first + assert_shutdown(pid) + end + + def test_rack_env_cli_override_ENV + File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) } + pid = fork { + ENV["RACK_ENV"] = "foobar" + redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") } + } + results = retry_hit(["http://#{@addr}:#{@port}/"]) + assert_equal "asdf", results.first + assert_shutdown(pid) + end + def test_ttin_ttou File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } } @@ -603,6 +790,26 @@ end reexec_usr2_quit_test(new_pid, pid_file) end + def test_daemonize_redirect_fail + pid_file = "#{@tmpdir}/test.pid" + log = Tempfile.new('unicorn_test_log') + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("pid #{pid_file}\"\n") + err = Tempfile.new('stderr') + out = Tempfile.new('stdout ') + + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + pid = xfork do + $stderr.reopen(err.path, "a") + $stdout.reopen(out.path, "a") + exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}") + end + pid, status = Process.waitpid2(pid) + assert ! status.success?, "original process exited successfully" + sleep 1 # can't waitpid on a daemonized process :< + assert err.stat.size > 0 + end + def test_reexec_fd_leak unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget... warn "FD leak test only works on Linux at the moment" @@ -626,6 +833,7 @@ end end wait_master_ready(log.path) + wait_workers_ready(log.path, 1) File.truncate(log.path, 0) wait_for_file(pid_file) orig_pid = pid = File.read(pid_file).to_i @@ -641,6 +849,7 @@ end wait_for_death(pid) wait_master_ready(log.path) + wait_workers_ready(log.path, 1) File.truncate(log.path, 0) wait_for_file(pid_file) pid = File.read(pid_file).to_i @@ -660,6 +869,7 @@ end wait_for_death(pid) wait_master_ready(log.path) + wait_workers_ready(log.path, 1) File.truncate(log.path, 0) wait_for_file(pid_file) pid = File.read(pid_file).to_i @@ -671,4 +881,158 @@ end wait_for_death(pid) end + def hup_test_common(preload) + File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) } + pid_file = Tempfile.new('pid') + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("listen '#@addr:#@port'\n") + ucfg.syswrite("pid '#{pid_file.path}'\n") + ucfg.syswrite("preload_app true\n") if preload + ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n") + ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n") + pid = xfork { + redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) } + } + _, status = Process.waitpid2(pid) + assert status.success? + wait_master_ready("test_stderr.#$$.log") + wait_workers_ready("test_stderr.#$$.log", 1) + uri = URI.parse("http://#@addr:#@port/") + pids = Tempfile.new('worker_pids') + hitter = fork { + bodies = Hash.new(0) + at_exit { pids.syswrite(bodies.inspect) } + trap(:TERM) { exit(0) } + loop { + rv = Net::HTTP.get(uri) + pid = rv.to_i + exit!(1) if pid <= 0 + bodies[pid] += 1 + } + } + sleep 5 # racy + daemon_pid = File.read(pid_file.path).to_i + assert daemon_pid > 0 + Process.kill(:HUP, daemon_pid) + sleep 5 # racy + assert_nothing_raised { Process.kill(:TERM, hitter) } + _, hitter_status = Process.waitpid2(hitter) + assert hitter_status.success? + pids.sysseek(0) + pids = eval(pids.read) + assert_kind_of(Hash, pids) + assert_equal 2, pids.size + pids.keys.each { |x| + assert_kind_of(Integer, x) + assert x > 0 + assert pids[x] > 0 + } + assert_nothing_raised { Process.kill(:QUIT, daemon_pid) } + wait_for_death(daemon_pid) + end + + def test_preload_app_hup + hup_test_common(true) + end + + def test_hup + hup_test_common(false) + end + + def test_default_listen_hup_holds_listener + default_listen_lock do + res, pid_path = default_listen_setup + daemon_pid = File.read(pid_path).to_i + assert_nothing_raised { Process.kill(:HUP, daemon_pid) } + wait_workers_ready("test_stderr.#$$.log", 1) + res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) + assert_match %r{\d+}, res2.first + assert res2.first != res.first + assert_nothing_raised { Process.kill(:QUIT, daemon_pid) } + wait_for_death(daemon_pid) + end + end + + def test_default_listen_upgrade_holds_listener + default_listen_lock do + res, pid_path = default_listen_setup + daemon_pid = File.read(pid_path).to_i + assert_nothing_raised { + Process.kill(:USR2, daemon_pid) + wait_for_file("#{pid_path}.oldbin") + wait_for_file(pid_path) + Process.kill(:QUIT, daemon_pid) + wait_for_death(daemon_pid) + } + daemon_pid = File.read(pid_path).to_i + wait_workers_ready("test_stderr.#$$.log", 1) + File.truncate("test_stderr.#$$.log", 0) + + res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) + assert_match %r{\d+}, res2.first + assert res2.first != res.first + + assert_nothing_raised { Process.kill(:HUP, daemon_pid) } + wait_workers_ready("test_stderr.#$$.log", 1) + File.truncate("test_stderr.#$$.log", 0) + res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) + assert res2.first != res3.first + + assert_nothing_raised { Process.kill(:QUIT, daemon_pid) } + wait_for_death(daemon_pid) + end + end + + def default_listen_setup + File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) } + pid_path = (tmp = Tempfile.new('pid')).path + tmp.close! + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("pid '#{pid_path}'\n") + ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n") + ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n") + pid = xfork { + redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) } + } + _, status = Process.waitpid2(pid) + assert status.success? + wait_master_ready("test_stderr.#$$.log") + wait_workers_ready("test_stderr.#$$.log", 1) + File.truncate("test_stderr.#$$.log", 0) + res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"]) + assert_match %r{\d+}, res.first + [ res, pid_path ] + end + + # we need to flock() something to prevent these tests from running + def default_listen_lock(&block) + fp = File.open(FLOCK_PATH, "rb") + begin + fp.flock(File::LOCK_EX) + begin + TCPServer.new(Unicorn::Const::DEFAULT_HOST, + Unicorn::Const::DEFAULT_PORT).close + rescue Errno::EADDRINUSE, Errno::EACCES + warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}" + return false + end + + # unused_port should never take this, but we may run an environment + # where tests are being run against older unicorns... + lock_path = "#{Dir::tmpdir}/unicorn_test." \ + "#{Unicorn::Const::DEFAULT_LISTEN}.lock" + begin + lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600) + yield + rescue Errno::EEXIST + lock_path = nil + return false + ensure + File.unlink(lock_path) if lock_path + end + ensure + fp.flock(File::LOCK_UN) + end + end + end if do_test diff --git a/test/rails/app-1.2.3/app/controllers/application.rb b/test/rails/app-1.2.3/app/controllers/application.rb index ae8cac0..e72474f 100644 --- a/test/rails/app-1.2.3/app/controllers/application.rb +++ b/test/rails/app-1.2.3/app/controllers/application.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + class ApplicationController < ActionController::Base # Pick a unique cookie name to distinguish our session data from others' session :session_key => "_unicorn_rails_test.#{rand}" diff --git a/test/rails/app-1.2.3/app/controllers/foo_controller.rb b/test/rails/app-1.2.3/app/controllers/foo_controller.rb index 8d877d1..52b7947 100644 --- a/test/rails/app-1.2.3/app/controllers/foo_controller.rb +++ b/test/rails/app-1.2.3/app/controllers/foo_controller.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'digest/sha1' class FooController < ApplicationController def index diff --git a/test/rails/app-1.2.3/app/helpers/application_helper.rb b/test/rails/app-1.2.3/app/helpers/application_helper.rb index de6be79..d9889b3 100644 --- a/test/rails/app-1.2.3/app/helpers/application_helper.rb +++ b/test/rails/app-1.2.3/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + module ApplicationHelper end diff --git a/test/rails/app-1.2.3/config/boot.rb b/test/rails/app-1.2.3/config/boot.rb index 71c7d7c..84a5c18 100644 --- a/test/rails/app-1.2.3/config/boot.rb +++ b/test/rails/app-1.2.3/config/boot.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + unless defined?(RAILS_ROOT) root_path = File.join(File.dirname(__FILE__), '..') RAILS_ROOT = root_path diff --git a/test/rails/app-1.2.3/config/environment.rb b/test/rails/app-1.2.3/config/environment.rb index 2ef6b4a..e230a66 100644 --- a/test/rails/app-1.2.3/config/environment.rb +++ b/test/rails/app-1.2.3/config/environment.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + unless defined? RAILS_GEM_VERSION RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] # || '1.2.3' end diff --git a/test/rails/app-1.2.3/config/environments/development.rb b/test/rails/app-1.2.3/config/environments/development.rb index 032fb46..9d78f5e 100644 --- a/test/rails/app-1.2.3/config/environments/development.rb +++ b/test/rails/app-1.2.3/config/environments/development.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = false config.whiny_nils = true config.breakpoint_server = true diff --git a/test/rails/app-1.2.3/config/environments/production.rb b/test/rails/app-1.2.3/config/environments/production.rb index c4059e3..1e049b2 100644 --- a/test/rails/app-1.2.3/config/environments/production.rb +++ b/test/rails/app-1.2.3/config/environments/production.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = true config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true diff --git a/test/rails/app-1.2.3/config/routes.rb b/test/rails/app-1.2.3/config/routes.rb index 774028f..70816dc 100644 --- a/test/rails/app-1.2.3/config/routes.rb +++ b/test/rails/app-1.2.3/config/routes.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id.:format' map.connect ':controller/:action/:id' diff --git a/test/rails/app-2.0.2/app/controllers/application.rb b/test/rails/app-2.0.2/app/controllers/application.rb index 09705d1..e7bb740 100644 --- a/test/rails/app-2.0.2/app/controllers/application.rb +++ b/test/rails/app-2.0.2/app/controllers/application.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + class ApplicationController < ActionController::Base end diff --git a/test/rails/app-2.0.2/app/controllers/foo_controller.rb b/test/rails/app-2.0.2/app/controllers/foo_controller.rb index 8d877d1..52b7947 100644 --- a/test/rails/app-2.0.2/app/controllers/foo_controller.rb +++ b/test/rails/app-2.0.2/app/controllers/foo_controller.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'digest/sha1' class FooController < ApplicationController def index diff --git a/test/rails/app-2.0.2/app/helpers/application_helper.rb b/test/rails/app-2.0.2/app/helpers/application_helper.rb index de6be79..d9889b3 100644 --- a/test/rails/app-2.0.2/app/helpers/application_helper.rb +++ b/test/rails/app-2.0.2/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + module ApplicationHelper end diff --git a/test/rails/app-2.0.2/config/boot.rb b/test/rails/app-2.0.2/config/boot.rb index 71c7d7c..84a5c18 100644 --- a/test/rails/app-2.0.2/config/boot.rb +++ b/test/rails/app-2.0.2/config/boot.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + unless defined?(RAILS_ROOT) root_path = File.join(File.dirname(__FILE__), '..') RAILS_ROOT = root_path diff --git a/test/rails/app-2.0.2/config/environment.rb b/test/rails/app-2.0.2/config/environment.rb index 7c720f6..9961f08 100644 --- a/test/rails/app-2.0.2/config/environment.rb +++ b/test/rails/app-2.0.2/config/environment.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + unless defined? RAILS_GEM_VERSION RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] end diff --git a/test/rails/app-2.0.2/config/environments/development.rb b/test/rails/app-2.0.2/config/environments/development.rb index 6a613c1..5e0f1ca 100644 --- a/test/rails/app-2.0.2/config/environments/development.rb +++ b/test/rails/app-2.0.2/config/environments/development.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = false config.whiny_nils = true config.action_controller.consider_all_requests_local = true diff --git a/test/rails/app-2.0.2/config/environments/production.rb b/test/rails/app-2.0.2/config/environments/production.rb index c4059e3..1e049b2 100644 --- a/test/rails/app-2.0.2/config/environments/production.rb +++ b/test/rails/app-2.0.2/config/environments/production.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = true config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true diff --git a/test/rails/app-2.0.2/config/routes.rb b/test/rails/app-2.0.2/config/routes.rb index 774028f..70816dc 100644 --- a/test/rails/app-2.0.2/config/routes.rb +++ b/test/rails/app-2.0.2/config/routes.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id.:format' map.connect ':controller/:action/:id' diff --git a/test/rails/app-2.1.2/app/controllers/application.rb b/test/rails/app-2.1.2/app/controllers/application.rb index 09705d1..e7bb740 100644 --- a/test/rails/app-2.1.2/app/controllers/application.rb +++ b/test/rails/app-2.1.2/app/controllers/application.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + class ApplicationController < ActionController::Base end diff --git a/test/rails/app-2.1.2/app/controllers/foo_controller.rb b/test/rails/app-2.1.2/app/controllers/foo_controller.rb index 8d877d1..52b7947 100644 --- a/test/rails/app-2.1.2/app/controllers/foo_controller.rb +++ b/test/rails/app-2.1.2/app/controllers/foo_controller.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'digest/sha1' class FooController < ApplicationController def index diff --git a/test/rails/app-2.1.2/app/helpers/application_helper.rb b/test/rails/app-2.1.2/app/helpers/application_helper.rb index de6be79..d9889b3 100644 --- a/test/rails/app-2.1.2/app/helpers/application_helper.rb +++ b/test/rails/app-2.1.2/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + module ApplicationHelper end diff --git a/test/rails/app-2.1.2/config/boot.rb b/test/rails/app-2.1.2/config/boot.rb index 0a51688..e357f0a 100644 --- a/test/rails/app-2.1.2/config/boot.rb +++ b/test/rails/app-2.1.2/config/boot.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Don't change this file! # Configure your app in config/environment.rb and config/environments/*.rb diff --git a/test/rails/app-2.1.2/config/environment.rb b/test/rails/app-2.1.2/config/environment.rb index 7c720f6..9961f08 100644 --- a/test/rails/app-2.1.2/config/environment.rb +++ b/test/rails/app-2.1.2/config/environment.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + unless defined? RAILS_GEM_VERSION RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] end diff --git a/test/rails/app-2.1.2/config/environments/development.rb b/test/rails/app-2.1.2/config/environments/development.rb index 7f49032..37f523f 100644 --- a/test/rails/app-2.1.2/config/environments/development.rb +++ b/test/rails/app-2.1.2/config/environments/development.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = false config.whiny_nils = true config.action_controller.consider_all_requests_local = true diff --git a/test/rails/app-2.1.2/config/environments/production.rb b/test/rails/app-2.1.2/config/environments/production.rb index c4059e3..1e049b2 100644 --- a/test/rails/app-2.1.2/config/environments/production.rb +++ b/test/rails/app-2.1.2/config/environments/production.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = true config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true diff --git a/test/rails/app-2.1.2/config/routes.rb b/test/rails/app-2.1.2/config/routes.rb index 774028f..70816dc 100644 --- a/test/rails/app-2.1.2/config/routes.rb +++ b/test/rails/app-2.1.2/config/routes.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id.:format' map.connect ':controller/:action/:id' diff --git a/test/rails/app-2.2.2/app/controllers/application.rb b/test/rails/app-2.2.2/app/controllers/application.rb index 09705d1..e7bb740 100644 --- a/test/rails/app-2.2.2/app/controllers/application.rb +++ b/test/rails/app-2.2.2/app/controllers/application.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + class ApplicationController < ActionController::Base end diff --git a/test/rails/app-2.2.2/app/controllers/foo_controller.rb b/test/rails/app-2.2.2/app/controllers/foo_controller.rb index 8d877d1..52b7947 100644 --- a/test/rails/app-2.2.2/app/controllers/foo_controller.rb +++ b/test/rails/app-2.2.2/app/controllers/foo_controller.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'digest/sha1' class FooController < ApplicationController def index diff --git a/test/rails/app-2.2.2/app/helpers/application_helper.rb b/test/rails/app-2.2.2/app/helpers/application_helper.rb index de6be79..d9889b3 100644 --- a/test/rails/app-2.2.2/app/helpers/application_helper.rb +++ b/test/rails/app-2.2.2/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + module ApplicationHelper end diff --git a/test/rails/app-2.2.2/config/boot.rb b/test/rails/app-2.2.2/config/boot.rb index 0a51688..e357f0a 100644 --- a/test/rails/app-2.2.2/config/boot.rb +++ b/test/rails/app-2.2.2/config/boot.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Don't change this file! # Configure your app in config/environment.rb and config/environments/*.rb diff --git a/test/rails/app-2.2.2/config/environment.rb b/test/rails/app-2.2.2/config/environment.rb index 7c720f6..9961f08 100644 --- a/test/rails/app-2.2.2/config/environment.rb +++ b/test/rails/app-2.2.2/config/environment.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + unless defined? RAILS_GEM_VERSION RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] end diff --git a/test/rails/app-2.2.2/config/environments/development.rb b/test/rails/app-2.2.2/config/environments/development.rb index 7f49032..37f523f 100644 --- a/test/rails/app-2.2.2/config/environments/development.rb +++ b/test/rails/app-2.2.2/config/environments/development.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = false config.whiny_nils = true config.action_controller.consider_all_requests_local = true diff --git a/test/rails/app-2.2.2/config/environments/production.rb b/test/rails/app-2.2.2/config/environments/production.rb index c4059e3..1e049b2 100644 --- a/test/rails/app-2.2.2/config/environments/production.rb +++ b/test/rails/app-2.2.2/config/environments/production.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = true config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true diff --git a/test/rails/app-2.2.2/config/routes.rb b/test/rails/app-2.2.2/config/routes.rb index 774028f..70816dc 100644 --- a/test/rails/app-2.2.2/config/routes.rb +++ b/test/rails/app-2.2.2/config/routes.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id.:format' map.connect ':controller/:action/:id' diff --git a/test/rails/app-2.3.2.1/.gitignore b/test/rails/app-2.3.5/.gitignore index f451f91..f451f91 100644 --- a/test/rails/app-2.3.2.1/.gitignore +++ b/test/rails/app-2.3.5/.gitignore diff --git a/test/rails/app-2.3.2.1/Rakefile b/test/rails/app-2.3.5/Rakefile index fbebfca..fbebfca 100644 --- a/test/rails/app-2.3.2.1/Rakefile +++ b/test/rails/app-2.3.5/Rakefile diff --git a/test/rails/app-2.3.2.1/app/controllers/application_controller.rb b/test/rails/app-2.3.5/app/controllers/application_controller.rb index 6160f52..07c333e 100644 --- a/test/rails/app-2.3.2.1/app/controllers/application_controller.rb +++ b/test/rails/app-2.3.5/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + class ApplicationController < ActionController::Base helper :all end diff --git a/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb b/test/rails/app-2.3.5/app/controllers/foo_controller.rb index 261669c..54ca1ed 100644 --- a/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb +++ b/test/rails/app-2.3.5/app/controllers/foo_controller.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'digest/sha1' class FooController < ApplicationController def index diff --git a/test/rails/app-2.3.2.1/app/helpers/application_helper.rb b/test/rails/app-2.3.5/app/helpers/application_helper.rb index de6be79..d9889b3 100644 --- a/test/rails/app-2.3.2.1/app/helpers/application_helper.rb +++ b/test/rails/app-2.3.5/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# -*- encoding: binary -*- + module ApplicationHelper end diff --git a/test/rails/app-2.3.2.1/config/boot.rb b/test/rails/app-2.3.5/config/boot.rb index d22e6b0..b6c80d5 100644 --- a/test/rails/app-2.3.2.1/config/boot.rb +++ b/test/rails/app-2.3.5/config/boot.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) module Rails diff --git a/test/rails/app-2.3.2.1/config/database.yml b/test/rails/app-2.3.5/config/database.yml index 9f77843..9f77843 100644 --- a/test/rails/app-2.3.2.1/config/database.yml +++ b/test/rails/app-2.3.5/config/database.yml diff --git a/test/rails/app-2.3.2.1/config/environment.rb b/test/rails/app-2.3.5/config/environment.rb index 17abdb7..6eb092c 100644 --- a/test/rails/app-2.3.2.1/config/environment.rb +++ b/test/rails/app-2.3.5/config/environment.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + unless defined? RAILS_GEM_VERSION RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] end diff --git a/test/rails/app-2.3.2.1/config/environments/development.rb b/test/rails/app-2.3.5/config/environments/development.rb index 55376c5..3d381d2 100644 --- a/test/rails/app-2.3.2.1/config/environments/development.rb +++ b/test/rails/app-2.3.5/config/environments/development.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = false config.whiny_nils = true config.action_controller.consider_all_requests_local = true diff --git a/test/rails/app-2.3.2.1/config/environments/production.rb b/test/rails/app-2.3.5/config/environments/production.rb index 474257d..08710a4 100644 --- a/test/rails/app-2.3.2.1/config/environments/production.rb +++ b/test/rails/app-2.3.5/config/environments/production.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + config.cache_classes = true config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true diff --git a/test/rails/app-2.3.2.1/config/routes.rb b/test/rails/app-2.3.5/config/routes.rb index 4248853..ac7877c 100644 --- a/test/rails/app-2.3.2.1/config/routes.rb +++ b/test/rails/app-2.3.5/config/routes.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' diff --git a/test/rails/app-2.3.2.1/db/.gitignore b/test/rails/app-2.3.5/db/.gitignore index e69de29..e69de29 100644 --- a/test/rails/app-2.3.2.1/db/.gitignore +++ b/test/rails/app-2.3.5/db/.gitignore diff --git a/test/rails/app-2.3.2.1/log/.gitignore b/test/rails/app-2.3.5/log/.gitignore index 397b4a7..397b4a7 100644 --- a/test/rails/app-2.3.2.1/log/.gitignore +++ b/test/rails/app-2.3.5/log/.gitignore diff --git a/test/rails/app-2.3.2.1/public/404.html b/test/rails/app-2.3.5/public/404.html index 44d986c..44d986c 100644 --- a/test/rails/app-2.3.2.1/public/404.html +++ b/test/rails/app-2.3.5/public/404.html diff --git a/test/rails/app-2.3.2.1/public/500.html b/test/rails/app-2.3.5/public/500.html index e534a49..e534a49 100644 --- a/test/rails/app-2.3.2.1/public/500.html +++ b/test/rails/app-2.3.5/public/500.html diff --git a/test/rails/app-2.3.5/public/x.txt b/test/rails/app-2.3.5/public/x.txt new file mode 100644 index 0000000..e427984 --- /dev/null +++ b/test/rails/app-2.3.5/public/x.txt @@ -0,0 +1 @@ +HELLO diff --git a/test/rails/test_rails.rb b/test/rails/test_rails.rb index c7add20..9502dcb 100644 --- a/test/rails/test_rails.rb +++ b/test/rails/test_rails.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong require 'test/test_helper' @@ -142,18 +144,24 @@ logger Logger.new('#{COMMON_TMP.path}') end end end - resp = `curl -isSfN -Ffile=@#{tmp.path} http://#@addr:#@port/foo/xpost` - assert $?.success? - resp = resp.split(/\r?\n/) - grepped = resp.grep(/^sha1: (.{40})/) - assert_equal 1, grepped.size - assert_equal(sha1.hexdigest, /^sha1: (.{40})/.match(grepped.first)[1]) - - grepped = resp.grep(/^Content-Type:\s+(.+)/i) - assert_equal 1, grepped.size - assert_match %r{^text/plain}, grepped.first.split(/\s*:\s*/)[1] - assert_equal 1, resp.grep(/^Status:/i).size + # fixed in Rack commit 44ed4640f077504a49b7f1cabf8d6ad7a13f6441, + # no released version of Rails or Rack has this fix + if RB_V[0] >= 1 && RB_V[1] >= 9 + warn "multipart broken with Rack 1.0.0 and Rails 2.3.2.1 under 1.9" + else + resp = `curl -isSfN -Ffile=@#{tmp.path} http://#@addr:#@port/foo/xpost` + assert $?.success? + resp = resp.split(/\r?\n/) + grepped = resp.grep(/^sha1: (.{40})/) + assert_equal 1, grepped.size + assert_equal(sha1.hexdigest, /^sha1: (.{40})/.match(grepped.first)[1]) + + grepped = resp.grep(/^Content-Type:\s+(.+)/i) + assert_equal 1, grepped.size + assert_match %r{^text/plain}, grepped.first.split(/\s*:\s*/)[1] + assert_equal 1, resp.grep(/^Status:/i).size + end # make sure we can get 403 responses, too uri = URI.parse("http://#@addr:#@port/foo/xpost") @@ -223,6 +231,31 @@ logger Logger.new('#{COMMON_TMP.path}') assert_equal '404 Not Found', res['Status'] end + def test_alt_url_root_config_env + # cbf to actually work on this since I never use this feature (ewong) + return unless ROR_V[0] >= 2 && ROR_V[1] >= 3 + tmp = Tempfile.new(nil) + tmp.syswrite("ENV['RAILS_RELATIVE_URL_ROOT'] = '/poo'\n") + redirect_test_io do + @pid = fork { exec 'unicorn_rails', "-l#@addr:#@port", "-c", tmp.path } + end + wait_master_ready("test_stderr.#$$.log") + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/poo/foo")) + assert_equal "200", res.code + assert_equal '200 OK', res['Status'] + assert_equal "FOO\n", res.body + assert_match %r{^text/html\b}, res['Content-Type'] + assert_equal "4", res['Content-Length'] + + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo")) + assert_equal "404", res.code + assert_equal '404 Not Found', res['Status'] + + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/poo/x.txt")) + assert_equal "200", res.code + assert_equal "HELLO\n", res.body + end + def teardown return if @start_pid != $$ diff --git a/test/test_helper.rb b/test/test_helper.rb index 787adbf..3bdbeb1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -27,7 +29,7 @@ require 'tempfile' require 'fileutils' require 'logger' require 'unicorn' -require 'unicorn/http11' +require 'unicorn_http' if ENV['DEBUG'] require 'ruby-debug' @@ -102,6 +104,10 @@ def unused_port(addr = '127.0.0.1') begin begin port = base + rand(32768 - base) + while port == Unicorn::Const::DEFAULT_PORT + port = base + rand(32768 - base) + end + sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) sock.bind(Socket.pack_sockaddr_in(port, addr)) sock.listen(5) @@ -139,7 +145,7 @@ def retry_hit(uris = []) tries = DEFAULT_TRIES begin hit(uris) - rescue Errno::ECONNREFUSED => err + rescue Errno::EINVAL, Errno::ECONNREFUSED => err if (tries -= 1) > 0 sleep DEFAULT_RES retry @@ -262,3 +268,29 @@ def wait_for_death(pid) end 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 diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb index 98f2db6..ac1efa8 100644 --- a/test/unit/test_configurator.rb +++ b/test/unit/test_configurator.rb @@ -1,7 +1,11 @@ +# -*- encoding: binary -*- + require 'test/unit' require 'tempfile' -require 'unicorn/configurator' +require 'unicorn' +TestStruct = Struct.new( + *(Unicorn::Configurator::DEFAULTS.keys + %w(listener_opts listeners))) class TestConfigurator < Test::Unit::TestCase def test_config_init @@ -28,8 +32,10 @@ class TestConfigurator < Test::Unit::TestCase assert_equal "0.0.0.0:2007", meth.call('*:2007') assert_equal "0.0.0.0:2007", meth.call('2007') assert_equal "0.0.0.0:2007", meth.call(2007) - assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007') - assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007') + + # the next two aren't portable, consider them unsupported for now + # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007') + # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007') end def test_config_invalid @@ -51,22 +57,23 @@ class TestConfigurator < Test::Unit::TestCase def test_config_defaults cfg = Unicorn::Configurator.new(:use_defaults => true) - assert_nothing_raised { cfg.commit!(self) } + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct) } Unicorn::Configurator::DEFAULTS.each do |key,value| - assert_equal value, instance_variable_get("@#{key.to_s}") + assert_equal value, test_struct.__send__(key) end end def test_config_defaults_skip cfg = Unicorn::Configurator.new(:use_defaults => true) skip = [ :logger ] - assert_nothing_raised { cfg.commit!(self, :skip => skip) } - @logger = nil + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct, :skip => skip) } Unicorn::Configurator::DEFAULTS.each do |key,value| next if skip.include?(key) - assert_equal value, instance_variable_get("@#{key.to_s}") + assert_equal value, test_struct.__send__(key) end - assert_nil @logger + assert_nil test_struct.logger end def test_listen_options @@ -78,8 +85,9 @@ class TestConfigurator < Test::Unit::TestCase assert_nothing_raised do cfg = Unicorn::Configurator.new(:config_file => tmp.path) end - assert_nothing_raised { cfg.commit!(self) } - assert(listener_opts = instance_variable_get("@listener_opts")) + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct) } + assert(listener_opts = test_struct.listener_opts) assert_equal expect, listener_opts[listener] end @@ -93,10 +101,41 @@ class TestConfigurator < Test::Unit::TestCase end end + def test_listen_option_bad_delay + tmp = Tempfile.new('unicorn_config') + expect = { :delay => "five" } + listener = "127.0.0.1:12345" + tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") + assert_raises(ArgumentError) do + Unicorn::Configurator.new(:config_file => tmp.path) + end + end + + def test_listen_option_float_delay + tmp = Tempfile.new('unicorn_config') + expect = { :delay => 0.5 } + listener = "127.0.0.1:12345" + tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") + assert_nothing_raised do + Unicorn::Configurator.new(:config_file => tmp.path) + end + end + + def test_listen_option_int_delay + tmp = Tempfile.new('unicorn_config') + expect = { :delay => 5 } + listener = "127.0.0.1:12345" + tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") + assert_nothing_raised do + Unicorn::Configurator.new(:config_file => tmp.path) + end + end + def test_after_fork_proc + test_struct = TestStruct.new [ proc { |a,b| }, Proc.new { |a,b| }, lambda { |a,b| } ].each do |my_proc| - Unicorn::Configurator.new(:after_fork => my_proc).commit!(self) - assert_equal my_proc, @after_fork + Unicorn::Configurator.new(:after_fork => my_proc).commit!(test_struct) + assert_equal my_proc, test_struct.after_fork end end diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb index a158ebb..0443b46 100644 --- a/test/unit/test_http_parser.rb +++ b/test/unit/test_http_parser.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -9,12 +11,13 @@ require 'test/test_helper' include Unicorn class HttpParserTest < Test::Unit::TestCase - + def test_parse_simple parser = HttpParser.new req = {} http = "GET / HTTP/1.1\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) + assert_equal '', http assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/', req['REQUEST_PATH'] @@ -24,15 +27,18 @@ class HttpParserTest < Test::Unit::TestCase assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert parser.keepalive? parser.reset req.clear - assert ! parser.execute(req, "G") + http = "G" + assert_nil parser.headers(req, http) + assert_equal "G", http assert req.empty? # try parsing again to ensure we were reset correctly http = "GET /hello-world HTTP/1.1\r\n\r\n" - assert parser.execute(req, http) + assert parser.headers(req, http) assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/hello-world', req['REQUEST_PATH'] @@ -41,55 +47,184 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert_equal '', http + assert parser.keepalive? + end + + def test_connection_close_no_ka + parser = HttpParser.new + req = {} + tmp = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert_equal "GET", req['REQUEST_METHOD'] + assert ! parser.keepalive? + end + + def test_connection_keep_alive_ka + parser = HttpParser.new + req = {} + tmp = "HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert parser.keepalive? + end + + def test_connection_keep_alive_ka_bad_method + parser = HttpParser.new + req = {} + tmp = "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert ! parser.keepalive? + end + + def test_connection_keep_alive_ka_bad_version + parser = HttpParser.new + req = {} + tmp = "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert parser.keepalive? end def test_parse_server_host_default_port parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_server_host_alt_port parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '999', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_server_host_empty_port parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_server_host_xfp_https parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n" \ - "X-Forwarded-Proto: https\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n" \ + "X-Forwarded-Proto: https\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_strange_headers parser = HttpParser.new req = {} should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" - assert parser.execute(req, should_be_good) + assert_equal req, parser.headers(req, should_be_good) + assert_equal '', should_be_good + assert parser.keepalive? + end + + # legacy test case from Mongrel that we never supported before... + # I still consider Pound irrelevant, unfortunately stupid clients that + # send extremely big headers do exist and they've managed to find Unicorn... + def test_nasty_pound_header + parser = HttpParser.new + nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n" + req = {} + buf = nasty_pound_header.dup + + assert nasty_pound_header =~ /(-----BEGIN .*--END CERTIFICATE-----)/m + expect = $1.dup + expect.gsub!(/\r\n\t/, ' ') + assert_equal req, parser.headers(req, buf) + assert_equal '', buf + assert_equal expect, req['HTTP_X_SSL_BULLSHIT'] + end + + def test_continuation_eats_leading_spaces + parser = HttpParser.new + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: \r\n" \ + "\t\r\n" \ + " \r\n" \ + " ASDF\r\n\r\n" + req = {} + assert_equal req, parser.headers(req, header) + assert_equal '', header + assert_equal 'ASDF', req['HTTP_X_ASDF'] + end + + def test_continuation_eats_scattered_leading_spaces + parser = HttpParser.new + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: hi\r\n" \ + " y\r\n" \ + "\t\r\n" \ + " x\r\n" \ + " ASDF\r\n\r\n" + req = {} + assert_equal req, parser.headers(req, header) + assert_equal '', header + assert_equal 'hi y x ASDF', req['HTTP_X_ASDF'] + end + + def test_continuation_with_absolute_uri_and_ignored_host_header + parser = HttpParser.new + header = "GET http://example.com/ HTTP/1.1\r\n" \ + "Host: \r\n" \ + " YHBT.net\r\n" \ + "\r\n" + req = {} + assert_equal req, parser.headers(req, header) + assert_equal 'example.com', req['HTTP_HOST'] + end - # ref: http://thread.gmane.org/gmane.comp.lang.ruby.mongrel.devel/37/focus=45 - # (note we got 'pen' mixed up with 'pound' in that thread, - # but the gist of it is still relevant: these nasty headers are irrelevant - # - # nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n" - # parser = HttpParser.new - # req = {} - # assert parser.execute(req, nasty_pound_header, 0) + # this may seem to be testing more of an implementation detail, but + # it also helps ensure we're safe in the presence of multiple parsers + # in case we ever go multithreaded/evented... + def test_resumable_continuations + nr = 1000 + req = {} + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: \r\n" \ + " hello\r\n" + tmp = [] + nr.times { |i| + parser = HttpParser.new + assert parser.headers(req, "#{header} #{i}\r\n").nil? + asdf = req['HTTP_X_ASDF'] + assert_equal "hello #{i}", asdf + tmp << [ parser, asdf ] + req.clear + } + tmp.each_with_index { |(parser, asdf), i| + assert_equal req, parser.headers(req, "#{header} #{i}\r\n .\r\n\r\n") + assert_equal "hello #{i} .", asdf + } + end + + def test_invalid_continuation + parser = HttpParser.new + header = "GET / HTTP/1.1\r\n" \ + " y\r\n" \ + "Host: hello\r\n" \ + "\r\n" + req = {} + assert_raises(HttpParserError) { parser.headers(req, header) } end def test_parse_ie6_urls @@ -103,7 +238,10 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n) - assert parser.execute(req, sorta_safe) + assert_equal req, parser.headers(req, sorta_safe) + assert_equal path, req['REQUEST_URI'] + assert_equal '', sorta_safe + assert parser.keepalive? end end @@ -112,28 +250,34 @@ class HttpParserTest < Test::Unit::TestCase req = {} bad_http = "GET / SsUTF/1.1" - assert_raises(HttpParserError) { parser.execute(req, bad_http) } + assert_raises(HttpParserError) { parser.headers(req, bad_http) } + + # make sure we can recover parser.reset - assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n")) + req.clear + assert_equal req, parser.headers(req, "GET / HTTP/1.0\r\n\r\n") + assert ! parser.keepalive? end def test_piecemeal parser = HttpParser.new req = {} http = "GET" - assert ! parser.execute(req, http) - assert_raises(HttpParserError) { parser.execute(req, http) } - assert ! parser.execute(req, http << " / HTTP/1.0") + assert_nil parser.headers(req, http) + assert_nil parser.headers(req, http) + assert_nil parser.headers(req, http << " / HTTP/1.0") assert_equal '/', req['REQUEST_PATH'] assert_equal '/', req['REQUEST_URI'] assert_equal 'GET', req['REQUEST_METHOD'] - assert ! parser.execute(req, http << "\r\n") + assert_nil parser.headers(req, http << "\r\n") assert_equal 'HTTP/1.0', req['HTTP_VERSION'] - assert ! parser.execute(req, http << "\r") - assert parser.execute(req, http << "\n") - assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] + assert_nil parser.headers(req, http << "\r") + assert_equal req, parser.headers(req, http << "\n") + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert_equal "", http + assert ! parser.keepalive? end # not common, but underscores do appear in practice @@ -141,7 +285,7 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -150,13 +294,54 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'under_score.example.com', req['HTTP_HOST'] assert_equal 'under_score.example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? + end + + # some dumb clients add users because they're stupid + def test_absolute_uri_w_user + parser = HttpParser.new + req = {} + http = "GET http://user%20space@example.com/foo?q=bar HTTP/1.0\r\n\r\n" + assert_equal req, parser.headers(req, http) + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + + assert_equal 'example.com', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? + end + + # since Mongrel supported anything URI.parse supported, we're stuck + # supporting everything URI.parse supports + def test_absolute_uri_uri_parse + "#{URI::REGEXP::PATTERN::UNRESERVED};:&=+$,".split(//).each do |char| + parser = HttpParser.new + req = {} + http = "GET http://#{char}@example.com/ HTTP/1.0\r\n\r\n" + assert_equal req, parser.headers(req, http) + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/', req['REQUEST_URI'] + assert_equal '/', req['REQUEST_PATH'] + assert_equal '', req['QUERY_STRING'] + + assert_equal 'example.com', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? + end end def test_absolute_uri parser = HttpParser.new req = {} http = "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -165,6 +350,8 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? end # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however... @@ -173,7 +360,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \ "X-Forwarded-Proto: http\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -182,6 +369,8 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] + assert_equal "", http + assert parser.keepalive? end # Host: header should be ignored for absolute URIs @@ -190,7 +379,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} http = "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \ "Host: bad.example.com\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -199,6 +388,8 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com:8080', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '8080', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_absolute_uri_with_empty_port @@ -206,7 +397,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} http = "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -215,32 +406,55 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com:', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] + assert_equal "", http + assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_put_body_oneshot parser = HttpParser.new req = {} http = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal '/', req['REQUEST_PATH'] assert_equal '/', req['REQUEST_URI'] assert_equal 'PUT', req['REQUEST_METHOD'] assert_equal 'HTTP/1.0', req['HTTP_VERSION'] - assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] - assert_equal "abcde", req[:http_body] + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] + assert_equal "abcde", http + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_put_body_later parser = HttpParser.new req = {} http = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal '/l', req['REQUEST_PATH'] assert_equal '/l', req['REQUEST_URI'] assert_equal 'PUT', req['REQUEST_METHOD'] assert_equal 'HTTP/1.0', req['HTTP_VERSION'] - assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] - assert_equal "", req[:http_body] + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] + assert_equal "", http + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final + end + + def test_unknown_methods + %w(GETT HEADR XGET XHEAD).each { |m| + parser = HttpParser.new + req = {} + s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" + ok = false + assert_nothing_raised do + ok = parser.headers(req, s) + end + assert ok + assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] + assert_equal 'posts-17408', req['FRAGMENT'] + assert_equal 'page=1', req['QUERY_STRING'] + assert_equal "", s + assert_equal m, req['REQUEST_METHOD'] + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final + } end def test_fragment_in_uri @@ -249,12 +463,14 @@ class HttpParserTest < Test::Unit::TestCase get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" ok = false assert_nothing_raised do - ok = parser.execute(req, get) + ok = parser.headers(req, get) end assert ok assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] assert_equal 'posts-17408', req['FRAGMENT'] assert_equal 'page=1', req['QUERY_STRING'] + assert_equal '', get + assert parser.keepalive? end # lame random garbage maker @@ -279,7 +495,7 @@ class HttpParserTest < Test::Unit::TestCase 10.times do |c| get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n" assert_raises Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end end @@ -288,7 +504,7 @@ class HttpParserTest < Test::Unit::TestCase 10.times do |c| get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" assert_raises Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end end @@ -297,7 +513,7 @@ class HttpParserTest < Test::Unit::TestCase get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n" get << "X-Test: test\r\n" * (80 * 1024) assert_raises Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end @@ -305,7 +521,7 @@ class HttpParserTest < Test::Unit::TestCase 10.times do |c| get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" assert_raises Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end end diff --git a/test/unit/test_http_parser_ng.rb b/test/unit/test_http_parser_ng.rb new file mode 100644 index 0000000..bb61e7f --- /dev/null +++ b/test/unit/test_http_parser_ng.rb @@ -0,0 +1,420 @@ +# -*- encoding: binary -*- + +# coding: binary +require 'test/test_helper' +require 'digest/md5' + +include Unicorn + +class HttpParserNgTest < Test::Unit::TestCase + + def setup + @parser = HttpParser.new + end + + def test_identity_byte_headers + req = {} + str = "PUT / HTTP/1.1\r\n" + str << "Content-Length: 123\r\n" + str << "\r" + hdr = "" + str.each_byte { |byte| + assert_nil @parser.headers(req, hdr << byte.chr) + } + hdr << "\n" + assert_equal req.object_id, @parser.headers(req, hdr).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 0, hdr.size + assert ! @parser.keepalive? + assert @parser.headers? + assert 123, @parser.content_length + end + + def test_identity_step_headers + req = {} + str = "PUT / HTTP/1.1\r\n" + assert ! @parser.headers(req, str) + str << "Content-Length: 123\r\n" + assert ! @parser.headers(req, str) + str << "\r\n" + assert_equal req.object_id, @parser.headers(req, str).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 0, str.size + assert ! @parser.keepalive? + assert @parser.headers? + end + + def test_identity_oneshot_header + req = {} + str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n" + assert_equal req.object_id, @parser.headers(req, str).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 0, str.size + assert ! @parser.keepalive? + end + + def test_identity_oneshot_header_with_body + body = ('a' * 123).freeze + req = {} + str = "PUT / HTTP/1.1\r\n" \ + "Content-Length: #{body.length}\r\n" \ + "\r\n#{body}" + assert_equal req.object_id, @parser.headers(req, str).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 123, str.size + assert_equal body, str + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 0, str.size + assert_equal tmp, body + assert_equal "", @parser.filter_body(tmp, str) + assert ! @parser.keepalive? + end + + def test_identity_oneshot_header_with_body_partial + str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na" + assert_equal Hash, @parser.headers({}, str).class + assert_equal 1, str.size + assert_equal 'a', str + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal "", str + assert_equal "a", tmp + str << ' ' * 122 + rv = @parser.filter_body(tmp, str) + assert_equal 122, tmp.size + assert_nil rv + assert_equal "", str + assert_equal str.object_id, @parser.filter_body(tmp, str).object_id + assert ! @parser.keepalive? + end + + def test_identity_oneshot_header_with_body_slop + str = "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG" + assert_equal Hash, @parser.headers({}, str).class + assert_equal 2, str.size + assert_equal 'aG', str + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal "G", str + assert_equal "G", @parser.filter_body(tmp, str) + assert_equal 1, tmp.size + assert_equal "a", tmp + assert ! @parser.keepalive? + end + + def test_chunked + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 0, str.size + tmp = "" + assert_nil @parser.filter_body(tmp, "6") + assert_equal 0, tmp.size + assert_nil @parser.filter_body(tmp, rv = "\r\n") + assert_equal 0, rv.size + assert_equal 0, tmp.size + tmp = "" + assert_nil @parser.filter_body(tmp, "..") + assert_equal "..", tmp + assert_nil @parser.filter_body(tmp, "abcd\r\n0\r\n") + assert_equal "abcd", tmp + rv = "PUT" + assert_equal rv.object_id, @parser.filter_body(tmp, rv).object_id + assert_equal "PUT", rv + assert ! @parser.keepalive? + end + + def test_two_chunks + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 0, str.size + tmp = "" + assert_nil @parser.filter_body(tmp, "6") + assert_equal 0, tmp.size + assert_nil @parser.filter_body(tmp, rv = "\r\n") + assert_equal "", rv + assert_equal 0, tmp.size + tmp = "" + assert_nil @parser.filter_body(tmp, "..") + assert_equal 2, tmp.size + assert_equal "..", tmp + assert_nil @parser.filter_body(tmp, "abcd\r\n1") + assert_equal "abcd", tmp + assert_nil @parser.filter_body(tmp, "\r") + assert_equal "", tmp + assert_nil @parser.filter_body(tmp, "\n") + assert_equal "", tmp + assert_nil @parser.filter_body(tmp, "z") + assert_equal "z", tmp + assert_nil @parser.filter_body(tmp, "\r\n") + assert_nil @parser.filter_body(tmp, "0") + assert_nil @parser.filter_body(tmp, "\r") + rv = @parser.filter_body(tmp, buf = "\nGET") + assert_equal "GET", rv + assert_equal buf.object_id, rv.object_id + assert ! @parser.keepalive? + end + + def test_big_chunk + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \ + "4000\r\nabcd" + req = {} + assert_equal req, @parser.headers(req, str) + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal '', str + str = ' ' * 16300 + assert_nil @parser.filter_body(tmp, str) + assert_equal '', str + str = ' ' * 80 + assert_nil @parser.filter_body(tmp, str) + assert_equal '', str + assert ! @parser.body_eof? + assert_equal "", @parser.filter_body(tmp, "\r\n0\r\n") + assert @parser.body_eof? + assert ! @parser.keepalive? + end + + def test_two_chunks_oneshot + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + rv = @parser.filter_body(tmp, str) + assert_equal rv.object_id, str.object_id + assert ! @parser.keepalive? + end + + def test_chunks_bytewise + chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n" + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n#{chunked}" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal chunked, str + tmp = '' + buf = '' + body = '' + str = str[0..-2] + str.each_byte { |byte| + assert_nil @parser.filter_body(tmp, buf << byte.chr) + body << tmp + } + assert_equal 'abcdefghijklmnop0123456789abcdefg', body + rv = @parser.filter_body(tmp, buf << "\n") + assert_equal rv.object_id, buf.object_id + assert ! @parser.keepalive? + end + + def test_trailers + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Content-MD5\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Content-MD5', req['HTTP_TRAILER'] + assert_nil req['HTTP_CONTENT_MD5'] + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze + rv = @parser.filter_body(tmp, str) + assert_equal rv.object_id, str.object_id + assert_equal '', str + md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze + str << md5_hdr + assert_nil @parser.trailers(req, str) + assert_equal md5_b64, req['HTTP_CONTENT_MD5'] + assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str + assert_nil @parser.trailers(req, str << "\r") + assert_equal req, @parser.trailers(req, str << "\nGET / ") + assert_equal "GET / ", str + assert ! @parser.keepalive? + end + + def test_trailers_slowly + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Content-MD5\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Content-MD5', req['HTTP_TRAILER'] + assert_nil req['HTTP_CONTENT_MD5'] + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze + rv = @parser.filter_body(tmp, str) + assert_equal rv.object_id, str.object_id + assert_equal '', str + assert_nil @parser.trailers(req, str) + md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze + md5_hdr.each_byte { |byte| + str << byte.chr + assert_nil @parser.trailers(req, str) + } + assert_equal md5_b64, req['HTTP_CONTENT_MD5'] + assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str + assert_nil @parser.trailers(req, str << "\r") + assert_equal req, @parser.trailers(req, str << "\n") + end + + def test_max_chunk + str = "PUT / HTTP/1.1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_nil @parser.content_length + assert_nothing_raised { @parser.filter_body('', str) } + assert ! @parser.keepalive? + end + + def test_max_body + n = HttpParser::LENGTH_MAX + str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n" + req = {} + assert_nothing_raised { @parser.headers(req, str) } + assert_equal n, req['CONTENT_LENGTH'].to_i + assert ! @parser.keepalive? + end + + def test_overflow_chunk + n = HttpParser::CHUNK_MAX + 1 + str = "PUT / HTTP/1.1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_nil @parser.content_length + assert_raise(HttpParserError) { @parser.filter_body('', str) } + assert ! @parser.keepalive? + end + + def test_overflow_content_length + n = HttpParser::LENGTH_MAX + 1 + str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n" + assert_raise(HttpParserError) { @parser.headers({}, str) } + assert ! @parser.keepalive? + end + + def test_bad_chunk + str = "PUT / HTTP/1.1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "#zzz\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_nil @parser.content_length + assert_raise(HttpParserError) { @parser.filter_body('', str) } + assert ! @parser.keepalive? + end + + def test_bad_content_length + str = "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n" + assert_raise(HttpParserError) { @parser.headers({}, str) } + assert ! @parser.keepalive? + end + + def test_bad_trailers + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Transfer-Encoding\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Transfer-Encoding', req['HTTP_TRAILER'] + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + assert_equal '', str + str << "Transfer-Encoding: identity\r\n\r\n" + assert_raise(HttpParserError) { @parser.trailers(req, str) } + assert ! @parser.keepalive? + end + + def test_repeat_headers + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Content-MD5\r\n" \ + "Trailer: Content-SHA1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER'] + assert ! @parser.keepalive? + end + + def test_parse_simple_request + parser = HttpParser.new + req = {} + http = "GET /read-rfc1945-if-you-dont-believe-me\r\n" + assert_equal req, parser.headers(req, http) + assert_equal '', http + expect = { + "SERVER_NAME"=>"localhost", + "rack.url_scheme"=>"http", + "REQUEST_PATH"=>"/read-rfc1945-if-you-dont-believe-me", + "PATH_INFO"=>"/read-rfc1945-if-you-dont-believe-me", + "REQUEST_URI"=>"/read-rfc1945-if-you-dont-believe-me", + "SERVER_PORT"=>"80", + "SERVER_PROTOCOL"=>"HTTP/0.9", + "REQUEST_METHOD"=>"GET", + "QUERY_STRING"=>"" + } + assert_equal expect, req + assert ! parser.headers? + end + + def test_path_info_semicolon + qs = "QUERY_STRING" + pi = "PATH_INFO" + req = {} + str = "GET %s HTTP/1.1\r\nHost: example.com\r\n\r\n" + { + "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" }, + "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" }, + "/1;a=b" => { qs => "", pi => "/1;a=b" }, + "/1;a=b?" => { qs => "", pi => "/1;a=b" }, + "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" }, + "*" => { qs => "", pi => "" }, + }.each do |uri,expect| + assert_equal req, @parser.headers(req.clear, str % [ uri ]) + @parser.reset + assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch" + assert_equal expect[qs], req[qs], "#{qs} mismatch" + assert_equal expect[pi], req[pi], "#{pi} mismatch" + next if uri == "*" + uri = URI.parse("http://example.com#{uri}") + assert_equal uri.query.to_s, req[qs], "#{qs} mismatch URI.parse disagrees" + assert_equal uri.path, req[pi], "#{pi} mismatch URI.parse disagrees" + end + end + + def test_path_info_semicolon_absolute + qs = "QUERY_STRING" + pi = "PATH_INFO" + req = {} + str = "GET http://example.com%s HTTP/1.1\r\nHost: www.example.com\r\n\r\n" + { + "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" }, + "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" }, + "/1;a=b" => { qs => "", pi => "/1;a=b" }, + "/1;a=b?" => { qs => "", pi => "/1;a=b" }, + "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" }, + }.each do |uri,expect| + assert_equal req, @parser.headers(req.clear, str % [ uri ]) + @parser.reset + assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch" + assert_equal "example.com", req["HTTP_HOST"], "Host: mismatch" + assert_equal expect[qs], req[qs], "#{qs} mismatch" + assert_equal expect[pi], req[pi], "#{pi} mismatch" + end + end + +end diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb index 0bfff7d..1896300 100644 --- a/test/unit/test_request.rb +++ b/test/unit/test_request.rb @@ -1,14 +1,9 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby. require 'test/test_helper' -begin - require 'rack' - require 'rack/lint' -rescue LoadError - warn "Unable to load rack, skipping test" - exit 0 -end include Unicorn @@ -16,10 +11,11 @@ class RequestTest < Test::Unit::TestCase class MockRequest < StringIO alias_method :readpartial, :sysread + alias_method :read_nonblock, :sysread end def setup - @request = HttpRequest.new(Logger.new($stderr)) + @request = HttpRequest.new @app = lambda do |env| [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ] end @@ -119,6 +115,31 @@ class RequestTest < Test::Unit::TestCase assert_nothing_raised { res = @lint.call(env) } end + def test_no_content_stringio + client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal StringIO, env['rack.input'].class + end + + def test_zero_content_stringio + client = MockRequest.new("PUT / HTTP/1.1\r\n" \ + "Content-Length: 0\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal StringIO, env['rack.input'].class + end + + def test_real_content_not_stringio + client = MockRequest.new("PUT / HTTP/1.1\r\n" \ + "Content-Length: 1\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal Unicorn::TeeInput, env['rack.input'].class + end + def test_rack_lint_put client = MockRequest.new( "PUT / HTTP/1.1\r\n" \ @@ -149,7 +170,11 @@ class RequestTest < Test::Unit::TestCase assert_nothing_raised { env = @request.read(client) } assert ! env.include?(:http_body) assert_equal length, env['rack.input'].size - count.times { assert_equal buf, env['rack.input'].read(bs) } + count.times { + tmp = env['rack.input'].read(bs) + tmp << env['rack.input'].read(bs - tmp.size) if tmp.size != bs + assert_equal buf, tmp + } assert_nil env['rack.input'].read(bs) assert_nothing_raised { env['rack.input'].rewind } assert_nothing_raised { res = @lint.call(env) } diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb index 66c2b54..f9eda8e 100644 --- a/test/unit/test_response.rb +++ b/test/unit/test_response.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -94,4 +96,15 @@ class ResponseTest < Test::Unit::TestCase assert_match(expect_body, out.string.split(/\r\n/).last) end + def test_unknown_status_pass_through + out = StringIO.new + HttpResponse.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]) + status = headers.grep(/\AStatus:/i).first + assert status + assert_equal "Status: 666 I AM THE BEAST", status + end + end diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb index 742b240..00705d0 100644 --- a/test/unit/test_server.rb +++ b/test/unit/test_server.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -10,9 +12,13 @@ include Unicorn class TestHandler - def call(env) - # response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n") + def call(env) + while env['rack.input'].read(4096) + end [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 @@ -31,6 +37,8 @@ class WebServerTest < Test::Unit::TestCase def teardown redirect_test_io do + wait_workers_ready("test_stderr.#$$.log", 1) + File.truncate("test_stderr.#$$.log", 0) @server.stop(true) end end @@ -51,8 +59,10 @@ class WebServerTest < Test::Unit::TestCase end results = hit(["http://localhost:#@port/"]) worker_pid = results[0].to_i + assert worker_pid != 0 tmp.sysseek(0) loader_pid = tmp.sysread(4096).to_i + assert loader_pid != 0 assert_equal worker_pid, loader_pid teardown @@ -63,6 +73,7 @@ class WebServerTest < Test::Unit::TestCase end results = hit(["http://localhost:#@port/"]) worker_pid = results[0].to_i + assert worker_pid != 0 tmp.sysseek(0) loader_pid = tmp.sysread(4096).to_i assert_equal $$, loader_pid @@ -94,6 +105,92 @@ class WebServerTest < Test::Unit::TestCase assert_equal 'hello!\n', results[0], "Handler didn't really run" end + def test_client_shutdown_writes + sock = nil + buf = nil + bs = 15609315 * rand + assert_nothing_raised do + 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 + end + 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_nothing_raised { sock.close } + end + + def test_client_shutdown_write_truncates + sock = nil + buf = nil + bs = 15609315 * rand + assert_nothing_raised do + 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 + end + 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_nothing_raised { sock.close } + end + + def test_client_malformed_body + sock = nil + buf = nil + bs = 15653984 + assert_nothing_raised do + 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) + end + begin + File.open("/dev/urandom", "rb") { |fp| sock.syswrite(fp.sysread(16384)) } + rescue + end + assert_nothing_raised { sock.close } + 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::HttpParserError: .* true$/) + assert_equal 1, lines.size + end def do_test(string, chunk, close_after=nil, shutdown_delay=0) # Do not use instance variables here, because it needs to be thread safe @@ -131,6 +228,16 @@ class WebServerTest < Test::Unit::TestCase end end + def test_logger_set + assert_equal @server.logger, Unicorn::HttpRequest::DEFAULTS["rack.logger"] + end + + def test_logger_changed + tmp = Logger.new($stdout) + @server.logger = tmp + assert_equal tmp, Unicorn::HttpRequest::DEFAULTS["rack.logger"] + end + def test_bad_client_400 sock = nil assert_nothing_raised do @@ -141,6 +248,16 @@ class WebServerTest < Test::Unit::TestCase assert_nothing_raised { sock.close } end + def test_http_0_9 + sock = nil + assert_nothing_raised do + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET /hello\r\n") + end + assert_match 'hello!\n', sock.sysread(4096) + assert_nothing_raised { sock.close } + end + def test_header_is_too_long redirect_test_io do long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n" @@ -152,9 +269,18 @@ class WebServerTest < Test::Unit::TestCase def test_file_streamed_request body = "a" * (Unicorn::Const::MAX_BODY * 2) - long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body + long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400) end + def test_file_streamed_request_bad_body + body = "a" * (Unicorn::Const::MAX_BODY * 2) + long = "GET /test HTTP/1.1\r\nContent-ength: #{body.length}\r\n\r\n" + body + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) { + do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400) + } + end + end diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb index ef66ed6..eb2af0b 100644 --- a/test/unit/test_signals.rb +++ b/test/unit/test_signals.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -24,14 +26,15 @@ class SignalsTest < Test::Unit::TestCase @bs = 1 * 1024 * 1024 @count = 100 @port = unused_port - tmp = @tmp = Tempfile.new('unicorn.sock') + @sock = Tempfile.new('unicorn.sock') + @tmp = Tempfile.new('unicorn.write') + @tmp.sync = true + File.unlink(@sock.path) File.unlink(@tmp.path) - n = 0 - tmp.chmod(0) @server_opts = { - :listeners => [ "127.0.0.1:#@port", @tmp.path ], + :listeners => [ "127.0.0.1:#@port", @sock.path ], :after_fork => lambda { |server,worker| - trap(:HUP) { tmp.chmod(n += 1) } + trap(:HUP) { @tmp.syswrite('.') } }, } @server = nil @@ -53,8 +56,10 @@ class SignalsTest < Test::Unit::TestCase 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) Process.kill(:KILL, pid) Process.waitpid(pid) + File.unlink("test_stderr.#{pid}.log", "test_stdout.#{pid}.log") t0 = Time.now end assert child @@ -137,8 +142,9 @@ class SignalsTest < Test::Unit::TestCase pid = buf[/\r\nX-Pid: (\d+)\r\n/, 1].to_i header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size end + assert pid > 0, "pid not positive: #{pid.inspect}" read = buf.size - mode_before = @tmp.stat.mode + size_before = @tmp.stat.size assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, Errno::EBADF) do loop do @@ -151,13 +157,17 @@ class SignalsTest < Test::Unit::TestCase redirect_test_io { @server.stop(true) } # can't check for == since pending signals get merged - assert mode_before < @tmp.stat.mode - assert_equal(read - header_len, @bs * @count) + assert size_before < @tmp.stat.size + got = read - header_len + expect = @bs * @count + assert_equal(expect, got, "expect=#{expect} got=#{got}") assert_nothing_raised { sock.close } end def test_request_read app = lambda { |env| + while env['rack.input'].read(4096) + end [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ] } redirect_test_io { @server = HttpServer.new(app, @server_opts).start } @@ -171,11 +181,12 @@ class SignalsTest < Test::Unit::TestCase sock.close end + assert pid > 0, "pid not positive: #{pid.inspect}" sock = TCPSocket.new('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) } - mode_before = @tmp.stat.mode + size_before = @tmp.stat.size killer = fork { loop { Process.kill(:HUP, pid); sleep(0.0001) } } buf = ' ' * @bs @count.times { sock.syswrite(buf) } @@ -183,7 +194,7 @@ class SignalsTest < Test::Unit::TestCase Process.waitpid2(killer) redirect_test_io { @server.stop(true) } # can't check for == since pending signals get merged - assert mode_before < @tmp.stat.mode + assert size_before < @tmp.stat.size assert_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i sock.close end diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb index 75d9f7b..c35b0c2 100644 --- a/test/unit/test_socket_helper.rb +++ b/test/unit/test_socket_helper.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'test/test_helper' require 'tempfile' @@ -61,6 +63,20 @@ class TestSocketHelper < Test::Unit::TestCase File.umask(old_umask) end + def test_bind_listen_unix_umask + old_umask = File.umask(0777) + tmp = Tempfile.new 'unix.sock' + @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 + ensure + File.umask(old_umask) + end + def test_bind_listen_unix_idempotent test_bind_listen_unix a = bind_listen(@unix_listener) diff --git a/test/unit/test_tee_input.rb b/test/unit/test_tee_input.rb new file mode 100644 index 0000000..403f698 --- /dev/null +++ b/test/unit/test_tee_input.rb @@ -0,0 +1,229 @@ +# -*- encoding: binary -*- + +require 'test/unit' +require 'digest/sha1' +require 'unicorn' + +class TestTeeInput < Test::Unit::TestCase + + def setup + @rs = $/ + @env = {} + @rd, @wr = IO.pipe + @rd.sync = @wr.sync = true + @start_pid = $$ + end + + def teardown + return if $$ != @start_pid + $/ = @rs + @rd.close rescue nil + @wr.close rescue nil + begin + Process.wait + rescue Errno::ECHILD + break + end while true + end + + def test_gets_long + init_parser("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size) + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + status = line = nil + pid = fork { + @rd.close + 3.times { @wr.write("ffff" * 4096) } + @wr.write "#$/foo#$/" + @wr.close + } + @wr.close + assert_nothing_raised { line = ti.gets } + assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size) + assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line) + assert_nothing_raised { line = ti.gets } + assert_equal "foo#$/", line + assert_nil ti.gets + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_gets_short + init_parser("hello", 5 + "#$/foo".size) + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + status = line = nil + pid = fork { + @rd.close + @wr.write "#$/foo" + @wr.close + } + @wr.close + assert_nothing_raised { line = ti.gets } + assert_equal("hello#$/", line) + assert_nothing_raised { line = ti.gets } + assert_equal "foo", line + assert_nil ti.gets + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_small_body + init_parser('hello') + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_equal 0, @parser.content_length + assert @parser.body_eof? + assert_equal StringIO, ti.instance_eval { @tmp.class } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal 5, ti.size + assert_equal 'hello', ti.read + assert_equal '', ti.read + assert_nil ti.read(4096) + end + + def test_read_with_buffer + init_parser('hello') + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + buf = '' + rv = ti.read(4, buf) + assert_equal 'hell', rv + assert_equal 'hell', buf + assert_equal rv.object_id, buf.object_id + assert_equal 'o', ti.read + assert_equal nil, ti.read(5, buf) + assert_equal 0, ti.rewind + assert_equal 'hello', ti.read(5, buf) + assert_equal 'hello', buf + end + + def test_big_body + init_parser('.' * Unicorn::Const::MAX_BODY << 'a') + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_equal 0, @parser.content_length + assert @parser.body_eof? + assert_kind_of File, ti.instance_eval { @tmp } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + end + + def test_read_in_full_if_content_length + a, b = 300, 3 + init_parser('.' * b, 300) + assert_equal 300, @parser.content_length + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + pid = fork { + @wr.write('.' * 197) + sleep 1 # still a *potential* race here that would make the test moot... + @wr.write('.' * 100) + } + assert_equal a, ti.read(a).size + _, status = Process.waitpid2(pid) + assert status.success? + @wr.close + end + + def test_big_body_multi + init_parser('.', Unicorn::Const::MAX_BODY + 1) + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_equal Unicorn::Const::MAX_BODY, @parser.content_length + assert ! @parser.body_eof? + assert_kind_of File, ti.instance_eval { @tmp } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal 1, ti.instance_eval { @tmp.size } + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + nr = Unicorn::Const::MAX_BODY / 4 + pid = fork { + @rd.close + nr.times { @wr.write('....') } + @wr.close + } + @wr.close + assert_equal '.', ti.read(1) + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + nr.times { + assert_equal '....', ti.read(4) + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + } + assert_nil ti.read(1) + status = nil + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_chunked + @parser = Unicorn::HttpParser.new + @buf = "POST / HTTP/1.1\r\n" \ + "Host: localhost\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" + assert_equal @env, @parser.headers(@env, @buf) + assert_equal "", @buf + + pid = fork { + @rd.close + 5.times { @wr.write("5\r\nabcde\r\n") } + @wr.write("0\r\n") + } + @wr.close + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_nil @parser.content_length + assert_nil ti.instance_eval { @size } + assert ! @parser.body_eof? + assert_equal 25, ti.size + assert @parser.body_eof? + assert_equal 25, ti.instance_eval { @size } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_nothing_raised { ti.rewind } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal 'abcdeabcdeabcdeabcde', ti.read(20) + assert_equal 20, ti.instance_eval { @tmp.pos } + assert_nothing_raised { ti.rewind } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_kind_of File, ti.instance_eval { @tmp } + status = nil + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_chunked_ping_pong + @parser = Unicorn::HttpParser.new + @buf = "POST / HTTP/1.1\r\n" \ + "Host: localhost\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" + assert_equal @env, @parser.headers(@env, @buf) + assert_equal "", @buf + chunks = %w(aa bbb cccc dddd eeee) + rd, wr = IO.pipe + + pid = fork { + chunks.each do |chunk| + rd.read(1) == "." and + @wr.write("#{'%x' % [ chunk.size]}\r\n#{chunk}\r\n") + end + @wr.write("0\r\n") + } + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_nil @parser.content_length + assert_nil ti.instance_eval { @size } + assert ! @parser.body_eof? + chunks.each do |chunk| + wr.write('.') + assert_equal chunk, ti.read(16384) + end + _, status = Process.waitpid2(pid) + assert status.success? + end + +private + + def init_parser(body, size = nil) + @parser = Unicorn::HttpParser.new + body = body.to_s.freeze + @buf = "POST / HTTP/1.1\r\n" \ + "Host: localhost\r\n" \ + "Content-Length: #{size || body.size}\r\n" \ + "\r\n#{body}" + assert_equal @env, @parser.headers(@env, @buf) + assert_equal body, @buf + end + +end diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb index 9ef3ed7..7ac3c9e 100644 --- a/test/unit/test_upload.rb +++ b/test/unit/test_upload.rb @@ -1,5 +1,8 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong require 'test/test_helper' +require 'digest/md5' include Unicorn @@ -18,29 +21,33 @@ class UploadTest < Test::Unit::TestCase @sha1 = Digest::SHA1.new @sha1_app = lambda do |env| input = env['rack.input'] - resp = { :pos => input.pos, :size => input.size, :class => input.class } + resp = {} - # sysread @sha1.reset - begin - loop { @sha1.update(input.sysread(@bs)) } - rescue EOFError + while buf = input.read(@bs) + @sha1.update(buf) end resp[:sha1] = @sha1.hexdigest - # read - input.sysseek(0) if input.respond_to?(:sysseek) + # rewind and read again input.rewind @sha1.reset - loop { - buf = input.read(@bs) or break + 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 @@ -54,7 +61,7 @@ class UploadTest < Test::Unit::TestCase 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 + @count.times do |i| buf = @random.sysread(@bs) @sha1.update(buf) sock.syswrite(buf) @@ -63,10 +70,34 @@ class UploadTest < Test::Unit::TestCase 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 0, resp[:pos] 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) @@ -85,42 +116,7 @@ class UploadTest < Test::Unit::TestCase 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 0, resp[:pos] assert_equal @sha1.hexdigest, resp[:sha1] - assert_equal StringIO, resp[:class] - end - - def test_tempfile_unlinked - spew_path = lambda do |env| - if orig = env['HTTP_X_OLD_PATH'] - assert orig != env['rack.input'].path - end - assert_equal length, env['rack.input'].size - [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ] - end - start_server(spew_path) - sock = TCPSocket.new(@addr, @port) - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(' ' * @bs) } - path = sock.read[/^X-Tempfile-Path: (\S+)/, 1] - sock.close - - # send another request to ensure we hit the next request - sock = TCPSocket.new(@addr, @port) - sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \ - "Content-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(' ' * @bs) } - path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1] - sock.close - assert path != path2 - - # make sure the next request comes in so the unlink got processed - sock = TCPSocket.new(@addr, @port) - sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n") - sock.sysread(4096) rescue nil - sock.close - - assert ! File.exist?(path) end def test_put_keepalive_truncates_small_overwrite @@ -136,75 +132,31 @@ class UploadTest < Test::Unit::TestCase sock.syswrite('12345') # write 4 bytes more than we expected @sha1.update('1') - read = sock.read.split(/\r\n/) + 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 0, resp[:pos] assert_equal @sha1.hexdigest, resp[:sha1] end def test_put_excessive_overwrite_closed - start_server(lambda { |env| [ 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 - end - - def test_put_handler_closed_file - nr = '0' start_server(lambda { |env| - env['rack.input'].close - resp = { :nr => nr.succ! } - [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ] + while env['rack.input'].read(65536); end + [ 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) } - 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 '1', resp[:nr] - # server still alive? - sock = TCPSocket.new(@addr, @port) - sock.syswrite("GET / HTTP/1.0\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 '2', resp[:nr] - end - - def test_renamed_file_not_closed - start_server(lambda { |env| - new_tmp = Tempfile.new('unicorn_test') - input = env['rack.input'] - File.rename(input.path, new_tmp.path) - resp = { - :inode => input.stat.ino, - :size => input.stat.size, - :new_tmp => new_tmp.path, - :old_tmp => input.path, - } - [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ] - }) - 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) } - 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: /, '')) - new_tmp = File.open(resp[:new_tmp]) - assert_equal resp[:inode], new_tmp.stat.ino - assert_equal length, resp[:size] - assert ! File.exist?(resp[:old_tmp]) - assert_equal resp[:size], new_tmp.stat.size + assert_raise(Errno::ECONNRESET, Errno::EPIPE) do + ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) } + end + assert_equal "HTTP/1.1 200 OK\r\n", sock.gets end # Despite reading numerous articles and inspecting the 1.9.1-p0 C @@ -233,7 +185,6 @@ class UploadTest < Test::Unit::TestCase resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` assert $?.success?, 'curl ran OK' assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/Tempfile/, resp) assert_match(/sysread_read_byte_match/, resp) # small StringIO path @@ -249,10 +200,87 @@ class UploadTest < Test::Unit::TestCase resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` assert $?.success?, 'curl ran OK' assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/StringIO/, 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 diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb index 032f0be..4a1e21f 100644 --- a/test/unit/test_util.rb +++ b/test/unit/test_util.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'test/test_helper' require 'tempfile' @@ -15,6 +17,7 @@ class TestUtil < Test::Unit::TestCase assert_equal before, File.stat(tmp.path).inspect assert_equal ext, (tmp.external_encoding rescue nil) assert_equal int, (tmp.internal_encoding rescue nil) + assert_nothing_raised { tmp.close! } end def test_reopen_logs_renamed @@ -37,6 +40,8 @@ class TestUtil < Test::Unit::TestCase assert_equal int, (tmp.internal_encoding rescue nil) assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL)) assert tmp.sync + assert_nothing_raised { tmp.close! } + assert_nothing_raised { to.close! } end def test_reopen_logs_renamed_with_encoding @@ -59,6 +64,7 @@ class TestUtil < Test::Unit::TestCase assert fp.sync } } + assert_nothing_raised { tmp.close! } end if STDIN.respond_to?(:external_encoding) def test_reopen_logs_renamed_with_internal_encoding @@ -84,6 +90,7 @@ class TestUtil < Test::Unit::TestCase } } } + assert_nothing_raised { tmp.close! } end if STDIN.respond_to?(:external_encoding) end |