diff options
Diffstat (limited to 'test')
88 files changed, 2506 insertions, 231 deletions
diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb index 712037c..014b270 100644 --- a/test/exec/test_exec.rb +++ b/test/exec/test_exec.rb @@ -1,43 +1,34 @@ # Copyright (c) 2009 Eric Wong -STDIN.sync = STDOUT.sync = STDERR.sync = true require 'test/test_helper' -require 'pathname' -require 'tempfile' -require 'fileutils' do_test = true -DEFAULT_TRIES = 1000 -DEFAULT_RES = 0.2 - $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn" redirect_test_io do do_test = system($unicorn_bin, '-v') end unless do_test - STDERR.puts "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \ - "skipping this test" + warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \ + "skipping this test" end -begin - require 'rack' -rescue LoadError - STDERR.puts "Unable to load Rack, skipping this test" +unless try_require('rack') + warn "Unable to load Rack, skipping this test" do_test = false end class ExecTest < Test::Unit::TestCase - trap('QUIT', 'IGNORE') + trap(:QUIT, 'IGNORE') HI = <<-EOS use Rack::ContentLength -run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] } +run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] } EOS HELLO = <<-EOS class Hello def call(env) - [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] + [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] end end EOS @@ -47,10 +38,9 @@ end HEAVY_CFG = <<-EOS worker_processes 4 timeout 30 -backlog 128 logger Logger.new('#{COMMON_TMP.path}') -before_fork do |server, worker_nr| - server.logger.info "before_fork: worker=\#{worker_nr}" +before_fork do |server, worker| + server.logger.info "before_fork: worker=\#{worker.nr}" end EOS @@ -82,6 +72,22 @@ end end 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") + status = nil + assert_nothing_raised do + Process.kill(sig, pid) + pid, status = Process.waitpid2(pid) + end + reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/) + assert_equal 1, reaped.size + assert status.exited? + end + end + def test_basic File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = fork do @@ -92,6 +98,28 @@ end 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") } } + log = "test_stderr.#{pid}.log" + wait_master_ready(log) + [ 2, 3].each { |i| + assert_nothing_raised { Process.kill(:TTIN, pid) } + wait_workers_ready(log, i) + } + File.truncate(log, 0) + reaped = nil + [ 2, 1, 0].each { |i| + assert_nothing_raised { Process.kill(:TTOU, pid) } + DEFAULT_TRIES.times { + sleep DEFAULT_RES + reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/) + break if reaped.size == 1 + } + assert_equal 1, reaped.size + } + end + def test_help redirect_test_io do assert(system($unicorn_bin, "-h"), "help text returns true") @@ -113,7 +141,7 @@ end pid_file = "#{@tmpdir}/test.pid" old_file = "#{pid_file}.oldbin" ucfg = Tempfile.new('unicorn_test_config') - ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n") + ucfg.syswrite("listen %(#@addr:#@port)\n") ucfg.syswrite("pid %(#{pid_file})\n") ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n") pid = xfork do @@ -126,14 +154,16 @@ end wait_for_file(pid_file) Process.waitpid(pid) - Process.kill('USR2', File.read(pid_file).to_i) + Process.kill(:USR2, File.read(pid_file).to_i) wait_for_file(old_file) wait_for_file(pid_file) - Process.kill('QUIT', File.read(old_file).to_i) + old_pid = File.read(old_file).to_i + Process.kill(:QUIT, old_pid) + wait_for_death(old_pid) ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug current_pid = File.read(pid_file).to_i - Process.kill('USR2', current_pid) + Process.kill(:USR2, current_pid) # wait for pid_file to restore itself tries = DEFAULT_TRIES @@ -156,9 +186,11 @@ end # fix the bug ucfg.sysseek(0) ucfg.truncate(0) - ucfg.syswrite("listeners %w(#{@addr}:#{@port} #{@addr}:#{port2})\n") + ucfg.syswrite("listen %(#@addr:#@port)\n") + ucfg.syswrite("listen %(#@addr:#{port2})\n") ucfg.syswrite("pid %(#{pid_file})\n") - Process.kill('USR2', current_pid) + assert_nothing_raised { Process.kill(:USR2, current_pid) } + wait_for_file(old_file) wait_for_file(pid_file) new_pid = File.read(pid_file).to_i @@ -170,8 +202,8 @@ end assert_equal String, results[1].class assert_nothing_raised do - Process.kill('QUIT', current_pid) - Process.kill('QUIT', new_pid) + Process.kill(:QUIT, current_pid) + Process.kill(:QUIT, new_pid) end end @@ -192,14 +224,16 @@ end wait_for_file(pid_file) Process.waitpid(pid) - Process.kill('USR2', File.read(pid_file).to_i) + Process.kill(:USR2, File.read(pid_file).to_i) wait_for_file(old_file) wait_for_file(pid_file) - Process.kill('QUIT', File.read(old_file).to_i) + old_pid = File.read(old_file).to_i + Process.kill(:QUIT, old_pid) + wait_for_death(old_pid) File.unlink("config.ru") # break reloading current_pid = File.read(pid_file).to_i - Process.kill('USR2', current_pid) + Process.kill(:USR2, current_pid) # wait for pid_file to restore itself tries = DEFAULT_TRIES @@ -210,17 +244,17 @@ end rescue Errno::ENOENT (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry end - assert_equal current_pid, File.read(pid_file).to_i tries = DEFAULT_TRIES while File.exist?(old_file) (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break end assert ! File.exist?(old_file), "oldbin=#{old_file} gone" + assert_equal current_pid, File.read(pid_file).to_i # fix the bug File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } - Process.kill('USR2', current_pid) + assert_nothing_raised { Process.kill(:USR2, current_pid) } wait_for_file(old_file) wait_for_file(pid_file) new_pid = File.read(pid_file).to_i @@ -230,25 +264,86 @@ end assert_equal String, results[0].class assert_nothing_raised do - Process.kill('QUIT', current_pid) - Process.kill('QUIT', new_pid) + Process.kill(:QUIT, current_pid) + Process.kill(:QUIT, new_pid) end end - def test_unicorn_config_listeners_overrides_cli - port2 = unused_port(@addr) + def test_unicorn_config_listener_swap + port_cli = unused_port File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } - # listeners = [ ... ] => should _override_ command-line options ucfg = Tempfile.new('unicorn_test_config') - ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n") + ucfg.syswrite("listen '#@addr:#@port'\n") pid = xfork do redirect_test_io do - exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}") + exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}") end end + results = retry_hit(["http://#@addr:#{port_cli}/"]) + assert_equal String, results[0].class + results = retry_hit(["http://#@addr:#@port/"]) + assert_equal String, results[0].class + + port2 = unused_port(@addr) + ucfg.sysseek(0) + ucfg.truncate(0) + ucfg.syswrite("listen '#@addr:#{port2}'\n") + Process.kill(:HUP, pid) + + results = retry_hit(["http://#@addr:#{port2}/"]) + assert_equal String, results[0].class + results = retry_hit(["http://#@addr:#{port_cli}/"]) + assert_equal String, results[0].class + assert_nothing_raised do + reuse = TCPServer.new(@addr, @port) + reuse.close + end + assert_shutdown(pid) + end + + def test_unicorn_config_listen_with_options + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n") + ucfg.syswrite(" :rcvbuf => 4096,\n") + ucfg.syswrite(" :sndbuf => 4096\n") + pid = xfork do + redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } + end + results = retry_hit(["http://#{@addr}:#{@port}/"]) + assert_equal String, results[0].class + assert_shutdown(pid) + end + + def test_unicorn_config_per_worker_listen + port2 = unused_port + pid_spit = 'use Rack::ContentLength;' \ + 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }' + File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) } + tmp = Tempfile.new('test.socket') + File.unlink(tmp.path) + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("listen '#@addr:#@port'\n") + ucfg.syswrite("before_fork { |s,w|\n") + ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n") + ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n") + ucfg.syswrite("\n}\n") + pid = xfork do + redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } + end results = retry_hit(["http://#{@addr}:#{@port}/"]) - assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) } assert_equal String, results[0].class + worker_pid = results[0].to_i + assert_not_equal pid, worker_pid + s = UNIXSocket.new(tmp.path) + s.syswrite("GET / HTTP/1.0\r\n\r\n") + results = '' + loop { results << s.sysread(4096) } rescue nil + assert_nothing_raised { s.close } + assert_equal worker_pid, results.split(/\r\n/).last.to_i + results = hit(["http://#@addr:#{port2}/"]) + assert_equal String, results[0].class + assert_equal worker_pid, results[0].to_i assert_shutdown(pid) end @@ -283,34 +378,36 @@ end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class wait_master_ready(COMMON_TMP.path) + wait_workers_ready(COMMON_TMP.path, 4) bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/) assert_equal 4, bf.size rotate = Tempfile.new('unicorn_rotate') assert_nothing_raised do File.rename(COMMON_TMP.path, rotate.path) - Process.kill('USR1', pid) + Process.kill(:USR1, pid) end wait_for_file(COMMON_TMP.path) assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists" # USR1 should've been passed to all workers tries = DEFAULT_TRIES log = File.readlines(rotate.path) - while (tries -= 1) > 0 && log.grep(/rotating logs\.\.\./).size < 4 + while (tries -= 1) > 0 && + log.grep(/reopening logs\.\.\./).size < 5 sleep DEFAULT_RES log = File.readlines(rotate.path) end - assert_equal 4, log.grep(/rotating logs\.\.\./).size - assert_equal 0, log.grep(/done rotating logs/).size + assert_equal 5, log.grep(/reopening logs\.\.\./).size + assert_equal 0, log.grep(/done reopening logs/).size tries = DEFAULT_TRIES log = File.readlines(COMMON_TMP.path) - while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 4 + while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5 sleep DEFAULT_RES log = File.readlines(COMMON_TMP.path) end - assert_equal 4, log.grep(/done rotating logs/).size - assert_equal 0, log.grep(/rotating logs\.\.\./).size - assert_nothing_raised { Process.kill('QUIT', pid) } + assert_equal 5, log.grep(/done reopening logs/).size + assert_equal 0, log.grep(/reopening logs\.\.\./).size + assert_nothing_raised { Process.kill(:QUIT, pid) } status = nil assert_nothing_raised { pid, status = Process.waitpid2(pid) } assert status.success?, "exited successfully" @@ -380,6 +477,39 @@ end reexec_basic_test(pid, pid_file) end + def test_socket_unlinked_restore + results = nil + sock = Tempfile.new('unicorn_test_sock') + sock_path = sock.path + @sockets << sock_path + sock.close! + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("listen \"#{sock_path}\"\n") + + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } } + wait_for_file(sock_path) + assert File.socket?(sock_path) + assert_nothing_raised do + sock = UNIXSocket.new(sock_path) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + results = sock.sysread(4096) + end + assert_equal String, results.class + assert_nothing_raised do + File.unlink(sock_path) + Process.kill(:HUP, pid) + end + wait_for_file(sock_path) + assert File.socket?(sock_path) + assert_nothing_raised do + sock = UNIXSocket.new(sock_path) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + results = sock.sysread(4096) + end + assert_equal String, results.class + end + def test_unicorn_config_file pid_file = "#{@tmpdir}/test.pid" sock = Tempfile.new('unicorn_test_sock') @@ -415,7 +545,7 @@ end assert_equal String, results.class # try reloading the config - sock = Tempfile.new('unicorn_test_sock') + sock = Tempfile.new('new_test_sock') new_sock_path = sock.path @sockets << new_sock_path sock.close! @@ -425,11 +555,12 @@ end assert_nothing_raised do ucfg = File.open(ucfg.path, "wb") + ucfg.syswrite("listen \"#{sock_path}\"\n") ucfg.syswrite("listen \"#{new_sock_path}\"\n") ucfg.syswrite("pid \"#{pid_file}\"\n") ucfg.syswrite("logger Logger.new('#{new_log.path}')\n") ucfg.close - Process.kill('HUP', pid) + Process.kill(:HUP, pid) end wait_for_file(new_sock_path) @@ -472,99 +603,72 @@ end reexec_usr2_quit_test(new_pid, pid_file) end - private + 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" + return + end + pid_file = "#{@tmpdir}/test.pid" + log = Tempfile.new('unicorn_test_log') + log.sync = true + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("pid \"#{pid_file}\"\n") + ucfg.syswrite("logger Logger.new('#{log.path}')\n") + ucfg.syswrite("stderr_path '#{log.path}'\n") + ucfg.syswrite("stdout_path '#{log.path}'\n") + ucfg.close - # sometimes the server may not come up right away - def retry_hit(uris = []) - tries = DEFAULT_TRIES - begin - hit(uris) - rescue Errno::ECONNREFUSED => err - if (tries -= 1) > 0 - sleep DEFAULT_RES - retry - end - raise err + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + pid = xfork do + redirect_test_io do + exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}") end end - def assert_shutdown(pid) - wait_master_ready("#{@tmpdir}/test_stderr.#{pid}.log") - assert_nothing_raised { Process.kill('QUIT', pid) } - status = nil - assert_nothing_raised { pid, status = Process.waitpid2(pid) } - assert status.success?, "exited successfully" - end + wait_master_ready(log.path) + File.truncate(log.path, 0) + wait_for_file(pid_file) + orig_pid = pid = File.read(pid_file).to_i + orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) + assert $?.success? + expect_size = orig_fds.size - def wait_master_ready(master_log) - tries = DEFAULT_TRIES - while (tries -= 1) > 0 - begin - File.readlines(master_log).grep(/master process ready/)[0] and return - rescue Errno::ENOENT - end - sleep DEFAULT_RES - end - raise "master process never became ready" + assert_nothing_raised do + Process.kill(:USR2, pid) + wait_for_file("#{pid_file}.oldbin") + Process.kill(:QUIT, pid) end + wait_for_death(pid) + + wait_master_ready(log.path) + File.truncate(log.path, 0) + wait_for_file(pid_file) + pid = File.read(pid_file).to_i + assert_not_equal orig_pid, pid + curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) + assert $?.success? - def reexec_usr2_quit_test(pid, pid_file) - assert File.exist?(pid_file), "pid file OK" - assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file" - assert_nothing_raised { Process.kill('USR2', pid) } - assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } + # we could've inherited descriptors the first time around + assert expect_size >= curr_fds.size, curr_fds.inspect + expect_size = curr_fds.size + + assert_nothing_raised do + Process.kill(:USR2, pid) wait_for_file("#{pid_file}.oldbin") - wait_for_file(pid_file) - - # kill old master process - assert_not_equal pid, File.read(pid_file).to_i - assert_equal pid, File.read("#{pid_file}.oldbin").to_i - assert_nothing_raised { Process.kill('QUIT', pid) } - assert_not_equal pid, File.read(pid_file).to_i - assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } - wait_for_file(pid_file) - assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } - assert_nothing_raised { Process.kill('QUIT', File.read(pid_file).to_i) } - end - - def reexec_basic_test(pid, pid_file) - results = retry_hit(["http://#{@addr}:#{@port}/"]) - assert_equal String, results[0].class - assert_nothing_raised { Process.kill(0, pid) } - master_log = "#{@tmpdir}/test_stderr.#{pid}.log" - wait_master_ready(master_log) - File.truncate(master_log, 0) - nr = 50 - kill_point = 2 - assert_nothing_raised do - nr.times do |i| - hit(["http://#{@addr}:#{@port}/#{i}"]) - i == kill_point and Process.kill('HUP', pid) - end - end - wait_master_ready(master_log) - assert File.exist?(pid_file), "pid=#{pid_file} exists" - new_pid = File.read(pid_file).to_i - assert_not_equal pid, new_pid - assert_nothing_raised { Process.kill(0, new_pid) } - assert_nothing_raised { Process.kill('QUIT', new_pid) } + Process.kill(:QUIT, pid) end + wait_for_death(pid) - def wait_for_file(path) - tries = DEFAULT_TRIES - while (tries -= 1) > 0 && ! File.exist?(path) - sleep DEFAULT_RES - end - assert File.exist?(path), "path=#{path} exists" - end + wait_master_ready(log.path) + File.truncate(log.path, 0) + wait_for_file(pid_file) + pid = File.read(pid_file).to_i + curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) + assert $?.success? + assert_equal expect_size, curr_fds.size, curr_fds.inspect - def xfork(&block) - fork do - ObjectSpace.each_object(Tempfile) do |tmp| - ObjectSpace.undefine_finalizer(tmp) - end - yield - end - end + Process.kill(:QUIT, pid) + wait_for_death(pid) + end end if do_test diff --git a/test/rails/app-1.2.3/.gitignore b/test/rails/app-1.2.3/.gitignore new file mode 100644 index 0000000..f451f91 --- /dev/null +++ b/test/rails/app-1.2.3/.gitignore @@ -0,0 +1,2 @@ +/tmp +/vendor diff --git a/test/rails/app-1.2.3/Rakefile b/test/rails/app-1.2.3/Rakefile new file mode 100644 index 0000000..fbebfca --- /dev/null +++ b/test/rails/app-1.2.3/Rakefile @@ -0,0 +1,7 @@ +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff --git a/test/rails/app-1.2.3/app/controllers/application.rb b/test/rails/app-1.2.3/app/controllers/application.rb new file mode 100644 index 0000000..ae8cac0 --- /dev/null +++ b/test/rails/app-1.2.3/app/controllers/application.rb @@ -0,0 +1,4 @@ +class ApplicationController < ActionController::Base + # Pick a unique cookie name to distinguish our session data from others' + session :session_key => "_unicorn_rails_test.#{rand}" +end 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 new file mode 100644 index 0000000..8d877d1 --- /dev/null +++ b/test/rails/app-1.2.3/app/controllers/foo_controller.rb @@ -0,0 +1,34 @@ +require 'digest/sha1' +class FooController < ApplicationController + def index + render :text => "FOO\n" + end + + def xcookie + cookies["foo"] = "cookie #$$" + render :text => "" + end + + def xnotice + flash[:notice] = "session #$$" + render :text => "" + end + + def xpost + if request.post? + digest = Digest::SHA1.new + out = "params: #{params.inspect}\n" + if file = params[:file] + loop do + buf = file.read(4096) or break + digest.update(buf) + end + out << "sha1: #{digest.to_s}\n" + end + headers['content-type'] = 'text/plain' + render :text => out + else + render :status => 403, :text => "need post\n" + end + end +end 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 new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/test/rails/app-1.2.3/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..71c7d7c --- /dev/null +++ b/test/rails/app-1.2.3/config/boot.rb @@ -0,0 +1,9 @@ +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + RAILS_ROOT = root_path +end + +unless defined?(Rails::Initializer) + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:set_load_path) +end diff --git a/test/rails/app-1.2.3/config/database.yml b/test/rails/app-1.2.3/config/database.yml new file mode 100644 index 0000000..9f77843 --- /dev/null +++ b/test/rails/app-1.2.3/config/database.yml @@ -0,0 +1,12 @@ +development: + adapter: sqlite3 + database: db/development.sqlite3 + timeout: 5000 +test: + adapter: sqlite3 + database: db/test.sqlite3 + timeout: 5000 +production: + adapter: sqlite3 + database: db/production.sqlite3 + timeout: 5000 diff --git a/test/rails/app-1.2.3/config/environment.rb b/test/rails/app-1.2.3/config/environment.rb new file mode 100644 index 0000000..2ef6b4a --- /dev/null +++ b/test/rails/app-1.2.3/config/environment.rb @@ -0,0 +1,11 @@ +unless defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] # || '1.2.3' +end + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + config.frameworks -= [ :action_web_service, :action_mailer ] + config.action_controller.session_store = :active_record_store +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 new file mode 100644 index 0000000..032fb46 --- /dev/null +++ b/test/rails/app-1.2.3/config/environments/development.rb @@ -0,0 +1,7 @@ +config.cache_classes = false +config.whiny_nils = true +config.breakpoint_server = true +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false +config.action_view.cache_template_extensions = false +config.action_view.debug_rjs = 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 new file mode 100644 index 0000000..c4059e3 --- /dev/null +++ b/test/rails/app-1.2.3/config/environments/production.rb @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..774028f --- /dev/null +++ b/test/rails/app-1.2.3/config/routes.rb @@ -0,0 +1,4 @@ +ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id.:format' + map.connect ':controller/:action/:id' +end diff --git a/test/rails/app-1.2.3/db/.gitignore b/test/rails/app-1.2.3/db/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/rails/app-1.2.3/db/.gitignore diff --git a/test/rails/app-1.2.3/log/.gitignore b/test/rails/app-1.2.3/log/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/test/rails/app-1.2.3/log/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/test/rails/app-1.2.3/public/404.html b/test/rails/app-1.2.3/public/404.html new file mode 100644 index 0000000..44d986c --- /dev/null +++ b/test/rails/app-1.2.3/public/404.html @@ -0,0 +1 @@ +404 Not Found diff --git a/test/rails/app-1.2.3/public/500.html b/test/rails/app-1.2.3/public/500.html new file mode 100644 index 0000000..e534a49 --- /dev/null +++ b/test/rails/app-1.2.3/public/500.html @@ -0,0 +1 @@ +500 Internal Server Error diff --git a/test/rails/app-2.0.2/.gitignore b/test/rails/app-2.0.2/.gitignore new file mode 100644 index 0000000..f451f91 --- /dev/null +++ b/test/rails/app-2.0.2/.gitignore @@ -0,0 +1,2 @@ +/tmp +/vendor diff --git a/test/rails/app-2.0.2/Rakefile b/test/rails/app-2.0.2/Rakefile new file mode 100644 index 0000000..fbebfca --- /dev/null +++ b/test/rails/app-2.0.2/Rakefile @@ -0,0 +1,7 @@ +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff --git a/test/rails/app-2.0.2/app/controllers/application.rb b/test/rails/app-2.0.2/app/controllers/application.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/test/rails/app-2.0.2/app/controllers/application.rb @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..8d877d1 --- /dev/null +++ b/test/rails/app-2.0.2/app/controllers/foo_controller.rb @@ -0,0 +1,34 @@ +require 'digest/sha1' +class FooController < ApplicationController + def index + render :text => "FOO\n" + end + + def xcookie + cookies["foo"] = "cookie #$$" + render :text => "" + end + + def xnotice + flash[:notice] = "session #$$" + render :text => "" + end + + def xpost + if request.post? + digest = Digest::SHA1.new + out = "params: #{params.inspect}\n" + if file = params[:file] + loop do + buf = file.read(4096) or break + digest.update(buf) + end + out << "sha1: #{digest.to_s}\n" + end + headers['content-type'] = 'text/plain' + render :text => out + else + render :status => 403, :text => "need post\n" + end + end +end 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 new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/test/rails/app-2.0.2/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..71c7d7c --- /dev/null +++ b/test/rails/app-2.0.2/config/boot.rb @@ -0,0 +1,9 @@ +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + RAILS_ROOT = root_path +end + +unless defined?(Rails::Initializer) + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:set_load_path) +end diff --git a/test/rails/app-2.0.2/config/database.yml b/test/rails/app-2.0.2/config/database.yml new file mode 100644 index 0000000..9f77843 --- /dev/null +++ b/test/rails/app-2.0.2/config/database.yml @@ -0,0 +1,12 @@ +development: + adapter: sqlite3 + database: db/development.sqlite3 + timeout: 5000 +test: + adapter: sqlite3 + database: db/test.sqlite3 + timeout: 5000 +production: + adapter: sqlite3 + database: db/production.sqlite3 + timeout: 5000 diff --git a/test/rails/app-2.0.2/config/environment.rb b/test/rails/app-2.0.2/config/environment.rb new file mode 100644 index 0000000..7c720f6 --- /dev/null +++ b/test/rails/app-2.0.2/config/environment.rb @@ -0,0 +1,15 @@ +unless defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] +end + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + config.frameworks -= [ :action_web_service, :action_mailer ] + config.action_controller.session_store = :active_record_store + config.action_controller.session = { + :session_key => "_unicorn_rails_test.#{rand}", + :secret => "#{rand}#{rand}#{rand}#{rand}", + } +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 new file mode 100644 index 0000000..6a613c1 --- /dev/null +++ b/test/rails/app-2.0.2/config/environments/development.rb @@ -0,0 +1,6 @@ +config.cache_classes = false +config.whiny_nils = true +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false +config.action_view.cache_template_extensions = false +config.action_view.debug_rjs = 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 new file mode 100644 index 0000000..c4059e3 --- /dev/null +++ b/test/rails/app-2.0.2/config/environments/production.rb @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..774028f --- /dev/null +++ b/test/rails/app-2.0.2/config/routes.rb @@ -0,0 +1,4 @@ +ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id.:format' + map.connect ':controller/:action/:id' +end diff --git a/test/rails/app-2.0.2/db/.gitignore b/test/rails/app-2.0.2/db/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/rails/app-2.0.2/db/.gitignore diff --git a/test/rails/app-2.0.2/log/.gitignore b/test/rails/app-2.0.2/log/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/test/rails/app-2.0.2/log/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/test/rails/app-2.0.2/public/404.html b/test/rails/app-2.0.2/public/404.html new file mode 100644 index 0000000..44d986c --- /dev/null +++ b/test/rails/app-2.0.2/public/404.html @@ -0,0 +1 @@ +404 Not Found diff --git a/test/rails/app-2.0.2/public/500.html b/test/rails/app-2.0.2/public/500.html new file mode 100644 index 0000000..e534a49 --- /dev/null +++ b/test/rails/app-2.0.2/public/500.html @@ -0,0 +1 @@ +500 Internal Server Error diff --git a/test/rails/app-2.1.2/.gitignore b/test/rails/app-2.1.2/.gitignore new file mode 100644 index 0000000..f451f91 --- /dev/null +++ b/test/rails/app-2.1.2/.gitignore @@ -0,0 +1,2 @@ +/tmp +/vendor diff --git a/test/rails/app-2.1.2/Rakefile b/test/rails/app-2.1.2/Rakefile new file mode 100644 index 0000000..fbebfca --- /dev/null +++ b/test/rails/app-2.1.2/Rakefile @@ -0,0 +1,7 @@ +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff --git a/test/rails/app-2.1.2/app/controllers/application.rb b/test/rails/app-2.1.2/app/controllers/application.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/test/rails/app-2.1.2/app/controllers/application.rb @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..8d877d1 --- /dev/null +++ b/test/rails/app-2.1.2/app/controllers/foo_controller.rb @@ -0,0 +1,34 @@ +require 'digest/sha1' +class FooController < ApplicationController + def index + render :text => "FOO\n" + end + + def xcookie + cookies["foo"] = "cookie #$$" + render :text => "" + end + + def xnotice + flash[:notice] = "session #$$" + render :text => "" + end + + def xpost + if request.post? + digest = Digest::SHA1.new + out = "params: #{params.inspect}\n" + if file = params[:file] + loop do + buf = file.read(4096) or break + digest.update(buf) + end + out << "sha1: #{digest.to_s}\n" + end + headers['content-type'] = 'text/plain' + render :text => out + else + render :status => 403, :text => "need post\n" + end + end +end 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 new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/test/rails/app-2.1.2/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..0a51688 --- /dev/null +++ b/test/rails/app-2.1.2/config/boot.rb @@ -0,0 +1,109 @@ +# Don't change this file! +# Configure your app in config/environment.rb and config/environments/*.rb + +RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) + +module Rails + class << self + def boot! + unless booted? + preinitialize + pick_boot.run + end + end + + def booted? + defined? Rails::Initializer + end + + def pick_boot + (vendor_rails? ? VendorBoot : GemBoot).new + end + + def vendor_rails? + File.exist?("#{RAILS_ROOT}/vendor/rails") + end + + def preinitialize + load(preinitializer_path) if File.exist?(preinitializer_path) + end + + def preinitializer_path + "#{RAILS_ROOT}/config/preinitializer.rb" + end + end + + class Boot + def run + load_initializer + Rails::Initializer.run(:set_load_path) + end + end + + class VendorBoot < Boot + def load_initializer + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:install_gem_spec_stubs) + end + end + + class GemBoot < Boot + def load_initializer + self.class.load_rubygems + load_rails_gem + require 'initializer' + end + + def load_rails_gem + if version = self.class.gem_version + gem 'rails', version + else + gem 'rails' + end + rescue Gem::LoadError => load_error + $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) + exit 1 + end + + class << self + def rubygems_version + Gem::RubyGemsVersion rescue nil + end + + def gem_version + if defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION + elsif ENV.include?('RAILS_GEM_VERSION') + ENV['RAILS_GEM_VERSION'] + else + parse_gem_version(read_environment_rb) + end + end + + def load_rubygems + require 'rubygems' + min_version = '1.3.1' + unless rubygems_version >= min_version + $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) + exit 1 + end + + rescue LoadError + $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) + exit 1 + end + + def parse_gem_version(text) + $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ + end + + private + def read_environment_rb + File.read("#{RAILS_ROOT}/config/environment.rb") + end + end + end +end + +# All that for this: +Rails.boot! diff --git a/test/rails/app-2.1.2/config/database.yml b/test/rails/app-2.1.2/config/database.yml new file mode 100644 index 0000000..9f77843 --- /dev/null +++ b/test/rails/app-2.1.2/config/database.yml @@ -0,0 +1,12 @@ +development: + adapter: sqlite3 + database: db/development.sqlite3 + timeout: 5000 +test: + adapter: sqlite3 + database: db/test.sqlite3 + timeout: 5000 +production: + adapter: sqlite3 + database: db/production.sqlite3 + timeout: 5000 diff --git a/test/rails/app-2.1.2/config/environment.rb b/test/rails/app-2.1.2/config/environment.rb new file mode 100644 index 0000000..7c720f6 --- /dev/null +++ b/test/rails/app-2.1.2/config/environment.rb @@ -0,0 +1,15 @@ +unless defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] +end + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + config.frameworks -= [ :action_web_service, :action_mailer ] + config.action_controller.session_store = :active_record_store + config.action_controller.session = { + :session_key => "_unicorn_rails_test.#{rand}", + :secret => "#{rand}#{rand}#{rand}#{rand}", + } +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 new file mode 100644 index 0000000..7f49032 --- /dev/null +++ b/test/rails/app-2.1.2/config/environments/development.rb @@ -0,0 +1,5 @@ +config.cache_classes = false +config.whiny_nils = true +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false +config.action_view.debug_rjs = 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 new file mode 100644 index 0000000..c4059e3 --- /dev/null +++ b/test/rails/app-2.1.2/config/environments/production.rb @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..774028f --- /dev/null +++ b/test/rails/app-2.1.2/config/routes.rb @@ -0,0 +1,4 @@ +ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id.:format' + map.connect ':controller/:action/:id' +end diff --git a/test/rails/app-2.1.2/db/.gitignore b/test/rails/app-2.1.2/db/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/rails/app-2.1.2/db/.gitignore diff --git a/test/rails/app-2.1.2/log/.gitignore b/test/rails/app-2.1.2/log/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/test/rails/app-2.1.2/log/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/test/rails/app-2.1.2/public/404.html b/test/rails/app-2.1.2/public/404.html new file mode 100644 index 0000000..44d986c --- /dev/null +++ b/test/rails/app-2.1.2/public/404.html @@ -0,0 +1 @@ +404 Not Found diff --git a/test/rails/app-2.1.2/public/500.html b/test/rails/app-2.1.2/public/500.html new file mode 100644 index 0000000..e534a49 --- /dev/null +++ b/test/rails/app-2.1.2/public/500.html @@ -0,0 +1 @@ +500 Internal Server Error diff --git a/test/rails/app-2.2.2/.gitignore b/test/rails/app-2.2.2/.gitignore new file mode 100644 index 0000000..f451f91 --- /dev/null +++ b/test/rails/app-2.2.2/.gitignore @@ -0,0 +1,2 @@ +/tmp +/vendor diff --git a/test/rails/app-2.2.2/Rakefile b/test/rails/app-2.2.2/Rakefile new file mode 100644 index 0000000..fbebfca --- /dev/null +++ b/test/rails/app-2.2.2/Rakefile @@ -0,0 +1,7 @@ +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff --git a/test/rails/app-2.2.2/app/controllers/application.rb b/test/rails/app-2.2.2/app/controllers/application.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/test/rails/app-2.2.2/app/controllers/application.rb @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..8d877d1 --- /dev/null +++ b/test/rails/app-2.2.2/app/controllers/foo_controller.rb @@ -0,0 +1,34 @@ +require 'digest/sha1' +class FooController < ApplicationController + def index + render :text => "FOO\n" + end + + def xcookie + cookies["foo"] = "cookie #$$" + render :text => "" + end + + def xnotice + flash[:notice] = "session #$$" + render :text => "" + end + + def xpost + if request.post? + digest = Digest::SHA1.new + out = "params: #{params.inspect}\n" + if file = params[:file] + loop do + buf = file.read(4096) or break + digest.update(buf) + end + out << "sha1: #{digest.to_s}\n" + end + headers['content-type'] = 'text/plain' + render :text => out + else + render :status => 403, :text => "need post\n" + end + end +end 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 new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/test/rails/app-2.2.2/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..0a51688 --- /dev/null +++ b/test/rails/app-2.2.2/config/boot.rb @@ -0,0 +1,109 @@ +# Don't change this file! +# Configure your app in config/environment.rb and config/environments/*.rb + +RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) + +module Rails + class << self + def boot! + unless booted? + preinitialize + pick_boot.run + end + end + + def booted? + defined? Rails::Initializer + end + + def pick_boot + (vendor_rails? ? VendorBoot : GemBoot).new + end + + def vendor_rails? + File.exist?("#{RAILS_ROOT}/vendor/rails") + end + + def preinitialize + load(preinitializer_path) if File.exist?(preinitializer_path) + end + + def preinitializer_path + "#{RAILS_ROOT}/config/preinitializer.rb" + end + end + + class Boot + def run + load_initializer + Rails::Initializer.run(:set_load_path) + end + end + + class VendorBoot < Boot + def load_initializer + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:install_gem_spec_stubs) + end + end + + class GemBoot < Boot + def load_initializer + self.class.load_rubygems + load_rails_gem + require 'initializer' + end + + def load_rails_gem + if version = self.class.gem_version + gem 'rails', version + else + gem 'rails' + end + rescue Gem::LoadError => load_error + $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) + exit 1 + end + + class << self + def rubygems_version + Gem::RubyGemsVersion rescue nil + end + + def gem_version + if defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION + elsif ENV.include?('RAILS_GEM_VERSION') + ENV['RAILS_GEM_VERSION'] + else + parse_gem_version(read_environment_rb) + end + end + + def load_rubygems + require 'rubygems' + min_version = '1.3.1' + unless rubygems_version >= min_version + $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) + exit 1 + end + + rescue LoadError + $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) + exit 1 + end + + def parse_gem_version(text) + $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ + end + + private + def read_environment_rb + File.read("#{RAILS_ROOT}/config/environment.rb") + end + end + end +end + +# All that for this: +Rails.boot! diff --git a/test/rails/app-2.2.2/config/database.yml b/test/rails/app-2.2.2/config/database.yml new file mode 100644 index 0000000..9f77843 --- /dev/null +++ b/test/rails/app-2.2.2/config/database.yml @@ -0,0 +1,12 @@ +development: + adapter: sqlite3 + database: db/development.sqlite3 + timeout: 5000 +test: + adapter: sqlite3 + database: db/test.sqlite3 + timeout: 5000 +production: + adapter: sqlite3 + database: db/production.sqlite3 + timeout: 5000 diff --git a/test/rails/app-2.2.2/config/environment.rb b/test/rails/app-2.2.2/config/environment.rb new file mode 100644 index 0000000..7c720f6 --- /dev/null +++ b/test/rails/app-2.2.2/config/environment.rb @@ -0,0 +1,15 @@ +unless defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] +end + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + config.frameworks -= [ :action_web_service, :action_mailer ] + config.action_controller.session_store = :active_record_store + config.action_controller.session = { + :session_key => "_unicorn_rails_test.#{rand}", + :secret => "#{rand}#{rand}#{rand}#{rand}", + } +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 new file mode 100644 index 0000000..7f49032 --- /dev/null +++ b/test/rails/app-2.2.2/config/environments/development.rb @@ -0,0 +1,5 @@ +config.cache_classes = false +config.whiny_nils = true +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false +config.action_view.debug_rjs = 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 new file mode 100644 index 0000000..c4059e3 --- /dev/null +++ b/test/rails/app-2.2.2/config/environments/production.rb @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..774028f --- /dev/null +++ b/test/rails/app-2.2.2/config/routes.rb @@ -0,0 +1,4 @@ +ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id.:format' + map.connect ':controller/:action/:id' +end diff --git a/test/rails/app-2.2.2/db/.gitignore b/test/rails/app-2.2.2/db/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/rails/app-2.2.2/db/.gitignore diff --git a/test/rails/app-2.2.2/log/.gitignore b/test/rails/app-2.2.2/log/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/test/rails/app-2.2.2/log/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/test/rails/app-2.2.2/public/404.html b/test/rails/app-2.2.2/public/404.html new file mode 100644 index 0000000..44d986c --- /dev/null +++ b/test/rails/app-2.2.2/public/404.html @@ -0,0 +1 @@ +404 Not Found diff --git a/test/rails/app-2.2.2/public/500.html b/test/rails/app-2.2.2/public/500.html new file mode 100644 index 0000000..e534a49 --- /dev/null +++ b/test/rails/app-2.2.2/public/500.html @@ -0,0 +1 @@ +500 Internal Server Error diff --git a/test/rails/app-2.3.2.1/.gitignore b/test/rails/app-2.3.2.1/.gitignore new file mode 100644 index 0000000..f451f91 --- /dev/null +++ b/test/rails/app-2.3.2.1/.gitignore @@ -0,0 +1,2 @@ +/tmp +/vendor diff --git a/test/rails/app-2.3.2.1/Rakefile b/test/rails/app-2.3.2.1/Rakefile new file mode 100644 index 0000000..fbebfca --- /dev/null +++ b/test/rails/app-2.3.2.1/Rakefile @@ -0,0 +1,7 @@ +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' diff --git a/test/rails/app-2.3.2.1/app/controllers/application_controller.rb b/test/rails/app-2.3.2.1/app/controllers/application_controller.rb new file mode 100644 index 0000000..6160f52 --- /dev/null +++ b/test/rails/app-2.3.2.1/app/controllers/application_controller.rb @@ -0,0 +1,3 @@ +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.2.1/app/controllers/foo_controller.rb new file mode 100644 index 0000000..261669c --- /dev/null +++ b/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb @@ -0,0 +1,34 @@ +require 'digest/sha1' +class FooController < ApplicationController + def index + render :text => "FOO\n" + end + + def xcookie + cookies["foo"] = "cookie-#$$-#{session[:gotta_use_the_session_in_2_3]}" + render :text => "" + end + + def xnotice + flash[:notice] = "session #$$" + render :text => "" + end + + def xpost + if request.post? + digest = Digest::SHA1.new + out = "params: #{params.inspect}\n" + if file = params[:file] + loop do + buf = file.read(4096) or break + digest.update(buf) + end + out << "sha1: #{digest.to_s}\n" + end + headers['content-type'] = 'text/plain' + render :text => out + else + render :status => 403, :text => "need post\n" + end + end +end diff --git a/test/rails/app-2.3.2.1/app/helpers/application_helper.rb b/test/rails/app-2.3.2.1/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/test/rails/app-2.3.2.1/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/test/rails/app-2.3.2.1/config/boot.rb b/test/rails/app-2.3.2.1/config/boot.rb new file mode 100644 index 0000000..d22e6b0 --- /dev/null +++ b/test/rails/app-2.3.2.1/config/boot.rb @@ -0,0 +1,107 @@ +RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) + +module Rails + class << self + def boot! + unless booted? + preinitialize + pick_boot.run + end + end + + def booted? + defined? Rails::Initializer + end + + def pick_boot + (vendor_rails? ? VendorBoot : GemBoot).new + end + + def vendor_rails? + File.exist?("#{RAILS_ROOT}/vendor/rails") + end + + def preinitialize + load(preinitializer_path) if File.exist?(preinitializer_path) + end + + def preinitializer_path + "#{RAILS_ROOT}/config/preinitializer.rb" + end + end + + class Boot + def run + load_initializer + Rails::Initializer.run(:set_load_path) + end + end + + class VendorBoot < Boot + def load_initializer + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:install_gem_spec_stubs) + Rails::GemDependency.add_frozen_gem_path + end + end + + class GemBoot < Boot + def load_initializer + self.class.load_rubygems + load_rails_gem + require 'initializer' + end + + def load_rails_gem + if version = self.class.gem_version + gem 'rails', version + else + gem 'rails' + end + rescue Gem::LoadError => load_error + $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) + exit 1 + end + + class << self + def rubygems_version + Gem::RubyGemsVersion rescue nil + end + + def gem_version + if defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION + elsif ENV.include?('RAILS_GEM_VERSION') + ENV['RAILS_GEM_VERSION'] + else + parse_gem_version(read_environment_rb) + end + end + + def load_rubygems + require 'rubygems' + min_version = '1.3.1' + unless rubygems_version >= min_version + $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) + exit 1 + end + + rescue LoadError + $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) + exit 1 + end + + def parse_gem_version(text) + $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ + end + + private + def read_environment_rb + File.read("#{RAILS_ROOT}/config/environment.rb") + end + end + end +end + +# All that for this: +Rails.boot! diff --git a/test/rails/app-2.3.2.1/config/database.yml b/test/rails/app-2.3.2.1/config/database.yml new file mode 100644 index 0000000..9f77843 --- /dev/null +++ b/test/rails/app-2.3.2.1/config/database.yml @@ -0,0 +1,12 @@ +development: + adapter: sqlite3 + database: db/development.sqlite3 + timeout: 5000 +test: + adapter: sqlite3 + database: db/test.sqlite3 + timeout: 5000 +production: + adapter: sqlite3 + database: db/production.sqlite3 + timeout: 5000 diff --git a/test/rails/app-2.3.2.1/config/environment.rb b/test/rails/app-2.3.2.1/config/environment.rb new file mode 100644 index 0000000..17abdb7 --- /dev/null +++ b/test/rails/app-2.3.2.1/config/environment.rb @@ -0,0 +1,15 @@ +unless defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] +end + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + config.frameworks -= [ :active_resource, :action_mailer ] + config.action_controller.session_store = :active_record_store + config.action_controller.session = { + :session_key => "_unicorn_rails_test.#{rand}", + :secret => "#{rand}#{rand}#{rand}#{rand}", + } +end diff --git a/test/rails/app-2.3.2.1/config/environments/development.rb b/test/rails/app-2.3.2.1/config/environments/development.rb new file mode 100644 index 0000000..55376c5 --- /dev/null +++ b/test/rails/app-2.3.2.1/config/environments/development.rb @@ -0,0 +1,5 @@ +config.cache_classes = false +config.whiny_nils = true +config.action_controller.consider_all_requests_local = true +config.action_view.debug_rjs = true +config.action_controller.perform_caching = false diff --git a/test/rails/app-2.3.2.1/config/environments/production.rb b/test/rails/app-2.3.2.1/config/environments/production.rb new file mode 100644 index 0000000..474257d --- /dev/null +++ b/test/rails/app-2.3.2.1/config/environments/production.rb @@ -0,0 +1,4 @@ +config.cache_classes = true +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true +config.action_view.cache_template_loading = true diff --git a/test/rails/app-2.3.2.1/config/routes.rb b/test/rails/app-2.3.2.1/config/routes.rb new file mode 100644 index 0000000..4248853 --- /dev/null +++ b/test/rails/app-2.3.2.1/config/routes.rb @@ -0,0 +1,4 @@ +ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id' + map.connect ':controller/:action/:id.:format' +end diff --git a/test/rails/app-2.3.2.1/db/.gitignore b/test/rails/app-2.3.2.1/db/.gitignore new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/rails/app-2.3.2.1/db/.gitignore diff --git a/test/rails/app-2.3.2.1/log/.gitignore b/test/rails/app-2.3.2.1/log/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/test/rails/app-2.3.2.1/log/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/test/rails/app-2.3.2.1/public/404.html b/test/rails/app-2.3.2.1/public/404.html new file mode 100644 index 0000000..44d986c --- /dev/null +++ b/test/rails/app-2.3.2.1/public/404.html @@ -0,0 +1 @@ +404 Not Found diff --git a/test/rails/app-2.3.2.1/public/500.html b/test/rails/app-2.3.2.1/public/500.html new file mode 100644 index 0000000..e534a49 --- /dev/null +++ b/test/rails/app-2.3.2.1/public/500.html @@ -0,0 +1 @@ +500 Internal Server Error diff --git a/test/rails/test_rails.rb b/test/rails/test_rails.rb new file mode 100644 index 0000000..c7add20 --- /dev/null +++ b/test/rails/test_rails.rb @@ -0,0 +1,247 @@ +# Copyright (c) 2009 Eric Wong +require 'test/test_helper' + +# don't call exit(0) since it may be run under rake (but gmake is recommended) +do_test = true + +$unicorn_rails_bin = ENV['UNICORN_RAILS_TEST_BIN'] || "unicorn_rails" +redirect_test_io { do_test = system($unicorn_rails_bin, '-v') } + +unless do_test + warn "#$unicorn_rails_bin not found in PATH=#{ENV['PATH']}, " \ + "skipping this test" +end + +unless which('git') + warn "git not found in PATH=#{ENV['PATH']}, skipping this test" + do_test = false +end + +if RAILS_GIT_REPO = ENV['RAILS_GIT_REPO'] + unless File.directory?(RAILS_GIT_REPO) + warn "#{RAILS_GIT_REPO} not found, create it with:\n" \ + "\tgit clone --mirror git://github.com/rails/rails #{RAILS_GIT_REPO}" \ + "skipping this test for now" + do_test = false + end +else + warn "RAILS_GIT_REPO not defined, don't know where to git clone from" + do_test = false +end + +unless UNICORN_RAILS_TEST_VERSION = ENV['UNICORN_RAILS_TEST_VERSION'] + warn 'UNICORN_RAILS_TEST_VERSION not defined in environment, ' \ + 'skipping this test' + do_test = false +end + +RAILS_ROOT = "#{File.dirname(__FILE__)}/app-#{UNICORN_RAILS_TEST_VERSION}" +unless File.directory?(RAILS_ROOT) + warn "unsupported UNICORN_RAILS_TEST_VERSION=#{UNICORN_RAILS_TEST_VERSION}" + do_test = false +end + +ROR_V = UNICORN_RAILS_TEST_VERSION.split(/\./).map { |x| x.to_i } +RB_V = RUBY_VERSION.split(/\./).map { |x| x.to_i } +if RB_V[0] >= 1 && RB_V[1] >= 9 + unless ROR_V[0] >= 2 && ROR_V[1] >= 3 + warn "skipping Ruby >=1.9 test with Rails <2.3" + do_test = false + end +end + +class RailsTest < Test::Unit::TestCase + trap(:QUIT, 'IGNORE') + + COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP) + + HEAVY_CFG = <<-EOS +worker_processes 2 +timeout 30 +logger Logger.new('#{COMMON_TMP.path}') + EOS + + def setup + @pwd = Dir.pwd + @tmpfile = Tempfile.new('unicorn_rails_test') + @tmpdir = @tmpfile.path + @tmpfile.close! + assert_nothing_raised do + FileUtils.cp_r(RAILS_ROOT, @tmpdir, :preserve => true) + end + Dir.chdir(@tmpdir) + system('git', 'clone', '-nsq', RAILS_GIT_REPO, 'vendor/rails') + Dir.chdir("#@tmpdir/vendor/rails") do + system('git', 'reset', '-q', '--hard', "v#{UNICORN_RAILS_TEST_VERSION}") + end + + assert(system('rake', 'db:sessions:create')) + assert(system('rake', 'db:migrate')) + + @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' + @port = unused_port(@addr) + @start_pid = $$ + @pid = nil + end + + def test_launcher + tmp_dirs = %w(cache pids sessions sockets) + tmp_dirs.each { |dir| assert(! File.exist?("tmp/#{dir}")) } + redirect_test_io { @pid = fork { exec 'unicorn_rails', "-l#@addr:#@port" } } + wait_master_ready("test_stderr.#$$.log") + + # temp dirs exist + tmp_dirs.each { |dir| assert(File.directory?("tmp/#{dir}")) } + + # basic GET + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo")) + assert_equal "FOO\n", res.body + assert_match %r{^text/html\b}, res['Content-Type'] + assert_equal "4", res['Content-Length'] + assert_equal "200 OK", res['Status'] + + # can we set cookies? + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo/xcookie")) + assert_equal "200", res.code + assert_equal "200 OK", res['Status'] + cookies = res.get_fields('Set-Cookie') + assert_equal 2, cookies.size + assert_equal 1, cookies.grep(/\A_unicorn_rails_test\./).size + assert_equal 1, cookies.grep(/\Afoo=cookie/).size + + # how about just a session? + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo/xnotice")) + assert_equal "200", res.code + assert_equal "200 OK", res['Status'] + cookies = res.get_fields('Set-Cookie') + assert_equal 1, cookies.size + assert_equal 1, cookies.grep(/\A_unicorn_rails_test\./).size + + # posting forms? + uri = URI.parse("http://#@addr:#@port/foo/xpost") + wait_master_ready("test_stderr.#$$.log") + res = Net::HTTP.post_form(uri, {"a" => "b", "c"=>"d"}) + assert_equal "200", res.code + params = res.body.split(/\n/).grep(/^params:/) + assert_equal 1, params.size + params = eval(params[0].gsub!(/\Aparams:/, '')) + assert_equal Hash, params.class + assert_equal 'b', params['a'] + assert_equal 'd', params['c'] + assert_equal "200 OK", res['Status'] + + # try uploading a big file + tmp = Tempfile.new('random') + sha1 = Digest::SHA1.new + assert_nothing_raised do + File.open("/dev/urandom", "rb") do |fp| + 256.times do + buf = fp.sysread(4096) + sha1.update(buf) + tmp.syswrite(buf) + 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 + + # make sure we can get 403 responses, too + uri = URI.parse("http://#@addr:#@port/foo/xpost") + wait_master_ready("test_stderr.#$$.log") + res = Net::HTTP.get_response(uri) + assert_equal "403", res.code + assert_equal "403 Forbidden", res['Status'] + + # non existent controller + uri = URI.parse("http://#@addr:#@port/asdf") + res = Net::HTTP.get_response(uri) + assert_equal "404", res.code + assert_equal "404 Not Found", res['Status'] + + # static files + + # ensure file we're about to serve is not there yet + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/pid.txt")) + assert_equal "404 Not Found", res['Status'] + assert_equal '404', res.code + + # can we serve text files based on suffix? + File.open("public/pid.txt", "wb") { |fp| fp.syswrite("#$$\n") } + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/pid.txt")) + assert_equal '200', res.code + assert_equal "200 OK", res['Status'] + assert_match %r{^text/plain}, res['Content-Type'] + assert_equal "#$$\n", res.body + + # can we serve HTML files based on suffix? + assert File.exist?("public/500.html") + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/500.html")) + assert_equal '200', res.code + assert_equal '200 OK', res['Status'] + assert_match %r{^text/html}, res['Content-Type'] + five_hundred_body = res.body + + # lets try pretending 500 is a controller that got cached + assert ! File.exist?("public/500") + assert_equal five_hundred_body, File.read("public/500.html") + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/500")) + assert_equal '200', res.code + assert_equal '200 OK', res['Status'] + assert_match %r{^text/html}, res['Content-Type'] + assert_equal five_hundred_body, res.body + end + + def test_alt_url_root + # cbf to actually work on this since I never use this feature (ewong) + return unless ROR_V[0] >= 2 && ROR_V[1] >= 3 + redirect_test_io do + @pid = fork { exec 'unicorn_rails', "-l#@addr:#@port", '-P/poo' } + end + wait_master_ready("test_stderr.#$$.log") + res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/poo/foo")) + # p res + # p res.body + # system 'cat', 'log/development.log' + 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'] + end + + def teardown + return if @start_pid != $$ + + if @pid + Process.kill(:QUIT, @pid) + pid2, status = Process.waitpid2(@pid) + assert status.success? + end + + Dir.chdir(@pwd) + FileUtils.rmtree(@tmpdir) + loop do + Process.kill('-QUIT', 0) + begin + Process.waitpid(-1, Process::WNOHANG) or break + rescue Errno::ECHILD + break + end + end + end + +end if do_test diff --git a/test/test_helper.rb b/test/test_helper.rb index f809af3..55aa70c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,9 +4,16 @@ # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html # for more information. +STDIN.sync = STDOUT.sync = STDERR.sync = true # buffering makes debugging hard + +# Some tests watch a log file or a pid file to spring up to check state +# Can't rely on inotify on non-Linux and logging to a pipe makes things +# more complicated +DEFAULT_TRIES = 1000 +DEFAULT_RES = 0.2 HERE = File.dirname(__FILE__) unless defined?(HERE) -%w(lib ext bin test).each do |dir| +%w(lib ext).each do |dir| $LOAD_PATH.unshift "#{HERE}/../#{dir}" end @@ -15,8 +22,10 @@ require 'net/http' require 'digest/sha1' require 'uri' require 'stringio' +require 'pathname' +require 'tempfile' +require 'fileutils' require 'unicorn' -require 'tmpdir' if ENV['DEBUG'] require 'ruby-debug' @@ -26,8 +35,9 @@ end def redirect_test_io orig_err = STDERR.dup orig_out = STDOUT.dup - STDERR.reopen("test_stderr.#{$$}.log") - STDOUT.reopen("test_stdout.#{$$}.log") + STDERR.reopen("test_stderr.#{$$}.log", "a") + STDOUT.reopen("test_stdout.#{$$}.log", "a") + STDERR.sync = STDOUT.sync = true at_exit do File.unlink("test_stderr.#{$$}.log") rescue nil @@ -41,7 +51,18 @@ def redirect_test_io STDOUT.reopen(orig_out) end end - + +# which(1) exit codes cannot be trusted on some systems +# We use UNIX shell utilities in some tests because we don't trust +# ourselves to write Ruby 100% correctly :) +def which(bin) + ex = ENV['PATH'].split(/:/).detect do |x| + x << "/#{bin}" + File.executable?(x) + end or warn "`#{bin}' not found in PATH=#{ENV['PATH']}" + ex +end + # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.) def hit(uris) @@ -101,3 +122,141 @@ def unused_port(addr = '127.0.0.1') sock.close rescue nil port end + +def try_require(lib) + begin + require lib + true + rescue LoadError + false + end +end + +# sometimes the server may not come up right away +def retry_hit(uris = []) + tries = DEFAULT_TRIES + begin + hit(uris) + rescue Errno::ECONNREFUSED => err + if (tries -= 1) > 0 + sleep DEFAULT_RES + retry + end + raise err + end +end + +def assert_shutdown(pid) + wait_master_ready("test_stderr.#{pid}.log") + assert_nothing_raised { Process.kill(:QUIT, pid) } + status = nil + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success?, "exited successfully" +end + +def wait_workers_ready(path, nr_workers) + tries = DEFAULT_TRIES + lines = [] + while (tries -= 1) > 0 + begin + lines = File.readlines(path).grep(/worker=\d+ ready/) + lines.size == nr_workers and return + rescue Errno::ENOENT + end + sleep DEFAULT_RES + end + raise "#{nr_workers} workers never became ready:" \ + "\n\t#{lines.join("\n\t")}\n" +end + +def wait_master_ready(master_log) + tries = DEFAULT_TRIES + while (tries -= 1) > 0 + begin + File.readlines(master_log).grep(/master process ready/)[0] and return + rescue Errno::ENOENT + end + sleep DEFAULT_RES + end + raise "master process never became ready" +end + +def reexec_usr2_quit_test(pid, pid_file) + assert File.exist?(pid_file), "pid file OK" + assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file" + assert_nothing_raised { Process.kill(:USR2, pid) } + assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } + wait_for_file("#{pid_file}.oldbin") + wait_for_file(pid_file) + + old_pid = File.read("#{pid_file}.oldbin").to_i + new_pid = File.read(pid_file).to_i + + # kill old master process + assert_not_equal pid, new_pid + assert_equal pid, old_pid + assert_nothing_raised { Process.kill(:QUIT, old_pid) } + assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } + wait_for_death(old_pid) + assert_equal new_pid, File.read(pid_file).to_i + assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } + assert_nothing_raised { Process.kill(:QUIT, new_pid) } +end + +def reexec_basic_test(pid, pid_file) + results = retry_hit(["http://#{@addr}:#{@port}/"]) + assert_equal String, results[0].class + assert_nothing_raised { Process.kill(0, pid) } + master_log = "#{@tmpdir}/test_stderr.#{pid}.log" + wait_master_ready(master_log) + File.truncate(master_log, 0) + nr = 50 + kill_point = 2 + assert_nothing_raised do + nr.times do |i| + hit(["http://#{@addr}:#{@port}/#{i}"]) + i == kill_point and Process.kill(:HUP, pid) + end + end + wait_master_ready(master_log) + assert File.exist?(pid_file), "pid=#{pid_file} exists" + new_pid = File.read(pid_file).to_i + assert_not_equal pid, new_pid + assert_nothing_raised { Process.kill(0, new_pid) } + assert_nothing_raised { Process.kill(:QUIT, new_pid) } +end + +def wait_for_file(path) + tries = DEFAULT_TRIES + while (tries -= 1) > 0 && ! File.exist?(path) + sleep DEFAULT_RES + end + assert File.exist?(path), "path=#{path} exists #{caller.inspect}" +end + +def xfork(&block) + fork do + ObjectSpace.each_object(Tempfile) do |tmp| + ObjectSpace.undefine_finalizer(tmp) + end + yield + end +end + +# can't waitpid on detached processes +def wait_for_death(pid) + tries = DEFAULT_TRIES + while (tries -= 1) > 0 + begin + Process.kill(0, pid) + begin + Process.waitpid(pid, Process::WNOHANG) + rescue Errno::ECHILD + end + sleep(DEFAULT_RES) + rescue Errno::ESRCH + return + end + end + raise "PID:#{pid} never died!" +end diff --git a/test/tools/trickletest.rb b/test/tools/trickletest.rb deleted file mode 100644 index e19ed71..0000000 --- a/test/tools/trickletest.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'socket' -require 'stringio' - -def do_test(st, chunk) - s = TCPSocket.new('127.0.0.1',ARGV[0].to_i); - req = StringIO.new(st) - nout = 0 - randstop = rand(st.length / 10) - STDERR.puts "stopping after: #{randstop}" - - begin - while data = req.read(chunk) - nout += s.write(data) - s.flush - sleep 0.1 - if nout > randstop - STDERR.puts "BANG! after #{nout} bytes." - break - end - end - rescue Object => e - STDERR.puts "ERROR: #{e}" - ensure - s.close - end -end - -content = "-" * (1024 * 240) -st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\nContent-Length: #{content.length}\r\n\r\n#{content}" - -puts "length: #{content.length}" - -threads = [] -ARGV[1].to_i.times do - t = Thread.new do - size = 100 - puts ">>>> #{size} sized chunks" - do_test(st, size) - end - - t.abort_on_exception = true - threads << t -end - -threads.each {|t| t.join} diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb index 8de0b13..98f2db6 100644 --- a/test/unit/test_configurator.rb +++ b/test/unit/test_configurator.rb @@ -4,10 +4,34 @@ require 'unicorn/configurator' class TestConfigurator < Test::Unit::TestCase - def test_config_defaults + def test_config_init assert_nothing_raised { Unicorn::Configurator.new {} } end + def test_expand_addr + meth = Unicorn::Configurator.new.method(:expand_addr) + + assert_equal "/var/run/unicorn.sock", meth.call("/var/run/unicorn.sock") + assert_equal "#{Dir.pwd}/foo/bar.sock", meth.call("unix:foo/bar.sock") + + path = meth.call("~/foo/bar.sock") + assert_equal "/", path[0..0] + assert_match %r{/foo/bar\.sock\z}, path + + path = meth.call("~root/foo/bar.sock") + assert_equal "/", path[0..0] + assert_match %r{/foo/bar\.sock\z}, path + + assert_equal "1.2.3.4:2007", meth.call('1.2.3.4:2007') + assert_equal "0.0.0.0:2007", meth.call('0.0.0.0:2007') + 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_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') + end + def test_config_invalid tmp = Tempfile.new('unicorn_config') tmp.syswrite(%q(asdfasdf "hello-world")) @@ -45,4 +69,43 @@ class TestConfigurator < Test::Unit::TestCase assert_nil @logger end + def test_listen_options + tmp = Tempfile.new('unicorn_config') + expect = { :sndbuf => 1, :rcvbuf => 2, :backlog => 10 }.freeze + listener = "127.0.0.1:12345" + tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") + cfg = nil + 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")) + assert_equal expect, listener_opts[listener] + end + + def test_listen_option_bad + tmp = Tempfile.new('unicorn_config') + expect = { :sndbuf => "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_after_fork_proc + [ 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 + end + end + + def test_after_fork_wrong_arity + [ proc { |a| }, Proc.new { }, lambda { |a,b,c| } ].each do |my_proc| + assert_raises(ArgumentError) do + Unicorn::Configurator.new(:after_fork => my_proc) + end + end + end + end diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb index ca1cd01..a158ebb 100644 --- a/test/unit/test_http_parser.rb +++ b/test/unit/test_http_parser.rb @@ -14,46 +14,82 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} http = "GET / HTTP/1.1\r\n\r\n" - nread = parser.execute(req, http, 0) - - assert nread == http.length, "Failed to parse the full HTTP request" - assert parser.finished?, "Parser didn't finish" - assert !parser.error?, "Parser had error" - assert nread == parser.nread, "Number read returned from execute does not match" + assert parser.execute(req, http) assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/', req['REQUEST_PATH'] assert_equal 'HTTP/1.1', req['HTTP_VERSION'] assert_equal '/', req['REQUEST_URI'] - assert_equal 'CGI/1.2', req['GATEWAY_INTERFACE'] - assert_equal 'GET', req['REQUEST_METHOD'] + assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] - assert_nil req['QUERY_STRING'] - + assert_equal '', req['QUERY_STRING'] + parser.reset - assert parser.nread == 0, "Number read after reset should be 0" + req.clear + + assert ! parser.execute(req, "G") + 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_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] + assert_equal '/hello-world', req['REQUEST_PATH'] + assert_equal 'HTTP/1.1', req['HTTP_VERSION'] + assert_equal '/hello-world', req['REQUEST_URI'] + assert_equal 'GET', req['REQUEST_METHOD'] + assert_nil req['FRAGMENT'] + assert_equal '', req['QUERY_STRING'] + 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") + assert_equal 'foo', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + 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") + assert_equal 'foo', req['SERVER_NAME'] + assert_equal '999', req['SERVER_PORT'] + 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") + assert_equal 'foo', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] 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") + assert_equal 'foo', req['SERVER_NAME'] + assert_equal '443', req['SERVER_PORT'] + end + def test_parse_strange_headers parser = HttpParser.new req = {} should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" - nread = parser.execute(req, should_be_good, 0) - assert_equal should_be_good.length, nread - assert parser.finished? - assert !parser.error? + assert parser.execute(req, should_be_good) - # ref: http://thread.gmane.org/gmane.comp.lang.ruby.Unicorn.devel/37/focus=45 + # 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 = {} - # nread = parser.execute(req, nasty_pound_header, 0) - # assert_equal nasty_pound_header.length, nread - # assert parser.finished? - # assert !parser.error? + # assert parser.execute(req, nasty_pound_header, 0) end def test_parse_ie6_urls @@ -67,10 +103,7 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n) - nread = parser.execute(req, sorta_safe, 0) - assert_equal sorta_safe.length, nread - assert parser.finished? - assert !parser.error? + assert parser.execute(req, sorta_safe) end end @@ -79,28 +112,149 @@ class HttpParserTest < Test::Unit::TestCase req = {} bad_http = "GET / SsUTF/1.1" - error = false - begin - nread = parser.execute(req, bad_http, 0) - rescue => details - error = true - end + assert_raises(HttpParserError) { parser.execute(req, bad_http) } + parser.reset + assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n")) + 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_equal '/', req['REQUEST_PATH'] + assert_equal '/', req['REQUEST_URI'] + assert_equal 'GET', req['REQUEST_METHOD'] + assert ! parser.execute(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 req['FRAGMENT'] + assert_equal '', req['QUERY_STRING'] + end + + # not common, but underscores do appear in practice + def test_absolute_uri_underscores + 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 '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 'under_score.example.com', req['HTTP_HOST'] + assert_equal 'under_score.example.com', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + 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 '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'] + end + + # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however... + def test_absolute_uri_https + parser = HttpParser.new + 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 'https', 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 '443', req['SERVER_PORT'] + end + + # Host: header should be ignored for absolute URIs + def test_absolute_uri_with_port + parser = HttpParser.new + 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 '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:8080', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '8080', req['SERVER_PORT'] + end + + def test_absolute_uri_with_empty_port + parser = HttpParser.new + 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 'https', 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 '443', req['SERVER_PORT'] + 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['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] + end - assert error, "failed to throw exception" - assert !parser.finished?, "Parser shouldn't be finished" - assert parser.error?, "Parser SHOULD have error" + 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 '/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] end def test_fragment_in_uri parser = HttpParser.new req = {} get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" + ok = false assert_nothing_raised do - parser.execute(req, get, 0) + ok = parser.execute(req, get) end - assert parser.finished? + 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'] end # lame random garbage maker @@ -125,7 +279,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, 0) + parser.execute({}, get) parser.reset end end @@ -134,7 +288,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, 0) + parser.execute({}, get) parser.reset end end @@ -143,7 +297,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, 0) + parser.execute({}, get) parser.reset end @@ -151,7 +305,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, 0) + parser.execute({}, get) parser.reset end end diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb new file mode 100644 index 0000000..0bfff7d --- /dev/null +++ b/test/unit/test_request.rb @@ -0,0 +1,159 @@ +# 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 + +class RequestTest < Test::Unit::TestCase + + class MockRequest < StringIO + alias_method :readpartial, :sysread + end + + def setup + @request = HttpRequest.new(Logger.new($stderr)) + @app = lambda do |env| + [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ] + end + @lint = Rack::Lint.new(@app) + end + + def test_options + client = MockRequest.new("OPTIONS * HTTP/1.1\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal '', env['REQUEST_PATH'] + assert_equal '', env['PATH_INFO'] + assert_equal '*', env['REQUEST_URI'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_absolute_uri_with_query + client = MockRequest.new("GET http://e:3/x?y=z HTTP/1.1\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal '/x', env['REQUEST_PATH'] + assert_equal '/x', env['PATH_INFO'] + assert_equal 'y=z', env['QUERY_STRING'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_absolute_uri_with_fragment + client = MockRequest.new("GET http://e:3/x#frag HTTP/1.1\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal '/x', env['REQUEST_PATH'] + assert_equal '/x', env['PATH_INFO'] + assert_equal '', env['QUERY_STRING'] + assert_equal 'frag', env['FRAGMENT'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_absolute_uri_with_query_and_fragment + client = MockRequest.new("GET http://e:3/x?a=b#frag HTTP/1.1\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal '/x', env['REQUEST_PATH'] + assert_equal '/x', env['PATH_INFO'] + assert_equal 'a=b', env['QUERY_STRING'] + assert_equal 'frag', env['FRAGMENT'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_absolute_uri_unsupported_schemes + %w(ssh+http://e/ ftp://e/x http+ssh://e/x).each do |abs_uri| + client = MockRequest.new("GET #{abs_uri} HTTP/1.1\r\n" \ + "Host: foo\r\n\r\n") + assert_raises(HttpParserError) { @request.read(client) } + end + end + + def test_x_forwarded_proto_https + res = env = nil + client = MockRequest.new("GET / HTTP/1.1\r\n" \ + "X-Forwarded-Proto: https\r\n" \ + "Host: foo\r\n\r\n") + assert_nothing_raised { env = @request.read(client) } + assert_equal "https", env['rack.url_scheme'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_x_forwarded_proto_http + res = env = nil + client = MockRequest.new("GET / HTTP/1.1\r\n" \ + "X-Forwarded-Proto: http\r\n" \ + "Host: foo\r\n\r\n") + assert_nothing_raised { env = @request.read(client) } + assert_equal "http", env['rack.url_scheme'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_x_forwarded_proto_invalid + res = env = nil + client = MockRequest.new("GET / HTTP/1.1\r\n" \ + "X-Forwarded-Proto: ftp\r\n" \ + "Host: foo\r\n\r\n") + assert_nothing_raised { env = @request.read(client) } + assert_equal "http", env['rack.url_scheme'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_rack_lint_get + 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 "http", env['rack.url_scheme'] + assert_equal '127.0.0.1', env['REMOTE_ADDR'] + assert_nothing_raised { res = @lint.call(env) } + end + + def test_rack_lint_put + client = MockRequest.new( + "PUT / HTTP/1.1\r\n" \ + "Host: foo\r\n" \ + "Content-Length: 5\r\n" \ + "\r\n" \ + "abcde") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert ! env.include?(:http_body) + assert_nothing_raised { res = @lint.call(env) } + end + + def test_rack_lint_big_put + count = 100 + bs = 0x10000 + buf = (' ' * bs).freeze + length = bs * count + client = Tempfile.new('big_put') + client.syswrite( + "PUT / HTTP/1.1\r\n" \ + "Host: foo\r\n" \ + "Content-Length: #{length}\r\n" \ + "\r\n") + count.times { assert_equal bs, client.syswrite(buf) } + assert_equal 0, client.sysseek(0) + res = env = nil + 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) } + assert_nil env['rack.input'].read(bs) + assert_nothing_raised { env['rack.input'].rewind } + assert_nothing_raised { res = @lint.call(env) } + end + +end + diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb index c30a141..66c2b54 100644 --- a/test/unit/test_response.rb +++ b/test/unit/test_response.rb @@ -13,16 +13,26 @@ class ResponseTest < Test::Unit::TestCase def test_response_headers out = StringIO.new HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]]) + assert out.closed? assert out.length > 0, "output didn't have data" end + def test_response_string_status + out = StringIO.new + HttpResponse.write(out,['200', {}, []]) + assert out.closed? + assert out.length > 0, "output didn't have data" + assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/).size + end + def test_response_OFS_set old_ofs = $, $, = "\f\v" out = StringIO.new - HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]]) - resp = out.read + HttpResponse.write(out,[200, {"X-k" => "cd","X-y" => "z"}, ["cool"]]) + assert out.closed? + resp = out.string assert ! resp.include?("\f\v"), "output didn't use $, ($OFS)" ensure $, = old_ofs @@ -31,6 +41,7 @@ class ResponseTest < Test::Unit::TestCase def test_response_200 io = StringIO.new HttpResponse.write(io, [200, {}, []]) + assert io.closed? assert io.length > 0, "output didn't have data" end @@ -38,8 +49,49 @@ class ResponseTest < Test::Unit::TestCase code = 400 io = StringIO.new HttpResponse.write(io, [code, {}, []]) - io.rewind - assert_match(/.* #{HTTP_STATUS_CODES[code]}$/, io.readline.chomp, "wrong default reason phrase") + assert io.closed? + lines = io.string.split(/\r\n/) + assert_match(/.* Bad Request$/, lines.first, + "wrong default reason phrase") end -end + def test_rack_multivalue_headers + out = StringIO.new + HttpResponse.write(out,[200, {"X-Whatever" => "stuff\nbleh"}, []]) + assert out.closed? + assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string) + end + + # Even though Rack explicitly forbids "Status" in the header hash, + # some broken clients still rely on it + def test_status_header_added + out = StringIO.new + HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, []]) + assert out.closed? + assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/i).size + end + + # we always favor the code returned by the application, since "Status" + # in the header hash is not allowed by Rack (but not every app is + # fully Rack-compliant). + def test_status_header_ignores_app_hash + out = StringIO.new + header_hash = {"X-Whatever" => "stuff", 'StaTus' => "666" } + HttpResponse.write(out,[200, header_hash, []]) + assert out.closed? + assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/i).size + assert_equal 1, out.string.split(/\r\n/).grep(/^Status:/i).size + end + + def test_body_closed + expect_body = %w(1 2 3 4).join("\n") + body = StringIO.new(expect_body) + body.rewind + out = StringIO.new + HttpResponse.write(out,[200, {}, body]) + assert out.closed? + assert body.closed? + assert_match(expect_body, out.string.split(/\r\n/).last) + end + +end diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb index d19064c..742b240 100644 --- a/test/unit/test_server.rb +++ b/test/unit/test_server.rb @@ -25,8 +25,8 @@ class WebServerTest < Test::Unit::TestCase @tester = TestHandler.new redirect_test_io do @server = HttpServer.new(@tester, :listeners => [ "127.0.0.1:#{@port}" ] ) + @server.start end - @server.start end def teardown @@ -35,6 +35,60 @@ class WebServerTest < Test::Unit::TestCase end end + def test_preload_app_config + teardown + tmp = Tempfile.new('test_preload_app_config') + ObjectSpace.undefine_finalizer(tmp) + app = lambda { || + tmp.sysseek(0) + tmp.truncate(0) + tmp.syswrite($$) + lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "#$$\n" ] ] } + } + redirect_test_io do + @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] ) + @server.start + end + results = hit(["http://localhost:#@port/"]) + worker_pid = results[0].to_i + tmp.sysseek(0) + loader_pid = tmp.sysread(4096).to_i + assert_equal worker_pid, loader_pid + teardown + + redirect_test_io do + @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"], + :preload_app => true) + @server.start + end + results = hit(["http://localhost:#@port/"]) + worker_pid = results[0].to_i + tmp.sysseek(0) + loader_pid = tmp.sysread(4096).to_i + assert_equal $$, loader_pid + assert worker_pid != loader_pid + ensure + tmp.close! + end + + def test_broken_app + teardown + app = lambda { |env| raise RuntimeError, "hello" } + # [200, {}, []] } + redirect_test_io do + @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] ) + @server.start + end + sock = nil + assert_nothing_raised do + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + end + + assert_match %r{\AHTTP/1.[01] 500\b}, sock.sysread(4096) + assert_nothing_raised { sock.close } + end + def test_simple_server results = hit(["http://localhost:#{@port}/test"]) assert_equal 'hello!\n', results[0], "Handler didn't really run" @@ -77,6 +131,16 @@ class WebServerTest < Test::Unit::TestCase end end + def test_bad_client_400 + sock = nil + assert_nothing_raised do + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\nHost: foo\rbar\r\n\r\n") + end + assert_match %r{\AHTTP/1.[01] 400\b}, 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" diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb new file mode 100644 index 0000000..ef66ed6 --- /dev/null +++ b/test/unit/test_signals.rb @@ -0,0 +1,191 @@ +# Copyright (c) 2009 Eric Wong +# You can redistribute it and/or modify it under the same terms as Ruby. +# +# Ensure we stay sane in the face of signals being sent to us + +require 'test/test_helper' + +include Unicorn + +class Dd + def initialize(bs, count) + @count = count + @buf = ' ' * bs + end + + def each(&block) + @count.times { yield @buf } + end +end + +class SignalsTest < Test::Unit::TestCase + + def setup + @bs = 1 * 1024 * 1024 + @count = 100 + @port = unused_port + tmp = @tmp = Tempfile.new('unicorn.sock') + File.unlink(@tmp.path) + n = 0 + tmp.chmod(0) + @server_opts = { + :listeners => [ "127.0.0.1:#@port", @tmp.path ], + :after_fork => lambda { |server,worker| + trap(:HUP) { tmp.chmod(n += 1) } + }, + } + @server = nil + end + + def test_worker_dies_on_dead_master + pid = fork { + app = lambda { |env| [ 200, {'X-Pid' => "#$$" }, [] ] } + opts = @server_opts.merge(:timeout => 3) + redirect_test_io { HttpServer.new(app, opts).start.join } + } + child = sock = buf = t0 = nil + assert_nothing_raised do + wait_workers_ready("test_stderr.#{pid}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + buf = sock.readpartial(4096) + sock.close + buf =~ /\bX-Pid: (\d+)\b/ or raise Exception + child = $1.to_i + wait_master_ready("test_stderr.#{pid}.log") + Process.kill(:KILL, pid) + Process.waitpid(pid) + t0 = Time.now + end + assert child + assert t0 + assert_raises(Errno::ESRCH) { loop { Process.kill(0, child); sleep 0.2 } } + assert((Time.now - t0) < 60) + end + + def test_sleepy_kill + rd, wr = IO.pipe + pid = fork { + rd.close + app = lambda { |env| wr.syswrite('.'); sleep; [ 200, {}, [] ] } + redirect_test_io { HttpServer.new(app, @server_opts).start.join } + } + sock = buf = nil + wr.close + assert_nothing_raised do + wait_workers_ready("test_stderr.#{pid}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + buf = rd.readpartial(1) + wait_master_ready("test_stderr.#{pid}.log") + Process.kill(:INT, pid) + Process.waitpid(pid) + end + assert_equal '.', buf + buf = nil + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) do + buf = sock.sysread(4096) + end + assert_nil buf + ensure + end + + def test_timeout_slow_response + pid = fork { + app = lambda { |env| sleep } + opts = @server_opts.merge(:timeout => 3) + redirect_test_io { HttpServer.new(app, opts).start.join } + } + t0 = Time.now + sock = nil + assert_nothing_raised do + wait_workers_ready("test_stderr.#{pid}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + end + + buf = nil + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) do + buf = sock.sysread(4096) + end + diff = Time.now - t0 + assert_nil buf + assert diff > 1.0, "diff was #{diff.inspect}" + assert diff < 60.0 + ensure + Process.kill(:QUIT, pid) rescue nil + end + + def test_response_write + app = lambda { |env| + [ 200, { 'Content-Type' => 'text/plain', 'X-Pid' => Process.pid.to_s }, + Dd.new(@bs, @count) ] + } + redirect_test_io { @server = HttpServer.new(app, @server_opts).start } + sock = nil + assert_nothing_raised do + wait_workers_ready("test_stderr.#{$$}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + end + buf = '' + header_len = pid = nil + assert_nothing_raised do + buf = sock.sysread(16384, buf) + pid = buf[/\r\nX-Pid: (\d+)\r\n/, 1].to_i + header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size + end + read = buf.size + mode_before = @tmp.stat.mode + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) do + loop do + 3.times { Process.kill(:HUP, pid) } + sock.sysread(16384, buf) + read += buf.size + 3.times { Process.kill(:HUP, pid) } + end + end + + 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_nothing_raised { sock.close } + end + + def test_request_read + app = lambda { |env| + [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ] + } + redirect_test_io { @server = HttpServer.new(app, @server_opts).start } + pid = nil + + assert_nothing_raised do + wait_workers_ready("test_stderr.#{$$}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + pid = sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i + sock.close + end + + 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 + killer = fork { loop { Process.kill(:HUP, pid); sleep(0.0001) } } + buf = ' ' * @bs + @count.times { sock.syswrite(buf) } + Process.kill(:TERM, killer) + 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_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i + sock.close + end + +end diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb new file mode 100644 index 0000000..75d9f7b --- /dev/null +++ b/test/unit/test_socket_helper.rb @@ -0,0 +1,131 @@ +require 'test/test_helper' +require 'tempfile' + +class TestSocketHelper < Test::Unit::TestCase + include Unicorn::SocketHelper + attr_reader :logger + GET_SLASH = "GET / HTTP/1.0\r\n\r\n".freeze + + def setup + @log_tmp = Tempfile.new 'logger' + @logger = Logger.new(@log_tmp.path) + @test_addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' + GC.disable + end + + def teardown + GC.enable + end + + def test_bind_listen_tcp + port = unused_port @test_addr + @tcp_listener_name = "#@test_addr:#{port}" + @tcp_listener = bind_listen(@tcp_listener_name) + assert TCPServer === @tcp_listener + assert_equal @tcp_listener_name, sock_name(@tcp_listener) + end + + def test_bind_listen_options + port = unused_port @test_addr + tcp_listener_name = "#@test_addr:#{port}" + tmp = Tempfile.new 'unix.sock' + unix_listener_name = tmp.path + File.unlink(tmp.path) + [ { :backlog => 5 }, { :sndbuf => 4096 }, { :rcvbuf => 4096 }, + { :backlog => 16, :rcvbuf => 4096, :sndbuf => 4096 } + ].each do |opts| + assert_nothing_raised do + tcp_listener = bind_listen(tcp_listener_name, opts) + assert TCPServer === tcp_listener + tcp_listener.close + unix_listener = bind_listen(unix_listener_name, opts) + assert UNIXServer === unix_listener + unix_listener.close + end + end + #system('cat', @log_tmp.path) + end + + def test_bind_listen_unix + 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) + assert UNIXServer === @unix_listener + assert_equal @unix_listener_path, sock_name(@unix_listener) + assert File.readable?(@unix_listener_path), "not readable" + assert File.writable?(@unix_listener_path), "not writable" + assert_equal 0777, File.umask + ensure + File.umask(old_umask) + end + + def test_bind_listen_unix_idempotent + test_bind_listen_unix + a = bind_listen(@unix_listener) + assert_equal a.fileno, @unix_listener.fileno + unix_server = server_cast(@unix_listener) + assert UNIXServer === unix_server + a = bind_listen(unix_server) + assert_equal a.fileno, unix_server.fileno + assert_equal a.fileno, @unix_listener.fileno + end + + def test_bind_listen_tcp_idempotent + test_bind_listen_tcp + a = bind_listen(@tcp_listener) + assert_equal a.fileno, @tcp_listener.fileno + tcp_server = server_cast(@tcp_listener) + assert TCPServer === tcp_server + a = bind_listen(tcp_server) + assert_equal a.fileno, tcp_server.fileno + assert_equal a.fileno, @tcp_listener.fileno + end + + def test_bind_listen_unix_rebind + test_bind_listen_unix + new_listener = bind_listen(@unix_listener_path) + assert UNIXServer === new_listener + assert new_listener.fileno != @unix_listener.fileno + assert_equal sock_name(new_listener), sock_name(@unix_listener) + assert_equal @unix_listener_path, sock_name(new_listener) + pid = fork do + client = server_cast(new_listener).accept + client.syswrite('abcde') + exit 0 + end + s = UNIXSocket.new(@unix_listener_path) + IO.select([s]) + assert_equal 'abcde', s.sysread(5) + pid, status = Process.waitpid2(pid) + assert status.success? + end + + def test_server_cast + assert_nothing_raised do + test_bind_listen_unix + test_bind_listen_tcp + end + unix_listener_socket = Socket.for_fd(@unix_listener.fileno) + assert Socket === unix_listener_socket + @unix_server = server_cast(unix_listener_socket) + assert_equal @unix_listener.fileno, @unix_server.fileno + assert UNIXServer === @unix_server + assert File.socket?(@unix_server.path) + assert_equal @unix_listener_path, sock_name(@unix_server) + + tcp_listener_socket = Socket.for_fd(@tcp_listener.fileno) + assert Socket === tcp_listener_socket + @tcp_server = server_cast(tcp_listener_socket) + assert_equal @tcp_listener.fileno, @tcp_server.fileno + assert TCPServer === @tcp_server + assert_equal @tcp_listener_name, sock_name(@tcp_server) + end + + def test_sock_name + test_server_cast + sock_name(@unix_server) + end + +end diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb index edc94da..9ef3ed7 100644 --- a/test/unit/test_upload.rb +++ b/test/unit/test_upload.rb @@ -18,12 +18,29 @@ class UploadTest < Test::Unit::TestCase @sha1 = Digest::SHA1.new @sha1_app = lambda do |env| input = env['rack.input'] - resp = { :pos => input.pos, :size => input.stat.size } + resp = { :pos => input.pos, :size => input.size, :class => input.class } + + # sysread + @sha1.reset begin loop { @sha1.update(input.sysread(@bs)) } rescue EOFError end resp[:sha1] = @sha1.hexdigest + + # read + input.sysseek(0) if input.respond_to?(:sysseek) + input.rewind + @sha1.reset + loop { + buf = input.read(@bs) or break + @sha1.update(buf) + } + + if resp[:sha1] == @sha1.hexdigest + resp[:sysread_read_byte_match] = true + end + [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ] end end @@ -50,6 +67,61 @@ class UploadTest < Test::Unit::TestCase assert_equal @sha1.hexdigest, resp[:sha1] end + def test_put_trickle_small + @count, @bs = 2, 128 + start_server(@sha1_app) + assert_equal 256, length + sock = TCPSocket.new(@addr, @port) + hdr = "PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n" + @count.times do + buf = @random.sysread(@bs) + @sha1.update(buf) + hdr << buf + sock.syswrite(hdr) + hdr = '' + sleep 0.6 + end + read = sock.read.split(/\r\n/) + assert_equal "HTTP/1.1 200 OK", read[0] + resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) + assert_equal length, resp[:size] + assert_equal 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 start_server(@sha1_app) @@ -135,6 +207,52 @@ class UploadTest < Test::Unit::TestCase assert_equal resp[:size], new_tmp.stat.size end + # Despite reading numerous articles and inspecting the 1.9.1-p0 C + # source, Eric Wong will never trust that we're always handling + # encoding-aware IO objects correctly. Thus this test uses shell + # utilities that should always operate on files/sockets on a + # byte-level. + def test_uncomfortable_with_onenine_encodings + # POSIX doesn't require all of these to be present on a system + which('curl') or return + which('sha1sum') or return + which('dd') or return + + start_server(@sha1_app) + + tmp = Tempfile.new('dd_dest') + assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", + "bs=#{@bs}", "count=#{@count}"), + "dd #@random to #{tmp}") + sha1_re = %r!\b([a-f0-9]{40})\b! + sha1_out = `sha1sum #{tmp.path}` + assert $?.success?, 'sha1sum ran OK' + + assert_match(sha1_re, sha1_out) + sha1 = sha1_re.match(sha1_out)[1] + resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` + assert $?.success?, 'curl ran OK' + assert_match(%r!\b#{sha1}\b!, resp) + assert_match(/Tempfile/, resp) + assert_match(/sysread_read_byte_match/, resp) + + # small StringIO path + assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", + "bs=1024", "count=1"), + "dd #@random to #{tmp}") + sha1_re = %r!\b([a-f0-9]{40})\b! + sha1_out = `sha1sum #{tmp.path}` + assert $?.success?, 'sha1sum ran OK' + + assert_match(sha1_re, sha1_out) + sha1 = sha1_re.match(sha1_out)[1] + resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` + assert $?.success?, 'curl ran OK' + assert_match(%r!\b#{sha1}\b!, resp) + assert_match(/StringIO/, resp) + assert_match(/sysread_read_byte_match/, resp) + end + private def length diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb new file mode 100644 index 0000000..1616eac --- /dev/null +++ b/test/unit/test_util.rb @@ -0,0 +1,87 @@ +require 'test/test_helper' +require 'tempfile' + +class TestUtil < Test::Unit::TestCase + + EXPECT_FLAGS = File::WRONLY | File::APPEND + def test_reopen_logs_noop + tmp = Tempfile.new(nil) + tmp.reopen(tmp.path, 'a') + tmp.sync = true + ext = tmp.external_encoding rescue nil + int = tmp.internal_encoding rescue nil + before = tmp.stat.inspect + Unicorn::Util.reopen_logs + assert_equal before, File.stat(tmp.path).inspect + assert_equal ext, (tmp.external_encoding rescue nil) + assert_equal int, (tmp.internal_encoding rescue nil) + end + + def test_reopen_logs_renamed + tmp = Tempfile.new(nil) + tmp_path = tmp.path.freeze + tmp.reopen(tmp_path, 'a') + tmp.sync = true + ext = tmp.external_encoding rescue nil + int = tmp.internal_encoding rescue nil + before = tmp.stat.inspect + to = Tempfile.new(nil) + File.rename(tmp_path, to.path) + assert ! File.exist?(tmp_path) + Unicorn::Util.reopen_logs + assert_equal tmp_path, tmp.path + assert File.exist?(tmp_path) + assert before != File.stat(tmp_path).inspect + assert_equal tmp.stat.inspect, File.stat(tmp_path).inspect + assert_equal ext, (tmp.external_encoding rescue nil) + assert_equal int, (tmp.internal_encoding rescue nil) + assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL)) + assert tmp.sync + end + + def test_reopen_logs_renamed_with_encoding + tmp = Tempfile.new(nil) + tmp_path = tmp.path.dup.freeze + Encoding.list.each { |encoding| + tmp.reopen(tmp_path, "a:#{encoding.to_s}") + tmp.sync = true + assert_equal encoding, tmp.external_encoding + assert_nil tmp.internal_encoding + File.unlink(tmp_path) + assert ! File.exist?(tmp_path) + Unicorn::Util.reopen_logs + assert_equal tmp_path, tmp.path + assert File.exist?(tmp_path) + assert_equal tmp.stat.inspect, File.stat(tmp_path).inspect + assert_equal encoding, tmp.external_encoding + assert_nil tmp.internal_encoding + assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL)) + assert tmp.sync + } + end if STDIN.respond_to?(:external_encoding) + + def test_reopen_logs_renamed_with_internal_encoding + tmp = Tempfile.new(nil) + tmp_path = tmp.path.dup.freeze + Encoding.list.each { |ext| + Encoding.list.each { |int| + next if ext == int + tmp.reopen(tmp_path, "a:#{ext.to_s}:#{int.to_s}") + tmp.sync = true + assert_equal ext, tmp.external_encoding + assert_equal int, tmp.internal_encoding + File.unlink(tmp_path) + assert ! File.exist?(tmp_path) + Unicorn::Util.reopen_logs + assert_equal tmp_path, tmp.path + assert File.exist?(tmp_path) + assert_equal tmp.stat.inspect, File.stat(tmp_path).inspect + assert_equal ext, tmp.external_encoding + assert_equal int, tmp.internal_encoding + assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL)) + assert tmp.sync + } + } + end if STDIN.respond_to?(:external_encoding) + +end |