1 # Copyright (c) 2009 Eric Wong
2 STDIN.sync = STDOUT.sync = STDERR.sync = true
3 require 'test/test_helper'
12 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
14 do_test = system($unicorn_bin, '-v') or \
15 STDERR.puts "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, "\
22 STDERR.puts "Unable to load Rack, skipping this test"
26 class ExecTest < Test::Unit::TestCase
27 trap('QUIT', 'IGNORE')
30 use Rack::ContentLength
31 run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] }
37 [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ]
42 COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
49 logger Logger.new('#{COMMON_TMP.path}')
50 before_fork do |server, worker_nr|
51 server.logger.info "before_fork: worker=\#{worker_nr}"
53 after_fork do |server, worker_nr|
54 trap('USR1') do # log rotation
55 server.logger.info "after_fork: worker=\#{worker_nr} rotating logs..."
56 ObjectSpace.each_object(File) do |fp|
57 next if fp.closed? || ! fp.sync
58 next unless (fp.fcntl(Fcntl::F_GETFL) & File::APPEND) == File::APPEND
60 fp.stat.ino == File.stat(fp.path).ino
63 fp.reopen(fp.path, "a")
66 server.logger.info "after_fork: worker=\#{worker_nr} done rotating logs"
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 FileUtils.rmtree(@tmpdir)
86 @sockets.each { |path| File.unlink(path) rescue nil }
88 Process.kill('-QUIT', 0)
90 Process.waitpid(-1, Process::WNOHANG) or break
98 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
100 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
102 results = retry_hit(["http://#{@addr}:#{@port}/"])
103 assert_equal String, results[0].class
109 assert(system($unicorn_bin, "-h"), "help text returns true")
111 assert_equal 0, File.stat("test_stderr.#$$.log").size
112 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
113 lines = File.readlines("test_stdout.#$$.log")
115 # Be considerate of the on-call technician working from their
116 # mobile phone or netbook on a slow connection :)
117 assert lines.size <= 24, "help height fits in an ANSI terminal window"
119 assert line.size <= 80, "help width fits in an ANSI terminal window"
123 def test_broken_reexec_config
124 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
125 pid_file = "#{@tmpdir}/test.pid"
126 old_file = "#{pid_file}.oldbin"
127 ucfg = Tempfile.new('unicorn_test_config')
128 ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
129 ucfg.syswrite("pid %(#{pid_file})\n")
130 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
133 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
136 results = retry_hit(["http://#{@addr}:#{@port}/"])
137 assert_equal String, results[0].class
139 wait_for_file(pid_file)
141 Process.kill('USR2', File.read(pid_file).to_i)
142 wait_for_file(old_file)
143 wait_for_file(pid_file)
144 Process.kill('QUIT', File.read(old_file).to_i)
146 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
147 current_pid = File.read(pid_file).to_i
148 Process.kill('USR2', current_pid)
150 # wait for pid_file to restore itself
151 tries = DEFAULT_TRIES
153 while current_pid != File.read(pid_file).to_i
154 sleep(DEFAULT_RES) and (tries -= 1) > 0
157 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
159 assert_equal current_pid, File.read(pid_file).to_i
161 tries = DEFAULT_TRIES
162 while File.exist?(old_file)
163 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
165 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
166 port2 = unused_port(@addr)
171 ucfg.syswrite("listeners %w(#{@addr}:#{@port} #{@addr}:#{port2})\n")
172 ucfg.syswrite("pid %(#{pid_file})\n")
173 Process.kill('USR2', current_pid)
174 wait_for_file(old_file)
175 wait_for_file(pid_file)
176 new_pid = File.read(pid_file).to_i
177 assert_not_equal current_pid, new_pid
178 assert_equal current_pid, File.read(old_file).to_i
179 results = retry_hit(["http://#{@addr}:#{@port}/",
180 "http://#{@addr}:#{port2}/"])
181 assert_equal String, results[0].class
182 assert_equal String, results[1].class
184 assert_nothing_raised do
185 Process.kill('QUIT', current_pid)
186 Process.kill('QUIT', new_pid)
190 def test_broken_reexec_ru
191 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
192 pid_file = "#{@tmpdir}/test.pid"
193 old_file = "#{pid_file}.oldbin"
194 ucfg = Tempfile.new('unicorn_test_config')
195 ucfg.syswrite("pid %(#{pid_file})\n")
196 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
199 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
202 results = retry_hit(["http://#{@addr}:#{@port}/"])
203 assert_equal String, results[0].class
205 wait_for_file(pid_file)
207 Process.kill('USR2', File.read(pid_file).to_i)
208 wait_for_file(old_file)
209 wait_for_file(pid_file)
210 Process.kill('QUIT', File.read(old_file).to_i)
212 File.unlink("config.ru") # break reloading
213 current_pid = File.read(pid_file).to_i
214 Process.kill('USR2', current_pid)
216 # wait for pid_file to restore itself
217 tries = DEFAULT_TRIES
219 while current_pid != File.read(pid_file).to_i
220 sleep(DEFAULT_RES) and (tries -= 1) > 0
223 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
225 assert_equal current_pid, File.read(pid_file).to_i
227 tries = DEFAULT_TRIES
228 while File.exist?(old_file)
229 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
231 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
234 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
235 Process.kill('USR2', current_pid)
236 wait_for_file(old_file)
237 wait_for_file(pid_file)
238 new_pid = File.read(pid_file).to_i
239 assert_not_equal current_pid, new_pid
240 assert_equal current_pid, File.read(old_file).to_i
241 results = retry_hit(["http://#{@addr}:#{@port}/"])
242 assert_equal String, results[0].class
244 assert_nothing_raised do
245 Process.kill('QUIT', current_pid)
246 Process.kill('QUIT', new_pid)
250 def test_unicorn_config_listeners_overrides_cli
251 port2 = unused_port(@addr)
252 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
253 # listeners = [ ... ] => should _override_ command-line options
254 ucfg = Tempfile.new('unicorn_test_config')
255 ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
258 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
261 results = retry_hit(["http://#{@addr}:#{@port}/"])
262 assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) }
263 assert_equal String, results[0].class
267 def test_unicorn_config_listen_augments_cli
268 port2 = unused_port(@addr)
269 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
270 ucfg = Tempfile.new('unicorn_test_config')
271 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
274 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
277 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
278 results = retry_hit(uris)
279 assert_equal results.size, uris.size
280 assert_equal String, results[0].class
281 assert_equal String, results[1].class
285 def test_weird_config_settings
286 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
287 ucfg = Tempfile.new('unicorn_test_config')
288 ucfg.syswrite(HEAVY_CFG)
291 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
295 results = retry_hit(["http://#{@addr}:#{@port}/"])
296 assert_equal String, results[0].class
297 wait_master_ready(COMMON_TMP.path)
298 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
299 assert_equal 4, bf.size
300 rotate = Tempfile.new('unicorn_rotate')
301 assert_nothing_raised do
302 File.rename(COMMON_TMP.path, rotate.path)
303 Process.kill('USR1', pid)
305 wait_for_file(COMMON_TMP.path)
306 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
307 # USR1 should've been passed to all workers
308 tries = DEFAULT_TRIES
309 log = File.readlines(rotate.path)
310 while (tries -= 1) > 0 && log.grep(/rotating logs\.\.\./).size < 4
312 log = File.readlines(rotate.path)
314 assert_equal 4, log.grep(/rotating logs\.\.\./).size
315 assert_equal 0, log.grep(/done rotating logs/).size
317 tries = DEFAULT_TRIES
318 log = File.readlines(COMMON_TMP.path)
319 while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 4
321 log = File.readlines(COMMON_TMP.path)
323 assert_equal 4, log.grep(/done rotating logs/).size
324 assert_equal 0, log.grep(/rotating logs\.\.\./).size
325 assert_nothing_raised { Process.kill('QUIT', pid) }
327 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
328 assert status.success?, "exited successfully"
331 def test_ignore_embedded_cli_switches
332 port2 = unused_port(@addr)
333 File.open("config.ru", "wb") do |fp|
334 fp.syswrite("#\\ -p #{port2} -o #{@addr}\n")
338 redirect_test_io { exec($unicorn_bin, "-l#{@addr}:#{@port}") }
340 results = retry_hit(["http://#{@addr}:#{@port}/"])
341 assert_equal String, results[0].class
342 assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) }
346 def test_config_ru_alt_path
347 config_path = "#{@tmpdir}/foo.ru"
348 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
352 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
355 results = retry_hit(["http://#{@addr}:#{@port}/"])
356 assert_equal String, results[0].class
361 libdir = "#{@tmpdir}/lib"
362 FileUtils.mkpath([ libdir ])
363 config_path = "#{libdir}/hello.rb"
364 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
368 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
371 results = retry_hit(["http://#{@addr}:#{@port}/"])
372 assert_equal String, results[0].class
377 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
378 pid_file = "#{@tmpdir}/test.pid"
379 ucfg = Tempfile.new('unicorn_test_config')
380 ucfg.syswrite("pid \"#{pid_file}\"\n")
383 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
386 wait_for_file(pid_file)
387 reexec_usr2_quit_test(pid, pid_file)
390 def test_reexec_alt_config
391 config_file = "#{@tmpdir}/foo.ru"
392 pid_file = "#{@tmpdir}/test.pid"
393 ucfg = Tempfile.new('unicorn_test_config')
394 ucfg.syswrite("pid \"#{pid_file}\"\n")
395 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
398 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-c#{ucfg.path}", config_file)
401 wait_for_file(pid_file)
402 reexec_usr2_quit_test(pid, pid_file)
405 def test_unicorn_config_file
406 pid_file = "#{@tmpdir}/test.pid"
407 sock = Tempfile.new('unicorn_test_sock')
408 sock_path = sock.path
410 @sockets << sock_path
412 log = Tempfile.new('unicorn_test_log')
413 ucfg = Tempfile.new('unicorn_test_config')
414 ucfg.syswrite("listen \"#{sock_path}\"\n")
415 ucfg.syswrite("pid \"#{pid_file}\"\n")
416 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
419 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
422 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
425 results = retry_hit(["http://#{@addr}:#{@port}/"])
426 assert_equal String, results[0].class
427 wait_master_ready(log.path)
428 assert File.exist?(pid_file), "pid_file created"
429 assert_equal pid, File.read(pid_file).to_i
430 assert File.socket?(sock_path), "socket created"
431 assert_nothing_raised do
432 sock = UNIXSocket.new(sock_path)
433 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
434 results = sock.sysread(4096)
436 assert_equal String, results.class
438 # try reloading the config
439 sock = Tempfile.new('unicorn_test_sock')
440 new_sock_path = sock.path
441 @sockets << new_sock_path
443 new_log = Tempfile.new('unicorn_test_log')
445 assert_equal 0, new_log.size
447 assert_nothing_raised do
448 ucfg = File.open(ucfg.path, "wb")
449 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
450 ucfg.syswrite("pid \"#{pid_file}\"\n")
451 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
453 Process.kill('HUP', pid)
456 wait_for_file(new_sock_path)
457 assert File.socket?(new_sock_path), "socket exists"
458 @sockets.each do |path|
459 assert_nothing_raised do
460 sock = UNIXSocket.new(path)
461 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
462 results = sock.sysread(4096)
464 assert_equal String, results.class
467 assert_not_equal 0, new_log.size
468 reexec_usr2_quit_test(pid, pid_file)
471 def test_daemonize_reexec
472 pid_file = "#{@tmpdir}/test.pid"
473 log = Tempfile.new('unicorn_test_log')
474 ucfg = Tempfile.new('unicorn_test_config')
475 ucfg.syswrite("pid \"#{pid_file}\"\n")
476 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
479 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
482 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
485 results = retry_hit(["http://#{@addr}:#{@port}/"])
486 assert_equal String, results[0].class
487 wait_for_file(pid_file)
488 new_pid = File.read(pid_file).to_i
489 assert_not_equal pid, new_pid
490 pid, status = Process.waitpid2(pid)
491 assert status.success?, "original process exited successfully"
492 assert_nothing_raised { Process.kill(0, new_pid) }
493 reexec_usr2_quit_test(new_pid, pid_file)
498 # sometimes the server may not come up right away
499 def retry_hit(uris = [])
500 tries = DEFAULT_TRIES
503 rescue Errno::ECONNREFUSED => err
512 def assert_shutdown(pid)
513 wait_master_ready("#{@tmpdir}/test_stderr.#{pid}.log")
514 assert_nothing_raised { Process.kill('QUIT', pid) }
516 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
517 assert status.success?, "exited successfully"
520 def wait_master_ready(master_log)
521 tries = DEFAULT_TRIES
522 while (tries -= 1) > 0
524 File.readlines(master_log).grep(/master process ready/)[0] and return
529 raise "master process never became ready"
532 def reexec_usr2_quit_test(pid, pid_file)
533 assert File.exist?(pid_file), "pid file OK"
534 assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
535 assert_nothing_raised { Process.kill('USR2', pid) }
536 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
537 wait_for_file("#{pid_file}.oldbin")
538 wait_for_file(pid_file)
540 # kill old master process
541 assert_not_equal pid, File.read(pid_file).to_i
542 assert_equal pid, File.read("#{pid_file}.oldbin").to_i
543 assert_nothing_raised { Process.kill('QUIT', pid) }
544 assert_not_equal pid, File.read(pid_file).to_i
545 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
546 wait_for_file(pid_file)
547 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
548 assert_nothing_raised { Process.kill('QUIT', File.read(pid_file).to_i) }
551 def reexec_basic_test(pid, pid_file)
552 results = retry_hit(["http://#{@addr}:#{@port}/"])
553 assert_equal String, results[0].class
554 assert_nothing_raised { Process.kill(0, pid) }
555 master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
556 wait_master_ready(master_log)
557 File.truncate(master_log, 0)
560 assert_nothing_raised do
562 hit(["http://#{@addr}:#{@port}/#{i}"])
563 i == kill_point and Process.kill('HUP', pid)
566 wait_master_ready(master_log)
567 assert File.exist?(pid_file), "pid=#{pid_file} exists"
568 new_pid = File.read(pid_file).to_i
569 assert_not_equal pid, new_pid
570 assert_nothing_raised { Process.kill(0, new_pid) }
571 assert_nothing_raised { Process.kill('QUIT', new_pid) }
574 def wait_for_file(path)
575 tries = DEFAULT_TRIES
576 while (tries -= 1) > 0 && ! File.exist?(path)
579 assert File.exist?(path), "path=#{path} exists"
584 ObjectSpace.each_object(Tempfile) do |tmp|
585 ObjectSpace.undefine_finalizer(tmp)