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')
18 STDERR.puts "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
25 STDERR.puts "Unable to load Rack, skipping this test"
29 class ExecTest < Test::Unit::TestCase
33 use Rack::ContentLength
34 run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] }
40 [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ]
45 COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
51 logger Logger.new('#{COMMON_TMP.path}')
52 before_fork do |server, worker_nr|
53 server.logger.info "before_fork: worker=\#{worker_nr}"
59 @tmpfile = Tempfile.new('unicorn_exec_test')
60 @tmpdir = @tmpfile.path
64 @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
65 @port = unused_port(@addr)
71 return if @start_pid != $$
73 FileUtils.rmtree(@tmpdir)
74 @sockets.each { |path| File.unlink(path) rescue nil }
76 Process.kill('-QUIT', 0)
78 Process.waitpid(-1, Process::WNOHANG) or break
86 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
88 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
90 results = retry_hit(["http://#{@addr}:#{@port}/"])
91 assert_equal String, results[0].class
97 assert(system($unicorn_bin, "-h"), "help text returns true")
99 assert_equal 0, File.stat("test_stderr.#$$.log").size
100 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
101 lines = File.readlines("test_stdout.#$$.log")
103 # Be considerate of the on-call technician working from their
104 # mobile phone or netbook on a slow connection :)
105 assert lines.size <= 24, "help height fits in an ANSI terminal window"
107 assert line.size <= 80, "help width fits in an ANSI terminal window"
111 def test_broken_reexec_config
112 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
113 pid_file = "#{@tmpdir}/test.pid"
114 old_file = "#{pid_file}.oldbin"
115 ucfg = Tempfile.new('unicorn_test_config')
116 ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
117 ucfg.syswrite("pid %(#{pid_file})\n")
118 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
121 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
124 results = retry_hit(["http://#{@addr}:#{@port}/"])
125 assert_equal String, results[0].class
127 wait_for_file(pid_file)
129 Process.kill(:USR2, File.read(pid_file).to_i)
130 wait_for_file(old_file)
131 wait_for_file(pid_file)
132 Process.kill(:QUIT, File.read(old_file).to_i)
134 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
135 current_pid = File.read(pid_file).to_i
136 Process.kill(:USR2, current_pid)
138 # wait for pid_file to restore itself
139 tries = DEFAULT_TRIES
141 while current_pid != File.read(pid_file).to_i
142 sleep(DEFAULT_RES) and (tries -= 1) > 0
145 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
147 assert_equal current_pid, File.read(pid_file).to_i
149 tries = DEFAULT_TRIES
150 while File.exist?(old_file)
151 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
153 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
154 port2 = unused_port(@addr)
159 ucfg.syswrite("listeners %w(#{@addr}:#{@port} #{@addr}:#{port2})\n")
160 ucfg.syswrite("pid %(#{pid_file})\n")
161 Process.kill(:USR2, current_pid)
162 wait_for_file(old_file)
163 wait_for_file(pid_file)
164 new_pid = File.read(pid_file).to_i
165 assert_not_equal current_pid, new_pid
166 assert_equal current_pid, File.read(old_file).to_i
167 results = retry_hit(["http://#{@addr}:#{@port}/",
168 "http://#{@addr}:#{port2}/"])
169 assert_equal String, results[0].class
170 assert_equal String, results[1].class
172 assert_nothing_raised do
173 Process.kill(:QUIT, current_pid)
174 Process.kill(:QUIT, new_pid)
178 def test_broken_reexec_ru
179 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
180 pid_file = "#{@tmpdir}/test.pid"
181 old_file = "#{pid_file}.oldbin"
182 ucfg = Tempfile.new('unicorn_test_config')
183 ucfg.syswrite("pid %(#{pid_file})\n")
184 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
187 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
190 results = retry_hit(["http://#{@addr}:#{@port}/"])
191 assert_equal String, results[0].class
193 wait_for_file(pid_file)
195 Process.kill(:USR2, File.read(pid_file).to_i)
196 wait_for_file(old_file)
197 wait_for_file(pid_file)
198 Process.kill(:QUIT, File.read(old_file).to_i)
200 File.unlink("config.ru") # break reloading
201 current_pid = File.read(pid_file).to_i
202 Process.kill(:USR2, current_pid)
204 # wait for pid_file to restore itself
205 tries = DEFAULT_TRIES
207 while current_pid != File.read(pid_file).to_i
208 sleep(DEFAULT_RES) and (tries -= 1) > 0
211 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
213 assert_equal current_pid, File.read(pid_file).to_i
215 tries = DEFAULT_TRIES
216 while File.exist?(old_file)
217 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
219 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
222 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
223 Process.kill(:USR2, current_pid)
224 wait_for_file(old_file)
225 wait_for_file(pid_file)
226 new_pid = File.read(pid_file).to_i
227 assert_not_equal current_pid, new_pid
228 assert_equal current_pid, File.read(old_file).to_i
229 results = retry_hit(["http://#{@addr}:#{@port}/"])
230 assert_equal String, results[0].class
232 assert_nothing_raised do
233 Process.kill(:QUIT, current_pid)
234 Process.kill(:QUIT, new_pid)
238 def test_unicorn_config_listeners_overrides_cli
239 port2 = unused_port(@addr)
240 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
241 # listeners = [ ... ] => should _override_ command-line options
242 ucfg = Tempfile.new('unicorn_test_config')
243 ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
246 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
249 results = retry_hit(["http://#{@addr}:#{@port}/"])
250 assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) }
251 assert_equal String, results[0].class
255 def test_unicorn_config_listen_augments_cli
256 port2 = unused_port(@addr)
257 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
258 ucfg = Tempfile.new('unicorn_test_config')
259 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
262 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
265 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
266 results = retry_hit(uris)
267 assert_equal results.size, uris.size
268 assert_equal String, results[0].class
269 assert_equal String, results[1].class
273 def test_weird_config_settings
274 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
275 ucfg = Tempfile.new('unicorn_test_config')
276 ucfg.syswrite(HEAVY_CFG)
279 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
283 results = retry_hit(["http://#{@addr}:#{@port}/"])
284 assert_equal String, results[0].class
285 wait_master_ready(COMMON_TMP.path)
286 wait_workers_ready(COMMON_TMP.path, 4)
287 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
288 assert_equal 4, bf.size
289 rotate = Tempfile.new('unicorn_rotate')
290 assert_nothing_raised do
291 File.rename(COMMON_TMP.path, rotate.path)
292 Process.kill(:USR1, pid)
294 wait_for_file(COMMON_TMP.path)
295 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
296 # USR1 should've been passed to all workers
297 tries = DEFAULT_TRIES
298 log = File.readlines(rotate.path)
299 while (tries -= 1) > 0 && log.grep(/rotating logs\.\.\./).size < 4
301 log = File.readlines(rotate.path)
303 assert_equal 4, log.grep(/worker=\d+ rotating logs\.\.\./).size
304 assert_equal 0, log.grep(/done rotating logs/).size
306 tries = DEFAULT_TRIES
307 log = File.readlines(COMMON_TMP.path)
308 while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 4
310 log = File.readlines(COMMON_TMP.path)
312 assert_equal 4, log.grep(/worker=\d+ done rotating logs/).size
313 assert_equal 0, log.grep(/rotating logs\.\.\./).size
314 assert_nothing_raised { Process.kill(:QUIT, pid) }
316 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
317 assert status.success?, "exited successfully"
320 def test_read_embedded_cli_switches
321 File.open("config.ru", "wb") do |fp|
322 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
325 pid = fork { redirect_test_io { exec($unicorn_bin) } }
326 results = retry_hit(["http://#{@addr}:#{@port}/"])
327 assert_equal String, results[0].class
331 def test_config_ru_alt_path
332 config_path = "#{@tmpdir}/foo.ru"
333 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
337 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
340 results = retry_hit(["http://#{@addr}:#{@port}/"])
341 assert_equal String, results[0].class
346 libdir = "#{@tmpdir}/lib"
347 FileUtils.mkpath([ libdir ])
348 config_path = "#{libdir}/hello.rb"
349 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
353 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
356 results = retry_hit(["http://#{@addr}:#{@port}/"])
357 assert_equal String, results[0].class
362 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
363 pid_file = "#{@tmpdir}/test.pid"
366 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
369 reexec_basic_test(pid, pid_file)
372 def test_reexec_alt_config
373 config_file = "#{@tmpdir}/foo.ru"
374 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
375 pid_file = "#{@tmpdir}/test.pid"
378 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
381 reexec_basic_test(pid, pid_file)
384 def test_unicorn_config_file
385 pid_file = "#{@tmpdir}/test.pid"
386 sock = Tempfile.new('unicorn_test_sock')
387 sock_path = sock.path
389 @sockets << sock_path
391 log = Tempfile.new('unicorn_test_log')
392 ucfg = Tempfile.new('unicorn_test_config')
393 ucfg.syswrite("listen \"#{sock_path}\"\n")
394 ucfg.syswrite("pid \"#{pid_file}\"\n")
395 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
398 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
401 exec($unicorn_bin, "-l#{@addr}:#{@port}",
402 "-P#{pid_file}", "-c#{ucfg.path}")
405 results = retry_hit(["http://#{@addr}:#{@port}/"])
406 assert_equal String, results[0].class
407 wait_master_ready(log.path)
408 assert File.exist?(pid_file), "pid_file created"
409 assert_equal pid, File.read(pid_file).to_i
410 assert File.socket?(sock_path), "socket created"
411 assert_nothing_raised do
412 sock = UNIXSocket.new(sock_path)
413 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
414 results = sock.sysread(4096)
416 assert_equal String, results.class
418 # try reloading the config
419 sock = Tempfile.new('unicorn_test_sock')
420 new_sock_path = sock.path
421 @sockets << new_sock_path
423 new_log = Tempfile.new('unicorn_test_log')
425 assert_equal 0, new_log.size
427 assert_nothing_raised do
428 ucfg = File.open(ucfg.path, "wb")
429 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
430 ucfg.syswrite("pid \"#{pid_file}\"\n")
431 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
433 Process.kill(:HUP, pid)
436 wait_for_file(new_sock_path)
437 assert File.socket?(new_sock_path), "socket exists"
438 @sockets.each do |path|
439 assert_nothing_raised do
440 sock = UNIXSocket.new(path)
441 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
442 results = sock.sysread(4096)
444 assert_equal String, results.class
447 assert_not_equal 0, new_log.size
448 reexec_usr2_quit_test(pid, pid_file)
451 def test_daemonize_reexec
452 pid_file = "#{@tmpdir}/test.pid"
453 log = Tempfile.new('unicorn_test_log')
454 ucfg = Tempfile.new('unicorn_test_config')
455 ucfg.syswrite("pid \"#{pid_file}\"\n")
456 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
459 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
462 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
465 results = retry_hit(["http://#{@addr}:#{@port}/"])
466 assert_equal String, results[0].class
467 wait_for_file(pid_file)
468 new_pid = File.read(pid_file).to_i
469 assert_not_equal pid, new_pid
470 pid, status = Process.waitpid2(pid)
471 assert status.success?, "original process exited successfully"
472 assert_nothing_raised { Process.kill(0, new_pid) }
473 reexec_usr2_quit_test(new_pid, pid_file)
478 # sometimes the server may not come up right away
479 def retry_hit(uris = [])
480 tries = DEFAULT_TRIES
483 rescue Errno::ECONNREFUSED => err
492 def assert_shutdown(pid)
493 wait_master_ready("#{@tmpdir}/test_stderr.#{pid}.log")
494 assert_nothing_raised { Process.kill(:QUIT, pid) }
496 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
497 assert status.success?, "exited successfully"
500 def wait_workers_ready(path, nr_workers)
501 tries = DEFAULT_TRIES
503 while (tries -= 1) > 0
505 lines = File.readlines(path).grep(/worker=\d+ ready/)
506 lines.size == nr_workers and return
511 raise "#{nr_workers} workers never became ready:" \
512 "\n\t#{lines.join("\n\t")}\n"
515 def wait_master_ready(master_log)
516 tries = DEFAULT_TRIES
517 while (tries -= 1) > 0
519 File.readlines(master_log).grep(/master process ready/)[0] and return
524 raise "master process never became ready"
527 def reexec_usr2_quit_test(pid, pid_file)
528 assert File.exist?(pid_file), "pid file OK"
529 assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
530 assert_nothing_raised { Process.kill(:USR2, pid) }
531 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
532 wait_for_file("#{pid_file}.oldbin")
533 wait_for_file(pid_file)
535 # kill old master process
536 assert_not_equal pid, File.read(pid_file).to_i
537 assert_equal pid, File.read("#{pid_file}.oldbin").to_i
538 assert_nothing_raised { Process.kill(:QUIT, pid) }
539 assert_not_equal pid, File.read(pid_file).to_i
540 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
541 wait_for_file(pid_file)
542 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
543 assert_nothing_raised { Process.kill(:QUIT, File.read(pid_file).to_i) }
546 def reexec_basic_test(pid, pid_file)
547 results = retry_hit(["http://#{@addr}:#{@port}/"])
548 assert_equal String, results[0].class
549 assert_nothing_raised { Process.kill(0, pid) }
550 master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
551 wait_master_ready(master_log)
552 File.truncate(master_log, 0)
555 assert_nothing_raised do
557 hit(["http://#{@addr}:#{@port}/#{i}"])
558 i == kill_point and Process.kill(:HUP, pid)
561 wait_master_ready(master_log)
562 assert File.exist?(pid_file), "pid=#{pid_file} exists"
563 new_pid = File.read(pid_file).to_i
564 assert_not_equal pid, new_pid
565 assert_nothing_raised { Process.kill(0, new_pid) }
566 assert_nothing_raised { Process.kill(:QUIT, new_pid) }
569 def wait_for_file(path)
570 tries = DEFAULT_TRIES
571 while (tries -= 1) > 0 && ! File.exist?(path)
574 assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
579 ObjectSpace.each_object(Tempfile) do |tmp|
580 ObjectSpace.undefine_finalizer(tmp)