1 # -*- encoding: binary -*-
3 # Copyright (c) 2009 Eric Wong
4 FLOCK_PATH = File.expand_path(__FILE__)
5 require './test/test_helper'
8 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
10 do_test = system($unicorn_bin, '-v')
14 warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
18 unless try_require('rack')
19 warn "Unable to load Rack, skipping this test"
23 class ExecTest < Test::Unit::TestCase
27 use Rack::ContentLength
28 run proc { |env| [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ] }
31 SHOW_RACK_ENV = <<-EOS
32 use Rack::ContentLength
34 [ 200, { 'content-type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ]
41 [ 200, { 'content-type' => 'text/plain' }, [ "HI\\n" ] ]
46 COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
50 worker_processes #{HEAVY_WORKERS}
52 logger Logger.new('#{COMMON_TMP.path}')
53 before_fork do |server, worker|
54 server.logger.info "before_fork: worker=\#{worker.nr}"
58 WORKING_DIRECTORY_CHECK_RU = <<-EOS
59 use Rack::ContentLength
63 b = ::File.stat(Dir.pwd)
64 if (a.ino == b.ino && a.dev == b.dev)
65 [ 200, { 'content-type' => 'text/plain' }, [ pwd ] ]
67 [ 404, { 'content-type' => 'text/plain' }, [] ]
74 @tmpfile = Tempfile.new('unicorn_exec_test')
75 @tmpdir = @tmpfile.path
79 @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
80 @port = unused_port(@addr)
86 return if @start_pid != $$
88 FileUtils.rmtree(@tmpdir)
89 @sockets.each { |path| File.unlink(path) rescue nil }
91 Process.kill('-QUIT', 0)
93 Process.waitpid(-1, Process::WNOHANG) or break
100 def test_inherit_listener_unspecified
101 File.open("config.ru", "wb") { |fp| fp.write(HI) }
102 sock = TCPServer.new(@addr, @port)
103 sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0)
107 ENV['UNICORN_FD'] = sock.fileno.to_s
108 exec($unicorn_bin, sock.fileno => sock.fileno)
111 res = hit(["http://#@addr:#@port/"])
112 assert_equal [ "HI\n" ], res
114 assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool,
115 'unicorn should always set SO_KEEPALIVE on inherited sockets'
120 def test_working_directory_rel_path_config_file
121 other = Tempfile.new('unicorn.wd')
122 File.unlink(other.path)
123 Dir.mkdir(other.path)
124 File.open("config.ru", "wb") do |fp|
125 fp.syswrite WORKING_DIRECTORY_CHECK_RU
127 FileUtils.cp("config.ru", other.path + "/config.ru")
130 tmp = File.open('unicorn.config', 'wb')
132 working_directory '#@tmpdir'
133 listen '#@addr:#@port'
135 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
136 wait_workers_ready("test_stderr.#{pid}.log", 1)
137 results = hit(["http://#@addr:#@port/"])
138 assert_equal @tmpdir, results.first
139 File.truncate("test_stderr.#{pid}.log", 0)
144 working_directory '#{other.path}'
145 listen '#@addr:#@port'
148 Process.kill(:HUP, pid)
150 re = /config_file=(.+) would not be accessible in working_directory=(.+)/
153 lines = File.readlines("test_stderr.#{pid}.log")
156 File.truncate("test_stderr.#{pid}.log", 0)
157 FileUtils.cp('unicorn.config', other.path + "/unicorn.config")
158 Process.kill(:HUP, pid)
159 wait_workers_ready("test_stderr.#{pid}.log", 1)
160 results = hit(["http://#@addr:#@port/"])
161 assert_equal other.path, results.first
163 Process.kill(:QUIT, pid)
165 FileUtils.rmtree(other.path)
168 def test_working_directory
169 other = Tempfile.new('unicorn.wd')
170 File.unlink(other.path)
171 Dir.mkdir(other.path)
172 File.open("config.ru", "wb") do |fp|
173 fp.syswrite WORKING_DIRECTORY_CHECK_RU
175 FileUtils.cp("config.ru", other.path + "/config.ru")
176 tmp = Tempfile.new('unicorn.config')
178 working_directory '#@tmpdir'
179 listen '#@addr:#@port'
181 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
182 wait_workers_ready("test_stderr.#{pid}.log", 1)
183 results = hit(["http://#@addr:#@port/"])
184 assert_equal @tmpdir, results.first
185 File.truncate("test_stderr.#{pid}.log", 0)
190 working_directory '#{other.path}'
191 listen '#@addr:#@port'
194 Process.kill(:HUP, pid)
195 wait_workers_ready("test_stderr.#{pid}.log", 1)
196 results = hit(["http://#@addr:#@port/"])
197 assert_equal other.path, results.first
199 Process.kill(:QUIT, pid)
201 FileUtils.rmtree(other.path)
204 def test_working_directory_controls_relative_paths
205 other = Tempfile.new('unicorn.wd')
206 File.unlink(other.path)
207 Dir.mkdir(other.path)
208 File.open("config.ru", "wb") do |fp|
209 fp.syswrite WORKING_DIRECTORY_CHECK_RU
211 FileUtils.cp("config.ru", other.path + "/config.ru")
212 system('mkfifo', "#{other.path}/fifo")
213 tmp = Tempfile.new('unicorn.config')
216 stderr_path "stderr_log_here"
217 stdout_path "stdout_log_here"
218 working_directory '#{other.path}'
219 listen '#@addr:#@port'
220 after_fork do |server, worker|
221 File.open("fifo", "wb").close
224 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
226 fifo = File.open("#{other.path}/fifo", "rb")
228 # OpenBSD raises Errno::EINTR when opening
229 return if RUBY_PLATFORM =~ /openbsd/
233 assert ! File.exist?("stderr_log_here")
234 assert ! File.exist?("stdout_log_here")
235 assert ! File.exist?("pid_file_here")
237 assert ! File.exist?("#@tmpdir/stderr_log_here")
238 assert ! File.exist?("#@tmpdir/stdout_log_here")
239 assert ! File.exist?("#@tmpdir/pid_file_here")
241 assert File.exist?("#{other.path}/pid_file_here")
242 assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here")
243 assert File.exist?("#{other.path}/stderr_log_here")
244 assert File.exist?("#{other.path}/stdout_log_here")
245 wait_master_ready("#{other.path}/stderr_log_here")
247 Process.kill(:QUIT, pid)
249 FileUtils.rmtree(other.path)
252 def test_exit_signals
253 %w(INT TERM QUIT).each do |sig|
254 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
255 pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
256 wait_master_ready("test_stderr.#{pid}.log")
257 wait_workers_ready("test_stderr.#{pid}.log", 1)
259 Process.kill(sig, pid)
260 pid, status = Process.waitpid2(pid)
262 reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
263 assert_equal 1, reaped.size
264 assert status.exited?
269 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
271 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
273 results = retry_hit(["http://#{@addr}:#{@port}/"])
274 assert_equal String, results[0].class
278 def test_rack_env_unset
279 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
280 pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
281 results = retry_hit(["http://#{@addr}:#{@port}/"])
282 assert_equal "development", results.first
286 def test_rack_env_cli_set
287 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
289 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
291 results = retry_hit(["http://#{@addr}:#{@port}/"])
292 assert_equal "asdf", results.first
296 def test_rack_env_ENV_set
297 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
299 ENV["RACK_ENV"] = "foobar"
300 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
302 results = retry_hit(["http://#{@addr}:#{@port}/"])
303 assert_equal "foobar", results.first
307 def test_rack_env_cli_override_ENV
308 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
310 ENV["RACK_ENV"] = "foobar"
311 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
313 results = retry_hit(["http://#{@addr}:#{@port}/"])
314 assert_equal "asdf", results.first
319 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
320 pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
321 log = "test_stderr.#{pid}.log"
322 wait_master_ready(log)
324 Process.kill(:TTIN, pid)
325 wait_workers_ready(log, i)
327 File.truncate(log, 0)
329 [ 2, 1, 0].each { |i|
330 Process.kill(:TTOU, pid)
331 DEFAULT_TRIES.times {
333 reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
334 break if reaped.size == 1
336 assert_equal 1, reaped.size
342 assert(system($unicorn_bin, "-h"), "help text returns true")
344 assert_equal 0, File.stat("test_stderr.#$$.log").size
345 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
346 lines = File.readlines("test_stdout.#$$.log")
348 # Be considerate of the on-call technician working from their
349 # mobile phone or netbook on a slow connection :)
350 assert lines.size <= 24, "help height fits in an ANSI terminal window"
353 assert line.size <= 80, "help width fits in an ANSI terminal window"
357 def test_broken_reexec_config
358 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
359 pid_file = "#{@tmpdir}/test.pid"
360 old_file = "#{pid_file}.oldbin"
361 ucfg = Tempfile.new('unicorn_test_config')
362 ucfg.syswrite("listen %(#@addr:#@port)\n")
363 ucfg.syswrite("pid %(#{pid_file})\n")
364 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
367 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
370 results = retry_hit(["http://#{@addr}:#{@port}/"])
371 assert_equal String, results[0].class
373 wait_for_file(pid_file)
375 Process.kill(:USR2, File.read(pid_file).to_i)
376 wait_for_file(old_file)
377 wait_for_file(pid_file)
378 old_pid = File.read(old_file).to_i
379 Process.kill(:QUIT, old_pid)
380 wait_for_death(old_pid)
382 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
383 current_pid = File.read(pid_file).to_i
384 Process.kill(:USR2, current_pid)
386 # wait for pid_file to restore itself
387 tries = DEFAULT_TRIES
389 while current_pid != File.read(pid_file).to_i
390 sleep(DEFAULT_RES) and (tries -= 1) > 0
393 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
395 assert_equal current_pid, File.read(pid_file).to_i
397 tries = DEFAULT_TRIES
398 while File.exist?(old_file)
399 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
401 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
402 port2 = unused_port(@addr)
407 ucfg.syswrite("listen %(#@addr:#@port)\n")
408 ucfg.syswrite("listen %(#@addr:#{port2})\n")
409 ucfg.syswrite("pid %(#{pid_file})\n")
410 Process.kill(:USR2, current_pid)
412 wait_for_file(old_file)
413 wait_for_file(pid_file)
414 new_pid = File.read(pid_file).to_i
415 assert_not_equal current_pid, new_pid
416 assert_equal current_pid, File.read(old_file).to_i
417 results = retry_hit(["http://#{@addr}:#{@port}/",
418 "http://#{@addr}:#{port2}/"])
419 assert_equal String, results[0].class
420 assert_equal String, results[1].class
422 Process.kill(:QUIT, current_pid)
423 Process.kill(:QUIT, new_pid)
426 def test_broken_reexec_ru
427 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
428 pid_file = "#{@tmpdir}/test.pid"
429 old_file = "#{pid_file}.oldbin"
430 ucfg = Tempfile.new('unicorn_test_config')
431 ucfg.syswrite("pid %(#{pid_file})\n")
432 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
435 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
438 results = retry_hit(["http://#{@addr}:#{@port}/"])
439 assert_equal String, results[0].class
441 wait_for_file(pid_file)
443 Process.kill(:USR2, File.read(pid_file).to_i)
444 wait_for_file(old_file)
445 wait_for_file(pid_file)
446 old_pid = File.read(old_file).to_i
447 Process.kill(:QUIT, old_pid)
448 wait_for_death(old_pid)
450 File.unlink("config.ru") # break reloading
451 current_pid = File.read(pid_file).to_i
452 Process.kill(:USR2, current_pid)
454 # wait for pid_file to restore itself
455 tries = DEFAULT_TRIES
457 while current_pid != File.read(pid_file).to_i
458 sleep(DEFAULT_RES) and (tries -= 1) > 0
461 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
464 tries = DEFAULT_TRIES
465 while File.exist?(old_file)
466 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
468 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
469 assert_equal current_pid, File.read(pid_file).to_i
472 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
473 Process.kill(:USR2, current_pid)
474 wait_for_file(old_file)
475 wait_for_file(pid_file)
476 new_pid = File.read(pid_file).to_i
477 assert_not_equal current_pid, new_pid
478 assert_equal current_pid, File.read(old_file).to_i
479 results = retry_hit(["http://#{@addr}:#{@port}/"])
480 assert_equal String, results[0].class
482 Process.kill(:QUIT, current_pid)
483 Process.kill(:QUIT, new_pid)
486 def test_unicorn_config_listener_swap
487 port_cli = unused_port
488 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
489 ucfg = Tempfile.new('unicorn_test_config')
490 ucfg.syswrite("listen '#@addr:#@port'\n")
493 exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
496 results = retry_hit(["http://#@addr:#{port_cli}/"])
497 assert_equal String, results[0].class
498 results = retry_hit(["http://#@addr:#@port/"])
499 assert_equal String, results[0].class
501 port2 = unused_port(@addr)
504 ucfg.syswrite("listen '#@addr:#{port2}'\n")
505 Process.kill(:HUP, pid)
507 results = retry_hit(["http://#@addr:#{port2}/"])
508 assert_equal String, results[0].class
509 results = retry_hit(["http://#@addr:#{port_cli}/"])
510 assert_equal String, results[0].class
511 reuse = TCPServer.new(@addr, @port)
516 def test_unicorn_config_listen_with_options
517 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
518 ucfg = Tempfile.new('unicorn_test_config')
519 ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
520 ucfg.syswrite(" :rcvbuf => 4096,\n")
521 ucfg.syswrite(" :sndbuf => 4096\n")
523 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
525 results = retry_hit(["http://#{@addr}:#{@port}/"])
526 assert_equal String, results[0].class
530 def test_unicorn_config_per_worker_listen
532 pid_spit = 'use Rack::ContentLength;' \
533 'run proc { |e| [ 200, {"content-type"=>"text/plain"}, ["#$$\\n"] ] }'
534 File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
535 tmp = Tempfile.new('test.socket')
536 File.unlink(tmp.path)
537 ucfg = Tempfile.new('unicorn_test_config')
538 ucfg.syswrite("listen '#@addr:#@port'\n")
539 ucfg.syswrite("after_fork { |s,w|\n")
540 ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
541 ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
542 ucfg.syswrite("\n}\n")
544 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
546 results = retry_hit(["http://#{@addr}:#{@port}/"])
547 assert_equal String, results[0].class
548 worker_pid = results[0].to_i
549 assert_not_equal pid, worker_pid
550 s = unix_socket(tmp.path)
551 s.syswrite("GET / HTTP/1.0\r\n\r\n")
553 loop { results << s.sysread(4096) } rescue nil
555 assert_equal worker_pid, results.split(/\r\n/).last.to_i
556 results = hit(["http://#@addr:#{port2}/"])
557 assert_equal String, results[0].class
558 assert_equal worker_pid, results[0].to_i
562 def test_unicorn_config_listen_augments_cli
563 port2 = unused_port(@addr)
564 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
565 ucfg = Tempfile.new('unicorn_test_config')
566 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
569 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
572 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
573 results = retry_hit(uris)
574 assert_equal results.size, uris.size
575 assert_equal String, results[0].class
576 assert_equal String, results[1].class
580 def test_weird_config_settings
581 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
582 ucfg = Tempfile.new('unicorn_test_config')
583 proc_total = HEAVY_WORKERS + 1 # + 1 for master
584 ucfg.syswrite(HEAVY_CFG)
587 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
591 results = retry_hit(["http://#{@addr}:#{@port}/"])
592 assert_equal String, results[0].class
593 wait_master_ready(COMMON_TMP.path)
594 wait_workers_ready(COMMON_TMP.path, HEAVY_WORKERS)
595 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
596 assert_equal HEAVY_WORKERS, bf.size
597 rotate = Tempfile.new('unicorn_rotate')
599 File.rename(COMMON_TMP.path, rotate.path)
600 Process.kill(:USR1, pid)
602 wait_for_file(COMMON_TMP.path)
603 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
604 # USR1 should've been passed to all workers
605 tries = DEFAULT_TRIES
606 log = File.readlines(rotate.path)
607 while (tries -= 1) > 0 &&
608 log.grep(/reopening logs\.\.\./).size < proc_total
610 log = File.readlines(rotate.path)
612 assert_equal proc_total, log.grep(/reopening logs\.\.\./).size
613 assert_equal 0, log.grep(/done reopening logs/).size
615 tries = DEFAULT_TRIES
616 log = File.readlines(COMMON_TMP.path)
617 while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < proc_total
619 log = File.readlines(COMMON_TMP.path)
621 assert_equal proc_total, log.grep(/done reopening logs/).size
622 assert_equal 0, log.grep(/reopening logs\.\.\./).size
624 Process.kill(:QUIT, pid)
625 pid, status = Process.waitpid2(pid)
627 assert status.success?, "exited successfully"
630 def test_read_embedded_cli_switches
631 File.open("config.ru", "wb") do |fp|
632 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
635 pid = fork { redirect_test_io { exec($unicorn_bin) } }
636 results = retry_hit(["http://#{@addr}:#{@port}/"])
637 assert_equal String, results[0].class
641 def test_config_ru_alt_path
642 config_path = "#{@tmpdir}/foo.ru"
643 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
647 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
650 results = retry_hit(["http://#{@addr}:#{@port}/"])
651 assert_equal String, results[0].class
656 libdir = "#{@tmpdir}/lib"
657 FileUtils.mkpath([ libdir ])
658 config_path = "#{libdir}/hello.rb"
659 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
663 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
666 results = retry_hit(["http://#{@addr}:#{@port}/"])
667 assert_equal String, results[0].class
672 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
673 pid_file = "#{@tmpdir}/test.pid"
676 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
679 reexec_basic_test(pid, pid_file)
682 def test_reexec_alt_config
683 config_file = "#{@tmpdir}/foo.ru"
684 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
685 pid_file = "#{@tmpdir}/test.pid"
688 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
691 reexec_basic_test(pid, pid_file)
694 def test_socket_unlinked_restore
696 sock = Tempfile.new('unicorn_test_sock')
697 sock_path = sock.path
698 @sockets << sock_path
700 ucfg = Tempfile.new('unicorn_test_config')
701 ucfg.syswrite("listen \"#{sock_path}\"\n")
703 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
704 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
705 wait_for_file(sock_path)
706 assert File.socket?(sock_path)
708 sock = unix_socket(sock_path)
709 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
710 results = sock.sysread(4096)
712 assert_equal String, results.class
713 File.unlink(sock_path)
714 Process.kill(:HUP, pid)
715 wait_for_file(sock_path)
716 assert File.socket?(sock_path)
718 sock = unix_socket(sock_path)
719 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
720 results = sock.sysread(4096)
722 assert_equal String, results.class
725 def test_unicorn_config_file
726 pid_file = "#{@tmpdir}/test.pid"
727 sock = Tempfile.new('unicorn_test_sock')
728 sock_path = sock.path
730 @sockets << sock_path
732 log = Tempfile.new('unicorn_test_log')
733 ucfg = Tempfile.new('unicorn_test_config')
734 ucfg.syswrite("listen \"#{sock_path}\"\n")
735 ucfg.syswrite("pid \"#{pid_file}\"\n")
736 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
739 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
742 exec($unicorn_bin, "-l#{@addr}:#{@port}",
743 "-P#{pid_file}", "-c#{ucfg.path}")
746 results = retry_hit(["http://#{@addr}:#{@port}/"])
747 assert_equal String, results[0].class
748 wait_master_ready(log.path)
749 assert File.exist?(pid_file), "pid_file created"
750 assert_equal pid, File.read(pid_file).to_i
751 assert File.socket?(sock_path), "socket created"
753 sock = unix_socket(sock_path)
754 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
755 results = sock.sysread(4096)
757 assert_equal String, results.class
759 # try reloading the config
760 sock = Tempfile.new('new_test_sock')
761 new_sock_path = sock.path
762 @sockets << new_sock_path
764 new_log = Tempfile.new('unicorn_test_log')
766 assert_equal 0, new_log.size
768 ucfg = File.open(ucfg.path, "wb")
769 ucfg.syswrite("listen \"#{sock_path}\"\n")
770 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
771 ucfg.syswrite("pid \"#{pid_file}\"\n")
772 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
774 Process.kill(:HUP, pid)
776 wait_for_file(new_sock_path)
777 assert File.socket?(new_sock_path), "socket exists"
778 @sockets.each do |path|
779 sock = unix_socket(path)
780 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
781 results = sock.sysread(4096)
782 assert_equal String, results.class
785 assert_not_equal 0, new_log.size
786 reexec_usr2_quit_test(pid, pid_file)
789 def test_daemonize_reexec
790 pid_file = "#{@tmpdir}/test.pid"
791 log = Tempfile.new('unicorn_test_log')
792 ucfg = Tempfile.new('unicorn_test_config')
793 ucfg.syswrite("pid \"#{pid_file}\"\n")
794 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
797 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
800 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
803 results = retry_hit(["http://#{@addr}:#{@port}/"])
804 assert_equal String, results[0].class
805 wait_for_file(pid_file)
806 new_pid = File.read(pid_file).to_i
807 assert_not_equal pid, new_pid
808 pid, status = Process.waitpid2(pid)
809 assert status.success?, "original process exited successfully"
810 Process.kill(0, new_pid)
811 reexec_usr2_quit_test(new_pid, pid_file)
814 def test_daemonize_redirect_fail
815 pid_file = "#{@tmpdir}/test.pid"
816 ucfg = Tempfile.new('unicorn_test_config')
817 ucfg.syswrite("pid #{pid_file}\"\n")
818 err = Tempfile.new('stderr')
819 out = Tempfile.new('stdout ')
821 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
823 $stderr.reopen(err.path, "a")
824 $stdout.reopen(out.path, "a")
825 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
827 pid, status = Process.waitpid2(pid)
828 assert ! status.success?, "original process exited successfully"
829 sleep 1 # can't waitpid on a daemonized process :<
830 assert err.stat.size > 0
833 def test_reexec_fd_leak
834 unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
835 warn "FD leak test only works on Linux at the moment"
838 pid_file = "#{@tmpdir}/test.pid"
839 log = Tempfile.new('unicorn_test_log')
841 ucfg = Tempfile.new('unicorn_test_config')
842 ucfg.syswrite("pid \"#{pid_file}\"\n")
843 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
844 ucfg.syswrite("stderr_path '#{log.path}'\n")
845 ucfg.syswrite("stdout_path '#{log.path}'\n")
848 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
851 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
855 wait_master_ready(log.path)
856 wait_workers_ready(log.path, 1)
857 File.truncate(log.path, 0)
858 wait_for_file(pid_file)
859 orig_pid = pid = File.read(pid_file).to_i
860 orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
862 expect_size = orig_fds.size
864 Process.kill(:USR2, pid)
865 wait_for_file("#{pid_file}.oldbin")
866 Process.kill(:QUIT, pid)
870 wait_master_ready(log.path)
871 wait_workers_ready(log.path, 1)
872 File.truncate(log.path, 0)
873 wait_for_file(pid_file)
874 pid = File.read(pid_file).to_i
875 assert_not_equal orig_pid, pid
876 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
879 # we could've inherited descriptors the first time around
880 assert expect_size >= curr_fds.size, curr_fds.inspect
881 expect_size = curr_fds.size
883 Process.kill(:USR2, pid)
884 wait_for_file("#{pid_file}.oldbin")
885 Process.kill(:QUIT, pid)
889 wait_master_ready(log.path)
890 wait_workers_ready(log.path, 1)
891 File.truncate(log.path, 0)
892 wait_for_file(pid_file)
893 pid = File.read(pid_file).to_i
894 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
896 assert_equal expect_size, curr_fds.size, curr_fds.inspect
898 Process.kill(:QUIT, pid)
902 def hup_test_common(preload, check_client=false)
903 File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
904 pid_file = Tempfile.new('pid')
905 ucfg = Tempfile.new('unicorn_test_config')
906 ucfg.syswrite("listen '#@addr:#@port'\n")
907 ucfg.syswrite("pid '#{pid_file.path}'\n")
908 ucfg.syswrite("preload_app true\n") if preload
909 ucfg.syswrite("check_client_connection true\n") if check_client
910 ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
911 ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
913 redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
915 _, status = Process.waitpid2(pid)
916 assert status.success?
917 wait_master_ready("test_stderr.#$$.log")
918 wait_workers_ready("test_stderr.#$$.log", 1)
919 uri = URI.parse("http://#@addr:#@port/")
920 pids = Tempfile.new('worker_pids')
925 at_exit { pids.syswrite(bodies.inspect) }
926 trap(:TERM) { exit(0) }
929 rv = Net::HTTP.get(uri)
936 elsif bodies.size > 1
943 assert_equal '1', r.read(1)
944 daemon_pid = File.read(pid_file.path).to_i
945 assert daemon_pid > 0
946 Process.kill(:HUP, daemon_pid)
947 assert_equal '2', r.read(1)
948 Process.kill(:TERM, hitter)
949 _, hitter_status = Process.waitpid2(hitter)
950 assert(hitter_status.success?,
951 "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \
952 "#{File.read("test_stderr.#$$.log")}")
954 pids = eval(pids.read)
955 assert_kind_of(Hash, pids)
956 assert_equal 2, pids.size
958 assert_kind_of(Integer, x)
962 Process.kill(:QUIT, daemon_pid)
963 wait_for_death(daemon_pid)
966 def test_preload_app_hup
967 hup_test_common(true)
971 hup_test_common(false)
974 def test_check_client_hup
975 hup_test_common(false, true)
978 def test_default_listen_hup_holds_listener
979 default_listen_lock do
980 res, pid_path = default_listen_setup
981 daemon_pid = File.read(pid_path).to_i
982 Process.kill(:HUP, daemon_pid)
983 wait_workers_ready("test_stderr.#$$.log", 1)
984 res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
985 assert_match %r{\d+}, res2.first
986 assert res2.first != res.first
987 Process.kill(:QUIT, daemon_pid)
988 wait_for_death(daemon_pid)
992 def test_default_listen_upgrade_holds_listener
993 default_listen_lock do
994 res, pid_path = default_listen_setup
995 daemon_pid = File.read(pid_path).to_i
997 Process.kill(:USR2, daemon_pid)
998 wait_for_file("#{pid_path}.oldbin")
999 wait_for_file(pid_path)
1000 Process.kill(:QUIT, daemon_pid)
1001 wait_for_death(daemon_pid)
1003 daemon_pid = File.read(pid_path).to_i
1004 wait_workers_ready("test_stderr.#$$.log", 1)
1005 File.truncate("test_stderr.#$$.log", 0)
1007 res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1008 assert_match %r{\d+}, res2.first
1009 assert res2.first != res.first
1011 Process.kill(:HUP, daemon_pid)
1012 wait_workers_ready("test_stderr.#$$.log", 1)
1013 File.truncate("test_stderr.#$$.log", 0)
1014 res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1015 assert res2.first != res3.first
1017 Process.kill(:QUIT, daemon_pid)
1018 wait_for_death(daemon_pid)
1022 def default_listen_setup
1023 File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
1024 pid_path = (tmp = Tempfile.new('pid')).path
1026 ucfg = Tempfile.new('unicorn_test_config')
1027 ucfg.syswrite("pid '#{pid_path}'\n")
1028 ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
1029 ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
1031 redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
1033 _, status = Process.waitpid2(pid)
1034 assert status.success?
1035 wait_master_ready("test_stderr.#$$.log")
1036 wait_workers_ready("test_stderr.#$$.log", 1)
1037 File.truncate("test_stderr.#$$.log", 0)
1038 res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1039 assert_match %r{\d+}, res.first
1043 # we need to flock() something to prevent these tests from running
1044 def default_listen_lock(&block)
1045 fp = File.open(FLOCK_PATH, "rb")
1047 fp.flock(File::LOCK_EX)
1049 TCPServer.new(Unicorn::Const::DEFAULT_HOST,
1050 Unicorn::Const::DEFAULT_PORT).close
1051 rescue Errno::EADDRINUSE, Errno::EACCES
1052 warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}"
1056 # unused_port should never take this, but we may run an environment
1057 # where tests are being run against older unicorns...
1058 lock_path = "#{Dir::tmpdir}/unicorn_test." \
1059 "#{Unicorn::Const::DEFAULT_LISTEN}.lock"
1061 File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1063 rescue Errno::EEXIST
1067 File.unlink(lock_path) if lock_path
1070 fp.flock(File::LOCK_UN)