1 # Copyright (c) 2009 Eric Wong
2 require 'test/test_helper'
5 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
7 do_test = system($unicorn_bin, '-v')
11 warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
15 unless try_require('rack')
16 warn "Unable to load Rack, skipping this test"
20 class ExecTest < Test::Unit::TestCase
24 use Rack::ContentLength
25 run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] }
31 [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ]
36 COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
41 logger Logger.new('#{COMMON_TMP.path}')
42 before_fork do |server, worker|
43 server.logger.info "before_fork: worker=\#{worker.nr}"
49 @tmpfile = Tempfile.new('unicorn_exec_test')
50 @tmpdir = @tmpfile.path
54 @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
55 @port = unused_port(@addr)
61 return if @start_pid != $$
63 FileUtils.rmtree(@tmpdir)
64 @sockets.each { |path| File.unlink(path) rescue nil }
66 Process.kill('-QUIT', 0)
68 Process.waitpid(-1, Process::WNOHANG) or break
76 %w(INT TERM QUIT).each do |sig|
77 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
78 pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
79 wait_master_ready("test_stderr.#{pid}.log")
80 wait_workers_ready("test_stderr.#{pid}.log", 1)
82 assert_nothing_raised do
83 Process.kill(sig, pid)
84 pid, status = Process.waitpid2(pid)
86 reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
87 assert_equal 1, reaped.size
93 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
95 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
97 results = retry_hit(["http://#{@addr}:#{@port}/"])
98 assert_equal String, results[0].class
103 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
104 pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
105 log = "test_stderr.#{pid}.log"
106 wait_master_ready(log)
108 assert_nothing_raised { Process.kill(:TTIN, pid) }
109 wait_workers_ready(log, i)
111 File.truncate(log, 0)
113 [ 2, 1, 0].each { |i|
114 assert_nothing_raised { Process.kill(:TTOU, pid) }
115 DEFAULT_TRIES.times {
117 reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
118 break if reaped.size == 1
120 assert_equal 1, reaped.size
126 assert(system($unicorn_bin, "-h"), "help text returns true")
128 assert_equal 0, File.stat("test_stderr.#$$.log").size
129 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
130 lines = File.readlines("test_stdout.#$$.log")
132 # Be considerate of the on-call technician working from their
133 # mobile phone or netbook on a slow connection :)
134 assert lines.size <= 24, "help height fits in an ANSI terminal window"
136 assert line.size <= 80, "help width fits in an ANSI terminal window"
140 def test_broken_reexec_config
141 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
142 pid_file = "#{@tmpdir}/test.pid"
143 old_file = "#{pid_file}.oldbin"
144 ucfg = Tempfile.new('unicorn_test_config')
145 ucfg.syswrite("listen %(#@addr:#@port)\n")
146 ucfg.syswrite("pid %(#{pid_file})\n")
147 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
150 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
153 results = retry_hit(["http://#{@addr}:#{@port}/"])
154 assert_equal String, results[0].class
156 wait_for_file(pid_file)
158 Process.kill(:USR2, File.read(pid_file).to_i)
159 wait_for_file(old_file)
160 wait_for_file(pid_file)
161 old_pid = File.read(old_file).to_i
162 Process.kill(:QUIT, old_pid)
163 wait_for_death(old_pid)
165 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
166 current_pid = File.read(pid_file).to_i
167 Process.kill(:USR2, current_pid)
169 # wait for pid_file to restore itself
170 tries = DEFAULT_TRIES
172 while current_pid != File.read(pid_file).to_i
173 sleep(DEFAULT_RES) and (tries -= 1) > 0
176 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
178 assert_equal current_pid, File.read(pid_file).to_i
180 tries = DEFAULT_TRIES
181 while File.exist?(old_file)
182 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
184 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
185 port2 = unused_port(@addr)
190 ucfg.syswrite("listen %(#@addr:#@port)\n")
191 ucfg.syswrite("listen %(#@addr:#{port2})\n")
192 ucfg.syswrite("pid %(#{pid_file})\n")
193 assert_nothing_raised { Process.kill(:USR2, current_pid) }
195 wait_for_file(old_file)
196 wait_for_file(pid_file)
197 new_pid = File.read(pid_file).to_i
198 assert_not_equal current_pid, new_pid
199 assert_equal current_pid, File.read(old_file).to_i
200 results = retry_hit(["http://#{@addr}:#{@port}/",
201 "http://#{@addr}:#{port2}/"])
202 assert_equal String, results[0].class
203 assert_equal String, results[1].class
205 assert_nothing_raised do
206 Process.kill(:QUIT, current_pid)
207 Process.kill(:QUIT, new_pid)
211 def test_broken_reexec_ru
212 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
213 pid_file = "#{@tmpdir}/test.pid"
214 old_file = "#{pid_file}.oldbin"
215 ucfg = Tempfile.new('unicorn_test_config')
216 ucfg.syswrite("pid %(#{pid_file})\n")
217 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
220 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
223 results = retry_hit(["http://#{@addr}:#{@port}/"])
224 assert_equal String, results[0].class
226 wait_for_file(pid_file)
228 Process.kill(:USR2, File.read(pid_file).to_i)
229 wait_for_file(old_file)
230 wait_for_file(pid_file)
231 old_pid = File.read(old_file).to_i
232 Process.kill(:QUIT, old_pid)
233 wait_for_death(old_pid)
235 File.unlink("config.ru") # break reloading
236 current_pid = File.read(pid_file).to_i
237 Process.kill(:USR2, current_pid)
239 # wait for pid_file to restore itself
240 tries = DEFAULT_TRIES
242 while current_pid != File.read(pid_file).to_i
243 sleep(DEFAULT_RES) and (tries -= 1) > 0
246 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
249 tries = DEFAULT_TRIES
250 while File.exist?(old_file)
251 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
253 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
254 assert_equal current_pid, File.read(pid_file).to_i
257 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
258 assert_nothing_raised { Process.kill(:USR2, current_pid) }
259 wait_for_file(old_file)
260 wait_for_file(pid_file)
261 new_pid = File.read(pid_file).to_i
262 assert_not_equal current_pid, new_pid
263 assert_equal current_pid, File.read(old_file).to_i
264 results = retry_hit(["http://#{@addr}:#{@port}/"])
265 assert_equal String, results[0].class
267 assert_nothing_raised do
268 Process.kill(:QUIT, current_pid)
269 Process.kill(:QUIT, new_pid)
273 def test_unicorn_config_listener_swap
274 port_cli = unused_port
275 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
276 ucfg = Tempfile.new('unicorn_test_config')
277 ucfg.syswrite("listen '#@addr:#@port'\n")
280 exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
283 results = retry_hit(["http://#@addr:#{port_cli}/"])
284 assert_equal String, results[0].class
285 results = retry_hit(["http://#@addr:#@port/"])
286 assert_equal String, results[0].class
288 port2 = unused_port(@addr)
291 ucfg.syswrite("listen '#@addr:#{port2}'\n")
292 Process.kill(:HUP, pid)
294 results = retry_hit(["http://#@addr:#{port2}/"])
295 assert_equal String, results[0].class
296 results = retry_hit(["http://#@addr:#{port_cli}/"])
297 assert_equal String, results[0].class
298 assert_nothing_raised do
299 reuse = TCPServer.new(@addr, @port)
305 def test_unicorn_config_listen_with_options
306 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
307 ucfg = Tempfile.new('unicorn_test_config')
308 ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
309 ucfg.syswrite(" :rcvbuf => 4096,\n")
310 ucfg.syswrite(" :sndbuf => 4096\n")
312 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
314 results = retry_hit(["http://#{@addr}:#{@port}/"])
315 assert_equal String, results[0].class
319 def test_unicorn_config_per_worker_listen
321 pid_spit = 'use Rack::ContentLength;' \
322 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
323 File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
324 tmp = Tempfile.new('test.socket')
325 File.unlink(tmp.path)
326 ucfg = Tempfile.new('unicorn_test_config')
327 ucfg.syswrite("listen '#@addr:#@port'\n")
328 ucfg.syswrite("before_fork { |s,w|\n")
329 ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
330 ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
331 ucfg.syswrite("\n}\n")
333 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
335 results = retry_hit(["http://#{@addr}:#{@port}/"])
336 assert_equal String, results[0].class
337 worker_pid = results[0].to_i
338 assert_not_equal pid, worker_pid
339 s = UNIXSocket.new(tmp.path)
340 s.syswrite("GET / HTTP/1.0\r\n\r\n")
342 loop { results << s.sysread(4096) } rescue nil
343 assert_nothing_raised { s.close }
344 assert_equal worker_pid, results.split(/\r\n/).last.to_i
345 results = hit(["http://#@addr:#{port2}/"])
346 assert_equal String, results[0].class
347 assert_equal worker_pid, results[0].to_i
351 def test_unicorn_config_listen_augments_cli
352 port2 = unused_port(@addr)
353 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
354 ucfg = Tempfile.new('unicorn_test_config')
355 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
358 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
361 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
362 results = retry_hit(uris)
363 assert_equal results.size, uris.size
364 assert_equal String, results[0].class
365 assert_equal String, results[1].class
369 def test_weird_config_settings
370 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
371 ucfg = Tempfile.new('unicorn_test_config')
372 ucfg.syswrite(HEAVY_CFG)
375 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
379 results = retry_hit(["http://#{@addr}:#{@port}/"])
380 assert_equal String, results[0].class
381 wait_master_ready(COMMON_TMP.path)
382 wait_workers_ready(COMMON_TMP.path, 4)
383 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
384 assert_equal 4, bf.size
385 rotate = Tempfile.new('unicorn_rotate')
386 assert_nothing_raised do
387 File.rename(COMMON_TMP.path, rotate.path)
388 Process.kill(:USR1, pid)
390 wait_for_file(COMMON_TMP.path)
391 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
392 # USR1 should've been passed to all workers
393 tries = DEFAULT_TRIES
394 log = File.readlines(rotate.path)
395 while (tries -= 1) > 0 &&
396 log.grep(/reopening logs\.\.\./).size < 5
398 log = File.readlines(rotate.path)
400 assert_equal 5, log.grep(/reopening logs\.\.\./).size
401 assert_equal 0, log.grep(/done reopening logs/).size
403 tries = DEFAULT_TRIES
404 log = File.readlines(COMMON_TMP.path)
405 while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
407 log = File.readlines(COMMON_TMP.path)
409 assert_equal 5, log.grep(/done reopening logs/).size
410 assert_equal 0, log.grep(/reopening logs\.\.\./).size
411 assert_nothing_raised { Process.kill(:QUIT, pid) }
413 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
414 assert status.success?, "exited successfully"
417 def test_read_embedded_cli_switches
418 File.open("config.ru", "wb") do |fp|
419 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
422 pid = fork { redirect_test_io { exec($unicorn_bin) } }
423 results = retry_hit(["http://#{@addr}:#{@port}/"])
424 assert_equal String, results[0].class
428 def test_config_ru_alt_path
429 config_path = "#{@tmpdir}/foo.ru"
430 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
434 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
437 results = retry_hit(["http://#{@addr}:#{@port}/"])
438 assert_equal String, results[0].class
443 libdir = "#{@tmpdir}/lib"
444 FileUtils.mkpath([ libdir ])
445 config_path = "#{libdir}/hello.rb"
446 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
450 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
453 results = retry_hit(["http://#{@addr}:#{@port}/"])
454 assert_equal String, results[0].class
459 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
460 pid_file = "#{@tmpdir}/test.pid"
463 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
466 reexec_basic_test(pid, pid_file)
469 def test_reexec_alt_config
470 config_file = "#{@tmpdir}/foo.ru"
471 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
472 pid_file = "#{@tmpdir}/test.pid"
475 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
478 reexec_basic_test(pid, pid_file)
481 def test_socket_unlinked_restore
483 sock = Tempfile.new('unicorn_test_sock')
484 sock_path = sock.path
485 @sockets << sock_path
487 ucfg = Tempfile.new('unicorn_test_config')
488 ucfg.syswrite("listen \"#{sock_path}\"\n")
490 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
491 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
492 wait_for_file(sock_path)
493 assert File.socket?(sock_path)
494 assert_nothing_raised do
495 sock = UNIXSocket.new(sock_path)
496 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
497 results = sock.sysread(4096)
499 assert_equal String, results.class
500 assert_nothing_raised do
501 File.unlink(sock_path)
502 Process.kill(:HUP, pid)
504 wait_for_file(sock_path)
505 assert File.socket?(sock_path)
506 assert_nothing_raised do
507 sock = UNIXSocket.new(sock_path)
508 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
509 results = sock.sysread(4096)
511 assert_equal String, results.class
514 def test_unicorn_config_file
515 pid_file = "#{@tmpdir}/test.pid"
516 sock = Tempfile.new('unicorn_test_sock')
517 sock_path = sock.path
519 @sockets << sock_path
521 log = Tempfile.new('unicorn_test_log')
522 ucfg = Tempfile.new('unicorn_test_config')
523 ucfg.syswrite("listen \"#{sock_path}\"\n")
524 ucfg.syswrite("pid \"#{pid_file}\"\n")
525 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
528 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
531 exec($unicorn_bin, "-l#{@addr}:#{@port}",
532 "-P#{pid_file}", "-c#{ucfg.path}")
535 results = retry_hit(["http://#{@addr}:#{@port}/"])
536 assert_equal String, results[0].class
537 wait_master_ready(log.path)
538 assert File.exist?(pid_file), "pid_file created"
539 assert_equal pid, File.read(pid_file).to_i
540 assert File.socket?(sock_path), "socket created"
541 assert_nothing_raised do
542 sock = UNIXSocket.new(sock_path)
543 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
544 results = sock.sysread(4096)
546 assert_equal String, results.class
548 # try reloading the config
549 sock = Tempfile.new('new_test_sock')
550 new_sock_path = sock.path
551 @sockets << new_sock_path
553 new_log = Tempfile.new('unicorn_test_log')
555 assert_equal 0, new_log.size
557 assert_nothing_raised do
558 ucfg = File.open(ucfg.path, "wb")
559 ucfg.syswrite("listen \"#{sock_path}\"\n")
560 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
561 ucfg.syswrite("pid \"#{pid_file}\"\n")
562 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
564 Process.kill(:HUP, pid)
567 wait_for_file(new_sock_path)
568 assert File.socket?(new_sock_path), "socket exists"
569 @sockets.each do |path|
570 assert_nothing_raised do
571 sock = UNIXSocket.new(path)
572 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
573 results = sock.sysread(4096)
575 assert_equal String, results.class
578 assert_not_equal 0, new_log.size
579 reexec_usr2_quit_test(pid, pid_file)
582 def test_daemonize_reexec
583 pid_file = "#{@tmpdir}/test.pid"
584 log = Tempfile.new('unicorn_test_log')
585 ucfg = Tempfile.new('unicorn_test_config')
586 ucfg.syswrite("pid \"#{pid_file}\"\n")
587 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
590 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
593 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
596 results = retry_hit(["http://#{@addr}:#{@port}/"])
597 assert_equal String, results[0].class
598 wait_for_file(pid_file)
599 new_pid = File.read(pid_file).to_i
600 assert_not_equal pid, new_pid
601 pid, status = Process.waitpid2(pid)
602 assert status.success?, "original process exited successfully"
603 assert_nothing_raised { Process.kill(0, new_pid) }
604 reexec_usr2_quit_test(new_pid, pid_file)
607 def test_reexec_fd_leak
608 unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
609 warn "FD leak test only works on Linux at the moment"
612 pid_file = "#{@tmpdir}/test.pid"
613 log = Tempfile.new('unicorn_test_log')
615 ucfg = Tempfile.new('unicorn_test_config')
616 ucfg.syswrite("pid \"#{pid_file}\"\n")
617 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
618 ucfg.syswrite("stderr_path '#{log.path}'\n")
619 ucfg.syswrite("stdout_path '#{log.path}'\n")
622 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
625 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
629 wait_master_ready(log.path)
630 wait_workers_ready(log.path, 1)
631 File.truncate(log.path, 0)
632 wait_for_file(pid_file)
633 orig_pid = pid = File.read(pid_file).to_i
634 orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
636 expect_size = orig_fds.size
638 assert_nothing_raised do
639 Process.kill(:USR2, pid)
640 wait_for_file("#{pid_file}.oldbin")
641 Process.kill(:QUIT, pid)
645 wait_master_ready(log.path)
646 wait_workers_ready(log.path, 1)
647 File.truncate(log.path, 0)
648 wait_for_file(pid_file)
649 pid = File.read(pid_file).to_i
650 assert_not_equal orig_pid, pid
651 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
654 # we could've inherited descriptors the first time around
655 assert expect_size >= curr_fds.size, curr_fds.inspect
656 expect_size = curr_fds.size
658 assert_nothing_raised do
659 Process.kill(:USR2, pid)
660 wait_for_file("#{pid_file}.oldbin")
661 Process.kill(:QUIT, pid)
665 wait_master_ready(log.path)
666 wait_workers_ready(log.path, 1)
667 File.truncate(log.path, 0)
668 wait_for_file(pid_file)
669 pid = File.read(pid_file).to_i
670 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
672 assert_equal expect_size, curr_fds.size, curr_fds.inspect
674 Process.kill(:QUIT, pid)