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")
81 assert_nothing_raised do
82 Process.kill(sig, pid)
83 pid, status = Process.waitpid2(pid)
85 reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
86 assert_equal 1, reaped.size
92 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
94 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
96 results = retry_hit(["http://#{@addr}:#{@port}/"])
97 assert_equal String, results[0].class
102 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
103 pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
104 log = "test_stderr.#{pid}.log"
105 wait_master_ready(log)
107 assert_nothing_raised { Process.kill(:TTIN, pid) }
108 wait_workers_ready(log, i)
110 File.truncate(log, 0)
112 [ 2, 1, 0].each { |i|
113 assert_nothing_raised { Process.kill(:TTOU, pid) }
114 DEFAULT_TRIES.times {
116 reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
117 break if reaped.size == 1
119 assert_equal 1, reaped.size
125 assert(system($unicorn_bin, "-h"), "help text returns true")
127 assert_equal 0, File.stat("test_stderr.#$$.log").size
128 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
129 lines = File.readlines("test_stdout.#$$.log")
131 # Be considerate of the on-call technician working from their
132 # mobile phone or netbook on a slow connection :)
133 assert lines.size <= 24, "help height fits in an ANSI terminal window"
135 assert line.size <= 80, "help width fits in an ANSI terminal window"
139 def test_broken_reexec_config
140 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
141 pid_file = "#{@tmpdir}/test.pid"
142 old_file = "#{pid_file}.oldbin"
143 ucfg = Tempfile.new('unicorn_test_config')
144 ucfg.syswrite("listen %(#@addr:#@port)\n")
145 ucfg.syswrite("pid %(#{pid_file})\n")
146 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
149 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
152 results = retry_hit(["http://#{@addr}:#{@port}/"])
153 assert_equal String, results[0].class
155 wait_for_file(pid_file)
157 Process.kill(:USR2, File.read(pid_file).to_i)
158 wait_for_file(old_file)
159 wait_for_file(pid_file)
160 old_pid = File.read(old_file).to_i
161 Process.kill(:QUIT, old_pid)
162 wait_for_death(old_pid)
164 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
165 current_pid = File.read(pid_file).to_i
166 Process.kill(:USR2, current_pid)
168 # wait for pid_file to restore itself
169 tries = DEFAULT_TRIES
171 while current_pid != File.read(pid_file).to_i
172 sleep(DEFAULT_RES) and (tries -= 1) > 0
175 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
177 assert_equal current_pid, File.read(pid_file).to_i
179 tries = DEFAULT_TRIES
180 while File.exist?(old_file)
181 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
183 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
184 port2 = unused_port(@addr)
189 ucfg.syswrite("listen %(#@addr:#@port)\n")
190 ucfg.syswrite("listen %(#@addr:#{port2})\n")
191 ucfg.syswrite("pid %(#{pid_file})\n")
192 assert_nothing_raised { Process.kill(:USR2, current_pid) }
194 wait_for_file(old_file)
195 wait_for_file(pid_file)
196 new_pid = File.read(pid_file).to_i
197 assert_not_equal current_pid, new_pid
198 assert_equal current_pid, File.read(old_file).to_i
199 results = retry_hit(["http://#{@addr}:#{@port}/",
200 "http://#{@addr}:#{port2}/"])
201 assert_equal String, results[0].class
202 assert_equal String, results[1].class
204 assert_nothing_raised do
205 Process.kill(:QUIT, current_pid)
206 Process.kill(:QUIT, new_pid)
210 def test_broken_reexec_ru
211 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
212 pid_file = "#{@tmpdir}/test.pid"
213 old_file = "#{pid_file}.oldbin"
214 ucfg = Tempfile.new('unicorn_test_config')
215 ucfg.syswrite("pid %(#{pid_file})\n")
216 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
219 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
222 results = retry_hit(["http://#{@addr}:#{@port}/"])
223 assert_equal String, results[0].class
225 wait_for_file(pid_file)
227 Process.kill(:USR2, File.read(pid_file).to_i)
228 wait_for_file(old_file)
229 wait_for_file(pid_file)
230 old_pid = File.read(old_file).to_i
231 Process.kill(:QUIT, old_pid)
232 wait_for_death(old_pid)
234 File.unlink("config.ru") # break reloading
235 current_pid = File.read(pid_file).to_i
236 Process.kill(:USR2, current_pid)
238 # wait for pid_file to restore itself
239 tries = DEFAULT_TRIES
241 while current_pid != File.read(pid_file).to_i
242 sleep(DEFAULT_RES) and (tries -= 1) > 0
245 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
248 tries = DEFAULT_TRIES
249 while File.exist?(old_file)
250 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
252 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
253 assert_equal current_pid, File.read(pid_file).to_i
256 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
257 assert_nothing_raised { Process.kill(:USR2, current_pid) }
258 wait_for_file(old_file)
259 wait_for_file(pid_file)
260 new_pid = File.read(pid_file).to_i
261 assert_not_equal current_pid, new_pid
262 assert_equal current_pid, File.read(old_file).to_i
263 results = retry_hit(["http://#{@addr}:#{@port}/"])
264 assert_equal String, results[0].class
266 assert_nothing_raised do
267 Process.kill(:QUIT, current_pid)
268 Process.kill(:QUIT, new_pid)
272 def test_unicorn_config_listener_swap
273 port_cli = unused_port
274 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
275 ucfg = Tempfile.new('unicorn_test_config')
276 ucfg.syswrite("listen '#@addr:#@port'\n")
279 exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
282 results = retry_hit(["http://#@addr:#{port_cli}/"])
283 assert_equal String, results[0].class
284 results = retry_hit(["http://#@addr:#@port/"])
285 assert_equal String, results[0].class
287 port2 = unused_port(@addr)
290 ucfg.syswrite("listen '#@addr:#{port2}'\n")
291 Process.kill(:HUP, pid)
293 results = retry_hit(["http://#@addr:#{port2}/"])
294 assert_equal String, results[0].class
295 results = retry_hit(["http://#@addr:#{port_cli}/"])
296 assert_equal String, results[0].class
297 assert_nothing_raised do
298 reuse = TCPServer.new(@addr, @port)
304 def test_unicorn_config_listen_with_options
305 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
306 ucfg = Tempfile.new('unicorn_test_config')
307 ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
308 ucfg.syswrite(" :rcvbuf => 4096,\n")
309 ucfg.syswrite(" :sndbuf => 4096\n")
311 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
313 results = retry_hit(["http://#{@addr}:#{@port}/"])
314 assert_equal String, results[0].class
318 def test_unicorn_config_per_worker_listen
320 pid_spit = 'use Rack::ContentLength;' \
321 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
322 File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
323 tmp = Tempfile.new('test.socket')
324 File.unlink(tmp.path)
325 ucfg = Tempfile.new('unicorn_test_config')
326 ucfg.syswrite("listen '#@addr:#@port'\n")
327 ucfg.syswrite("before_fork { |s,w|\n")
328 ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
329 ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
330 ucfg.syswrite("\n}\n")
332 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
334 results = retry_hit(["http://#{@addr}:#{@port}/"])
335 assert_equal String, results[0].class
336 worker_pid = results[0].to_i
337 assert_not_equal pid, worker_pid
338 s = UNIXSocket.new(tmp.path)
339 s.syswrite("GET / HTTP/1.0\r\n\r\n")
341 loop { results << s.sysread(4096) } rescue nil
342 assert_nothing_raised { s.close }
343 assert_equal worker_pid, results.split(/\r\n/).last.to_i
344 results = hit(["http://#@addr:#{port2}/"])
345 assert_equal String, results[0].class
346 assert_equal worker_pid, results[0].to_i
350 def test_unicorn_config_listen_augments_cli
351 port2 = unused_port(@addr)
352 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
353 ucfg = Tempfile.new('unicorn_test_config')
354 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
357 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
360 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
361 results = retry_hit(uris)
362 assert_equal results.size, uris.size
363 assert_equal String, results[0].class
364 assert_equal String, results[1].class
368 def test_weird_config_settings
369 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
370 ucfg = Tempfile.new('unicorn_test_config')
371 ucfg.syswrite(HEAVY_CFG)
374 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
378 results = retry_hit(["http://#{@addr}:#{@port}/"])
379 assert_equal String, results[0].class
380 wait_master_ready(COMMON_TMP.path)
381 wait_workers_ready(COMMON_TMP.path, 4)
382 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
383 assert_equal 4, bf.size
384 rotate = Tempfile.new('unicorn_rotate')
385 assert_nothing_raised do
386 File.rename(COMMON_TMP.path, rotate.path)
387 Process.kill(:USR1, pid)
389 wait_for_file(COMMON_TMP.path)
390 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
391 # USR1 should've been passed to all workers
392 tries = DEFAULT_TRIES
393 log = File.readlines(rotate.path)
394 while (tries -= 1) > 0 &&
395 log.grep(/reopening logs\.\.\./).size < 5
397 log = File.readlines(rotate.path)
399 assert_equal 5, log.grep(/reopening logs\.\.\./).size
400 assert_equal 0, log.grep(/done reopening logs/).size
402 tries = DEFAULT_TRIES
403 log = File.readlines(COMMON_TMP.path)
404 while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
406 log = File.readlines(COMMON_TMP.path)
408 assert_equal 5, log.grep(/done reopening logs/).size
409 assert_equal 0, log.grep(/reopening logs\.\.\./).size
410 assert_nothing_raised { Process.kill(:QUIT, pid) }
412 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
413 assert status.success?, "exited successfully"
416 def test_read_embedded_cli_switches
417 File.open("config.ru", "wb") do |fp|
418 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
421 pid = fork { redirect_test_io { exec($unicorn_bin) } }
422 results = retry_hit(["http://#{@addr}:#{@port}/"])
423 assert_equal String, results[0].class
427 def test_config_ru_alt_path
428 config_path = "#{@tmpdir}/foo.ru"
429 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
433 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
436 results = retry_hit(["http://#{@addr}:#{@port}/"])
437 assert_equal String, results[0].class
442 libdir = "#{@tmpdir}/lib"
443 FileUtils.mkpath([ libdir ])
444 config_path = "#{libdir}/hello.rb"
445 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
449 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
452 results = retry_hit(["http://#{@addr}:#{@port}/"])
453 assert_equal String, results[0].class
458 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
459 pid_file = "#{@tmpdir}/test.pid"
462 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
465 reexec_basic_test(pid, pid_file)
468 def test_reexec_alt_config
469 config_file = "#{@tmpdir}/foo.ru"
470 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
471 pid_file = "#{@tmpdir}/test.pid"
474 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
477 reexec_basic_test(pid, pid_file)
480 def test_socket_unlinked_restore
482 sock = Tempfile.new('unicorn_test_sock')
483 sock_path = sock.path
484 @sockets << sock_path
486 ucfg = Tempfile.new('unicorn_test_config')
487 ucfg.syswrite("listen \"#{sock_path}\"\n")
489 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
490 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
491 wait_for_file(sock_path)
492 assert File.socket?(sock_path)
493 assert_nothing_raised do
494 sock = UNIXSocket.new(sock_path)
495 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
496 results = sock.sysread(4096)
498 assert_equal String, results.class
499 assert_nothing_raised do
500 File.unlink(sock_path)
501 Process.kill(:HUP, pid)
503 wait_for_file(sock_path)
504 assert File.socket?(sock_path)
505 assert_nothing_raised do
506 sock = UNIXSocket.new(sock_path)
507 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
508 results = sock.sysread(4096)
510 assert_equal String, results.class
513 def test_unicorn_config_file
514 pid_file = "#{@tmpdir}/test.pid"
515 sock = Tempfile.new('unicorn_test_sock')
516 sock_path = sock.path
518 @sockets << sock_path
520 log = Tempfile.new('unicorn_test_log')
521 ucfg = Tempfile.new('unicorn_test_config')
522 ucfg.syswrite("listen \"#{sock_path}\"\n")
523 ucfg.syswrite("pid \"#{pid_file}\"\n")
524 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
527 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
530 exec($unicorn_bin, "-l#{@addr}:#{@port}",
531 "-P#{pid_file}", "-c#{ucfg.path}")
534 results = retry_hit(["http://#{@addr}:#{@port}/"])
535 assert_equal String, results[0].class
536 wait_master_ready(log.path)
537 assert File.exist?(pid_file), "pid_file created"
538 assert_equal pid, File.read(pid_file).to_i
539 assert File.socket?(sock_path), "socket created"
540 assert_nothing_raised do
541 sock = UNIXSocket.new(sock_path)
542 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
543 results = sock.sysread(4096)
545 assert_equal String, results.class
547 # try reloading the config
548 sock = Tempfile.new('new_test_sock')
549 new_sock_path = sock.path
550 @sockets << new_sock_path
552 new_log = Tempfile.new('unicorn_test_log')
554 assert_equal 0, new_log.size
556 assert_nothing_raised do
557 ucfg = File.open(ucfg.path, "wb")
558 ucfg.syswrite("listen \"#{sock_path}\"\n")
559 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
560 ucfg.syswrite("pid \"#{pid_file}\"\n")
561 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
563 Process.kill(:HUP, pid)
566 wait_for_file(new_sock_path)
567 assert File.socket?(new_sock_path), "socket exists"
568 @sockets.each do |path|
569 assert_nothing_raised do
570 sock = UNIXSocket.new(path)
571 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
572 results = sock.sysread(4096)
574 assert_equal String, results.class
577 assert_not_equal 0, new_log.size
578 reexec_usr2_quit_test(pid, pid_file)
581 def test_daemonize_reexec
582 pid_file = "#{@tmpdir}/test.pid"
583 log = Tempfile.new('unicorn_test_log')
584 ucfg = Tempfile.new('unicorn_test_config')
585 ucfg.syswrite("pid \"#{pid_file}\"\n")
586 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
589 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
592 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
595 results = retry_hit(["http://#{@addr}:#{@port}/"])
596 assert_equal String, results[0].class
597 wait_for_file(pid_file)
598 new_pid = File.read(pid_file).to_i
599 assert_not_equal pid, new_pid
600 pid, status = Process.waitpid2(pid)
601 assert status.success?, "original process exited successfully"
602 assert_nothing_raised { Process.kill(0, new_pid) }
603 reexec_usr2_quit_test(new_pid, pid_file)
606 def test_reexec_fd_leak
607 unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
608 warn "FD leak test only works on Linux at the moment"
611 pid_file = "#{@tmpdir}/test.pid"
612 log = Tempfile.new('unicorn_test_log')
614 ucfg = Tempfile.new('unicorn_test_config')
615 ucfg.syswrite("pid \"#{pid_file}\"\n")
616 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
617 ucfg.syswrite("stderr_path '#{log.path}'\n")
618 ucfg.syswrite("stdout_path '#{log.path}'\n")
621 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
624 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
628 wait_master_ready(log.path)
629 File.truncate(log.path, 0)
630 wait_for_file(pid_file)
631 orig_pid = pid = File.read(pid_file).to_i
632 orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
634 expect_size = orig_fds.size
636 assert_nothing_raised do
637 Process.kill(:USR2, pid)
638 wait_for_file("#{pid_file}.oldbin")
639 Process.kill(:QUIT, pid)
643 wait_master_ready(log.path)
644 File.truncate(log.path, 0)
645 wait_for_file(pid_file)
646 pid = File.read(pid_file).to_i
647 assert_not_equal orig_pid, pid
648 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
651 # we could've inherited descriptors the first time around
652 assert expect_size >= curr_fds.size, curr_fds.inspect
653 expect_size = curr_fds.size
655 assert_nothing_raised do
656 Process.kill(:USR2, pid)
657 wait_for_file("#{pid_file}.oldbin")
658 Process.kill(:QUIT, pid)
662 wait_master_ready(log.path)
663 File.truncate(log.path, 0)
664 wait_for_file(pid_file)
665 pid = File.read(pid_file).to_i
666 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
668 assert_equal expect_size, curr_fds.size, curr_fds.inspect
670 Process.kill(:QUIT, pid)