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)
51 logger Logger.new('#{COMMON_TMP.path}')
52 before_fork do |server, worker|
53 server.logger.info "before_fork: worker=\#{worker.nr}"
57 WORKING_DIRECTORY_CHECK_RU = <<-EOS
58 use Rack::ContentLength
62 b = ::File.stat(Dir.pwd)
63 if (a.ino == b.ino && a.dev == b.dev)
64 [ 200, { 'Content-Type' => 'text/plain' }, [ pwd ] ]
66 [ 404, { 'Content-Type' => 'text/plain' }, [] ]
73 @tmpfile = Tempfile.new('unicorn_exec_test')
74 @tmpdir = @tmpfile.path
78 @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
79 @port = unused_port(@addr)
85 return if @start_pid != $$
87 FileUtils.rmtree(@tmpdir)
88 @sockets.each { |path| File.unlink(path) rescue nil }
90 Process.kill('-QUIT', 0)
92 Process.waitpid(-1, Process::WNOHANG) or break
99 def test_sd_listen_fds_emulation
100 File.open("config.ru", "wb") { |fp| fp.write(HI) }
101 sock = TCPServer.new(@addr, @port)
102 sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0)
106 # pretend to be systemd
107 ENV['LISTEN_PID'] = "#$$"
108 ENV['LISTEN_FDS'] = '1'
110 # 3 = SD_LISTEN_FDS_START
111 exec($unicorn_bin, "-l", "#@addr:#@port", 3 => sock)
114 res = hit(["http://#{@addr}:#{@port}/"])
115 assert_equal [ "HI\n"], res
117 assert_equal 1, sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).int,
118 "unicorn should always set SO_KEEPALIVE on inherited sockets"
123 def test_working_directory_rel_path_config_file
124 other = Tempfile.new('unicorn.wd')
125 File.unlink(other.path)
126 Dir.mkdir(other.path)
127 File.open("config.ru", "wb") do |fp|
128 fp.syswrite WORKING_DIRECTORY_CHECK_RU
130 FileUtils.cp("config.ru", other.path + "/config.ru")
133 tmp = File.open('unicorn.config', 'wb')
135 working_directory '#@tmpdir'
136 listen '#@addr:#@port'
138 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
139 wait_workers_ready("test_stderr.#{pid}.log", 1)
140 results = hit(["http://#@addr:#@port/"])
141 assert_equal @tmpdir, results.first
142 File.truncate("test_stderr.#{pid}.log", 0)
147 working_directory '#{other.path}'
148 listen '#@addr:#@port'
151 Process.kill(:HUP, pid)
153 re = /config_file=(.+) would not be accessible in working_directory=(.+)/
156 lines = File.readlines("test_stderr.#{pid}.log")
159 File.truncate("test_stderr.#{pid}.log", 0)
160 FileUtils.cp('unicorn.config', other.path + "/unicorn.config")
161 Process.kill(:HUP, pid)
162 wait_workers_ready("test_stderr.#{pid}.log", 1)
163 results = hit(["http://#@addr:#@port/"])
164 assert_equal other.path, results.first
166 Process.kill(:QUIT, pid)
168 FileUtils.rmtree(other.path)
171 def test_working_directory
172 other = Tempfile.new('unicorn.wd')
173 File.unlink(other.path)
174 Dir.mkdir(other.path)
175 File.open("config.ru", "wb") do |fp|
176 fp.syswrite WORKING_DIRECTORY_CHECK_RU
178 FileUtils.cp("config.ru", other.path + "/config.ru")
179 tmp = Tempfile.new('unicorn.config')
181 working_directory '#@tmpdir'
182 listen '#@addr:#@port'
184 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
185 wait_workers_ready("test_stderr.#{pid}.log", 1)
186 results = hit(["http://#@addr:#@port/"])
187 assert_equal @tmpdir, results.first
188 File.truncate("test_stderr.#{pid}.log", 0)
193 working_directory '#{other.path}'
194 listen '#@addr:#@port'
197 Process.kill(:HUP, pid)
198 wait_workers_ready("test_stderr.#{pid}.log", 1)
199 results = hit(["http://#@addr:#@port/"])
200 assert_equal other.path, results.first
202 Process.kill(:QUIT, pid)
204 FileUtils.rmtree(other.path)
207 def test_working_directory_controls_relative_paths
208 other = Tempfile.new('unicorn.wd')
209 File.unlink(other.path)
210 Dir.mkdir(other.path)
211 File.open("config.ru", "wb") do |fp|
212 fp.syswrite WORKING_DIRECTORY_CHECK_RU
214 FileUtils.cp("config.ru", other.path + "/config.ru")
215 system('mkfifo', "#{other.path}/fifo")
216 tmp = Tempfile.new('unicorn.config')
219 stderr_path "stderr_log_here"
220 stdout_path "stdout_log_here"
221 working_directory '#{other.path}'
222 listen '#@addr:#@port'
223 after_fork do |server, worker|
224 File.open("fifo", "wb").close
227 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
228 File.open("#{other.path}/fifo", "rb").close
230 assert ! File.exist?("stderr_log_here")
231 assert ! File.exist?("stdout_log_here")
232 assert ! File.exist?("pid_file_here")
234 assert ! File.exist?("#@tmpdir/stderr_log_here")
235 assert ! File.exist?("#@tmpdir/stdout_log_here")
236 assert ! File.exist?("#@tmpdir/pid_file_here")
238 assert File.exist?("#{other.path}/pid_file_here")
239 assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here")
240 assert File.exist?("#{other.path}/stderr_log_here")
241 assert File.exist?("#{other.path}/stdout_log_here")
242 wait_master_ready("#{other.path}/stderr_log_here")
244 Process.kill(:QUIT, pid)
246 FileUtils.rmtree(other.path)
250 def test_exit_signals
251 %w(INT TERM QUIT).each do |sig|
252 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
253 pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
254 wait_master_ready("test_stderr.#{pid}.log")
255 wait_workers_ready("test_stderr.#{pid}.log", 1)
257 Process.kill(sig, pid)
258 pid, status = Process.waitpid2(pid)
260 reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
261 assert_equal 1, reaped.size
262 assert status.exited?
267 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
269 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
271 results = retry_hit(["http://#{@addr}:#{@port}/"])
272 assert_equal String, results[0].class
276 def test_rack_env_unset
277 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
278 pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
279 results = retry_hit(["http://#{@addr}:#{@port}/"])
280 assert_equal "development", results.first
284 def test_rack_env_cli_set
285 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
287 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
289 results = retry_hit(["http://#{@addr}:#{@port}/"])
290 assert_equal "asdf", results.first
294 def test_rack_env_ENV_set
295 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
297 ENV["RACK_ENV"] = "foobar"
298 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
300 results = retry_hit(["http://#{@addr}:#{@port}/"])
301 assert_equal "foobar", results.first
305 def test_rack_env_cli_override_ENV
306 File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
308 ENV["RACK_ENV"] = "foobar"
309 redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
311 results = retry_hit(["http://#{@addr}:#{@port}/"])
312 assert_equal "asdf", results.first
317 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
318 pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
319 log = "test_stderr.#{pid}.log"
320 wait_master_ready(log)
322 Process.kill(:TTIN, pid)
323 wait_workers_ready(log, i)
325 File.truncate(log, 0)
327 [ 2, 1, 0].each { |i|
328 Process.kill(:TTOU, pid)
329 DEFAULT_TRIES.times {
331 reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
332 break if reaped.size == 1
334 assert_equal 1, reaped.size
340 assert(system($unicorn_bin, "-h"), "help text returns true")
342 assert_equal 0, File.stat("test_stderr.#$$.log").size
343 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
344 lines = File.readlines("test_stdout.#$$.log")
346 # Be considerate of the on-call technician working from their
347 # mobile phone or netbook on a slow connection :)
348 assert lines.size <= 24, "help height fits in an ANSI terminal window"
351 assert line.size <= 80, "help width fits in an ANSI terminal window"
355 def test_broken_reexec_config
356 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
357 pid_file = "#{@tmpdir}/test.pid"
358 old_file = "#{pid_file}.oldbin"
359 ucfg = Tempfile.new('unicorn_test_config')
360 ucfg.syswrite("listen %(#@addr:#@port)\n")
361 ucfg.syswrite("pid %(#{pid_file})\n")
362 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
365 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
368 results = retry_hit(["http://#{@addr}:#{@port}/"])
369 assert_equal String, results[0].class
371 wait_for_file(pid_file)
373 Process.kill(:USR2, File.read(pid_file).to_i)
374 wait_for_file(old_file)
375 wait_for_file(pid_file)
376 old_pid = File.read(old_file).to_i
377 Process.kill(:QUIT, old_pid)
378 wait_for_death(old_pid)
380 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
381 current_pid = File.read(pid_file).to_i
382 Process.kill(:USR2, current_pid)
384 # wait for pid_file to restore itself
385 tries = DEFAULT_TRIES
387 while current_pid != File.read(pid_file).to_i
388 sleep(DEFAULT_RES) and (tries -= 1) > 0
391 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
393 assert_equal current_pid, File.read(pid_file).to_i
395 tries = DEFAULT_TRIES
396 while File.exist?(old_file)
397 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
399 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
400 port2 = unused_port(@addr)
405 ucfg.syswrite("listen %(#@addr:#@port)\n")
406 ucfg.syswrite("listen %(#@addr:#{port2})\n")
407 ucfg.syswrite("pid %(#{pid_file})\n")
408 Process.kill(:USR2, current_pid)
410 wait_for_file(old_file)
411 wait_for_file(pid_file)
412 new_pid = File.read(pid_file).to_i
413 assert_not_equal current_pid, new_pid
414 assert_equal current_pid, File.read(old_file).to_i
415 results = retry_hit(["http://#{@addr}:#{@port}/",
416 "http://#{@addr}:#{port2}/"])
417 assert_equal String, results[0].class
418 assert_equal String, results[1].class
420 Process.kill(:QUIT, current_pid)
421 Process.kill(:QUIT, new_pid)
424 def test_broken_reexec_ru
425 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
426 pid_file = "#{@tmpdir}/test.pid"
427 old_file = "#{pid_file}.oldbin"
428 ucfg = Tempfile.new('unicorn_test_config')
429 ucfg.syswrite("pid %(#{pid_file})\n")
430 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
433 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
436 results = retry_hit(["http://#{@addr}:#{@port}/"])
437 assert_equal String, results[0].class
439 wait_for_file(pid_file)
441 Process.kill(:USR2, File.read(pid_file).to_i)
442 wait_for_file(old_file)
443 wait_for_file(pid_file)
444 old_pid = File.read(old_file).to_i
445 Process.kill(:QUIT, old_pid)
446 wait_for_death(old_pid)
448 File.unlink("config.ru") # break reloading
449 current_pid = File.read(pid_file).to_i
450 Process.kill(:USR2, current_pid)
452 # wait for pid_file to restore itself
453 tries = DEFAULT_TRIES
455 while current_pid != File.read(pid_file).to_i
456 sleep(DEFAULT_RES) and (tries -= 1) > 0
459 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
462 tries = DEFAULT_TRIES
463 while File.exist?(old_file)
464 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
466 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
467 assert_equal current_pid, File.read(pid_file).to_i
470 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
471 Process.kill(:USR2, current_pid)
472 wait_for_file(old_file)
473 wait_for_file(pid_file)
474 new_pid = File.read(pid_file).to_i
475 assert_not_equal current_pid, new_pid
476 assert_equal current_pid, File.read(old_file).to_i
477 results = retry_hit(["http://#{@addr}:#{@port}/"])
478 assert_equal String, results[0].class
480 Process.kill(:QUIT, current_pid)
481 Process.kill(:QUIT, new_pid)
484 def test_unicorn_config_listener_swap
485 port_cli = unused_port
486 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
487 ucfg = Tempfile.new('unicorn_test_config')
488 ucfg.syswrite("listen '#@addr:#@port'\n")
491 exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
494 results = retry_hit(["http://#@addr:#{port_cli}/"])
495 assert_equal String, results[0].class
496 results = retry_hit(["http://#@addr:#@port/"])
497 assert_equal String, results[0].class
499 port2 = unused_port(@addr)
502 ucfg.syswrite("listen '#@addr:#{port2}'\n")
503 Process.kill(:HUP, pid)
505 results = retry_hit(["http://#@addr:#{port2}/"])
506 assert_equal String, results[0].class
507 results = retry_hit(["http://#@addr:#{port_cli}/"])
508 assert_equal String, results[0].class
509 reuse = TCPServer.new(@addr, @port)
514 def test_unicorn_config_listen_with_options
515 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
516 ucfg = Tempfile.new('unicorn_test_config')
517 ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
518 ucfg.syswrite(" :rcvbuf => 4096,\n")
519 ucfg.syswrite(" :sndbuf => 4096\n")
521 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
523 results = retry_hit(["http://#{@addr}:#{@port}/"])
524 assert_equal String, results[0].class
528 def test_unicorn_config_per_worker_listen
530 pid_spit = 'use Rack::ContentLength;' \
531 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
532 File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
533 tmp = Tempfile.new('test.socket')
534 File.unlink(tmp.path)
535 ucfg = Tempfile.new('unicorn_test_config')
536 ucfg.syswrite("listen '#@addr:#@port'\n")
537 ucfg.syswrite("after_fork { |s,w|\n")
538 ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
539 ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
540 ucfg.syswrite("\n}\n")
542 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
544 results = retry_hit(["http://#{@addr}:#{@port}/"])
545 assert_equal String, results[0].class
546 worker_pid = results[0].to_i
547 assert_not_equal pid, worker_pid
548 s = UNIXSocket.new(tmp.path)
549 s.syswrite("GET / HTTP/1.0\r\n\r\n")
551 loop { results << s.sysread(4096) } rescue nil
553 assert_equal worker_pid, results.split(/\r\n/).last.to_i
554 results = hit(["http://#@addr:#{port2}/"])
555 assert_equal String, results[0].class
556 assert_equal worker_pid, results[0].to_i
560 def test_unicorn_config_listen_augments_cli
561 port2 = unused_port(@addr)
562 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
563 ucfg = Tempfile.new('unicorn_test_config')
564 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
567 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
570 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
571 results = retry_hit(uris)
572 assert_equal results.size, uris.size
573 assert_equal String, results[0].class
574 assert_equal String, results[1].class
578 def test_weird_config_settings
579 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
580 ucfg = Tempfile.new('unicorn_test_config')
581 ucfg.syswrite(HEAVY_CFG)
584 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
588 results = retry_hit(["http://#{@addr}:#{@port}/"])
589 assert_equal String, results[0].class
590 wait_master_ready(COMMON_TMP.path)
591 wait_workers_ready(COMMON_TMP.path, 4)
592 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
593 assert_equal 4, bf.size
594 rotate = Tempfile.new('unicorn_rotate')
596 File.rename(COMMON_TMP.path, rotate.path)
597 Process.kill(:USR1, pid)
599 wait_for_file(COMMON_TMP.path)
600 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
601 # USR1 should've been passed to all workers
602 tries = DEFAULT_TRIES
603 log = File.readlines(rotate.path)
604 while (tries -= 1) > 0 &&
605 log.grep(/reopening logs\.\.\./).size < 5
607 log = File.readlines(rotate.path)
609 assert_equal 5, log.grep(/reopening logs\.\.\./).size
610 assert_equal 0, log.grep(/done reopening logs/).size
612 tries = DEFAULT_TRIES
613 log = File.readlines(COMMON_TMP.path)
614 while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
616 log = File.readlines(COMMON_TMP.path)
618 assert_equal 5, log.grep(/done reopening logs/).size
619 assert_equal 0, log.grep(/reopening logs\.\.\./).size
621 Process.kill(:QUIT, pid)
622 pid, status = Process.waitpid2(pid)
624 assert status.success?, "exited successfully"
627 def test_read_embedded_cli_switches
628 File.open("config.ru", "wb") do |fp|
629 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
632 pid = fork { redirect_test_io { exec($unicorn_bin) } }
633 results = retry_hit(["http://#{@addr}:#{@port}/"])
634 assert_equal String, results[0].class
638 def test_config_ru_alt_path
639 config_path = "#{@tmpdir}/foo.ru"
640 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
644 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
647 results = retry_hit(["http://#{@addr}:#{@port}/"])
648 assert_equal String, results[0].class
653 libdir = "#{@tmpdir}/lib"
654 FileUtils.mkpath([ libdir ])
655 config_path = "#{libdir}/hello.rb"
656 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
660 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
663 results = retry_hit(["http://#{@addr}:#{@port}/"])
664 assert_equal String, results[0].class
669 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
670 pid_file = "#{@tmpdir}/test.pid"
673 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
676 reexec_basic_test(pid, pid_file)
679 def test_reexec_alt_config
680 config_file = "#{@tmpdir}/foo.ru"
681 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
682 pid_file = "#{@tmpdir}/test.pid"
685 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
688 reexec_basic_test(pid, pid_file)
691 def test_socket_unlinked_restore
693 sock = Tempfile.new('unicorn_test_sock')
694 sock_path = sock.path
695 @sockets << sock_path
697 ucfg = Tempfile.new('unicorn_test_config')
698 ucfg.syswrite("listen \"#{sock_path}\"\n")
700 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
701 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
702 wait_for_file(sock_path)
703 assert File.socket?(sock_path)
705 sock = UNIXSocket.new(sock_path)
706 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
707 results = sock.sysread(4096)
709 assert_equal String, results.class
710 File.unlink(sock_path)
711 Process.kill(:HUP, pid)
712 wait_for_file(sock_path)
713 assert File.socket?(sock_path)
715 sock = UNIXSocket.new(sock_path)
716 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
717 results = sock.sysread(4096)
719 assert_equal String, results.class
722 def test_unicorn_config_file
723 pid_file = "#{@tmpdir}/test.pid"
724 sock = Tempfile.new('unicorn_test_sock')
725 sock_path = sock.path
727 @sockets << sock_path
729 log = Tempfile.new('unicorn_test_log')
730 ucfg = Tempfile.new('unicorn_test_config')
731 ucfg.syswrite("listen \"#{sock_path}\"\n")
732 ucfg.syswrite("pid \"#{pid_file}\"\n")
733 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
736 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
739 exec($unicorn_bin, "-l#{@addr}:#{@port}",
740 "-P#{pid_file}", "-c#{ucfg.path}")
743 results = retry_hit(["http://#{@addr}:#{@port}/"])
744 assert_equal String, results[0].class
745 wait_master_ready(log.path)
746 assert File.exist?(pid_file), "pid_file created"
747 assert_equal pid, File.read(pid_file).to_i
748 assert File.socket?(sock_path), "socket created"
750 sock = UNIXSocket.new(sock_path)
751 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
752 results = sock.sysread(4096)
754 assert_equal String, results.class
756 # try reloading the config
757 sock = Tempfile.new('new_test_sock')
758 new_sock_path = sock.path
759 @sockets << new_sock_path
761 new_log = Tempfile.new('unicorn_test_log')
763 assert_equal 0, new_log.size
765 ucfg = File.open(ucfg.path, "wb")
766 ucfg.syswrite("listen \"#{sock_path}\"\n")
767 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
768 ucfg.syswrite("pid \"#{pid_file}\"\n")
769 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
771 Process.kill(:HUP, pid)
773 wait_for_file(new_sock_path)
774 assert File.socket?(new_sock_path), "socket exists"
775 @sockets.each do |path|
776 sock = UNIXSocket.new(path)
777 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
778 results = sock.sysread(4096)
779 assert_equal String, results.class
782 assert_not_equal 0, new_log.size
783 reexec_usr2_quit_test(pid, pid_file)
786 def test_daemonize_reexec
787 pid_file = "#{@tmpdir}/test.pid"
788 log = Tempfile.new('unicorn_test_log')
789 ucfg = Tempfile.new('unicorn_test_config')
790 ucfg.syswrite("pid \"#{pid_file}\"\n")
791 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
794 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
797 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
800 results = retry_hit(["http://#{@addr}:#{@port}/"])
801 assert_equal String, results[0].class
802 wait_for_file(pid_file)
803 new_pid = File.read(pid_file).to_i
804 assert_not_equal pid, new_pid
805 pid, status = Process.waitpid2(pid)
806 assert status.success?, "original process exited successfully"
807 Process.kill(0, new_pid)
808 reexec_usr2_quit_test(new_pid, pid_file)
811 def test_daemonize_redirect_fail
812 pid_file = "#{@tmpdir}/test.pid"
813 ucfg = Tempfile.new('unicorn_test_config')
814 ucfg.syswrite("pid #{pid_file}\"\n")
815 err = Tempfile.new('stderr')
816 out = Tempfile.new('stdout ')
818 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
820 $stderr.reopen(err.path, "a")
821 $stdout.reopen(out.path, "a")
822 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
824 pid, status = Process.waitpid2(pid)
825 assert ! status.success?, "original process exited successfully"
826 sleep 1 # can't waitpid on a daemonized process :<
827 assert err.stat.size > 0
830 def test_reexec_fd_leak
831 unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
832 warn "FD leak test only works on Linux at the moment"
835 pid_file = "#{@tmpdir}/test.pid"
836 log = Tempfile.new('unicorn_test_log')
838 ucfg = Tempfile.new('unicorn_test_config')
839 ucfg.syswrite("pid \"#{pid_file}\"\n")
840 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
841 ucfg.syswrite("stderr_path '#{log.path}'\n")
842 ucfg.syswrite("stdout_path '#{log.path}'\n")
845 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
848 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
852 wait_master_ready(log.path)
853 wait_workers_ready(log.path, 1)
854 File.truncate(log.path, 0)
855 wait_for_file(pid_file)
856 orig_pid = pid = File.read(pid_file).to_i
857 orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
859 expect_size = orig_fds.size
861 Process.kill(:USR2, pid)
862 wait_for_file("#{pid_file}.oldbin")
863 Process.kill(:QUIT, pid)
867 wait_master_ready(log.path)
868 wait_workers_ready(log.path, 1)
869 File.truncate(log.path, 0)
870 wait_for_file(pid_file)
871 pid = File.read(pid_file).to_i
872 assert_not_equal orig_pid, pid
873 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
876 # we could've inherited descriptors the first time around
877 assert expect_size >= curr_fds.size, curr_fds.inspect
878 expect_size = curr_fds.size
880 Process.kill(:USR2, pid)
881 wait_for_file("#{pid_file}.oldbin")
882 Process.kill(:QUIT, pid)
886 wait_master_ready(log.path)
887 wait_workers_ready(log.path, 1)
888 File.truncate(log.path, 0)
889 wait_for_file(pid_file)
890 pid = File.read(pid_file).to_i
891 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
893 assert_equal expect_size, curr_fds.size, curr_fds.inspect
895 Process.kill(:QUIT, pid)
899 def hup_test_common(preload, check_client=false)
900 File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
901 pid_file = Tempfile.new('pid')
902 ucfg = Tempfile.new('unicorn_test_config')
903 ucfg.syswrite("listen '#@addr:#@port'\n")
904 ucfg.syswrite("pid '#{pid_file.path}'\n")
905 ucfg.syswrite("preload_app true\n") if preload
906 ucfg.syswrite("check_client_connection true\n") if check_client
907 ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
908 ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
910 redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
912 _, status = Process.waitpid2(pid)
913 assert status.success?
914 wait_master_ready("test_stderr.#$$.log")
915 wait_workers_ready("test_stderr.#$$.log", 1)
916 uri = URI.parse("http://#@addr:#@port/")
917 pids = Tempfile.new('worker_pids')
922 at_exit { pids.syswrite(bodies.inspect) }
923 trap(:TERM) { exit(0) }
926 rv = Net::HTTP.get(uri)
933 elsif bodies.size > 1
940 assert_equal '1', r.read(1)
941 daemon_pid = File.read(pid_file.path).to_i
942 assert daemon_pid > 0
943 Process.kill(:HUP, daemon_pid)
944 assert_equal '2', r.read(1)
945 Process.kill(:TERM, hitter)
946 _, hitter_status = Process.waitpid2(hitter)
947 assert(hitter_status.success?,
948 "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \
949 "#{File.read("test_stderr.#$$.log")}")
951 pids = eval(pids.read)
952 assert_kind_of(Hash, pids)
953 assert_equal 2, pids.size
955 assert_kind_of(Integer, x)
959 Process.kill(:QUIT, daemon_pid)
960 wait_for_death(daemon_pid)
963 def test_preload_app_hup
964 hup_test_common(true)
968 hup_test_common(false)
971 def test_check_client_hup
972 hup_test_common(false, true)
975 def test_default_listen_hup_holds_listener
976 default_listen_lock do
977 res, pid_path = default_listen_setup
978 daemon_pid = File.read(pid_path).to_i
979 Process.kill(:HUP, daemon_pid)
980 wait_workers_ready("test_stderr.#$$.log", 1)
981 res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
982 assert_match %r{\d+}, res2.first
983 assert res2.first != res.first
984 Process.kill(:QUIT, daemon_pid)
985 wait_for_death(daemon_pid)
989 def test_default_listen_upgrade_holds_listener
990 default_listen_lock do
991 res, pid_path = default_listen_setup
992 daemon_pid = File.read(pid_path).to_i
994 Process.kill(:USR2, daemon_pid)
995 wait_for_file("#{pid_path}.oldbin")
996 wait_for_file(pid_path)
997 Process.kill(:QUIT, daemon_pid)
998 wait_for_death(daemon_pid)
1000 daemon_pid = File.read(pid_path).to_i
1001 wait_workers_ready("test_stderr.#$$.log", 1)
1002 File.truncate("test_stderr.#$$.log", 0)
1004 res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1005 assert_match %r{\d+}, res2.first
1006 assert res2.first != res.first
1008 Process.kill(:HUP, daemon_pid)
1009 wait_workers_ready("test_stderr.#$$.log", 1)
1010 File.truncate("test_stderr.#$$.log", 0)
1011 res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1012 assert res2.first != res3.first
1014 Process.kill(:QUIT, daemon_pid)
1015 wait_for_death(daemon_pid)
1019 def default_listen_setup
1020 File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
1021 pid_path = (tmp = Tempfile.new('pid')).path
1023 ucfg = Tempfile.new('unicorn_test_config')
1024 ucfg.syswrite("pid '#{pid_path}'\n")
1025 ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
1026 ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
1028 redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
1030 _, status = Process.waitpid2(pid)
1031 assert status.success?
1032 wait_master_ready("test_stderr.#$$.log")
1033 wait_workers_ready("test_stderr.#$$.log", 1)
1034 File.truncate("test_stderr.#$$.log", 0)
1035 res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1036 assert_match %r{\d+}, res.first
1040 # we need to flock() something to prevent these tests from running
1041 def default_listen_lock(&block)
1042 fp = File.open(FLOCK_PATH, "rb")
1044 fp.flock(File::LOCK_EX)
1046 TCPServer.new(Unicorn::Const::DEFAULT_HOST,
1047 Unicorn::Const::DEFAULT_PORT).close
1048 rescue Errno::EADDRINUSE, Errno::EACCES
1049 warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}"
1053 # unused_port should never take this, but we may run an environment
1054 # where tests are being run against older unicorns...
1055 lock_path = "#{Dir::tmpdir}/unicorn_test." \
1056 "#{Unicorn::Const::DEFAULT_LISTEN}.lock"
1058 File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1060 rescue Errno::EEXIST
1064 File.unlink(lock_path) if lock_path
1067 fp.flock(File::LOCK_UN)