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
103 assert(system($unicorn_bin, "-h"), "help text returns true")
105 assert_equal 0, File.stat("test_stderr.#$$.log").size
106 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
107 lines = File.readlines("test_stdout.#$$.log")
109 # Be considerate of the on-call technician working from their
110 # mobile phone or netbook on a slow connection :)
111 assert lines.size <= 24, "help height fits in an ANSI terminal window"
113 assert line.size <= 80, "help width fits in an ANSI terminal window"
117 def test_broken_reexec_config
118 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
119 pid_file = "#{@tmpdir}/test.pid"
120 old_file = "#{pid_file}.oldbin"
121 ucfg = Tempfile.new('unicorn_test_config')
122 ucfg.syswrite("listen %(#@addr:#@port)\n")
123 ucfg.syswrite("pid %(#{pid_file})\n")
124 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
127 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
130 results = retry_hit(["http://#{@addr}:#{@port}/"])
131 assert_equal String, results[0].class
133 wait_for_file(pid_file)
135 Process.kill(:USR2, File.read(pid_file).to_i)
136 wait_for_file(old_file)
137 wait_for_file(pid_file)
138 old_pid = File.read(old_file).to_i
139 Process.kill(:QUIT, old_pid)
140 wait_for_death(old_pid)
142 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
143 current_pid = File.read(pid_file).to_i
144 Process.kill(:USR2, current_pid)
146 # wait for pid_file to restore itself
147 tries = DEFAULT_TRIES
149 while current_pid != File.read(pid_file).to_i
150 sleep(DEFAULT_RES) and (tries -= 1) > 0
153 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
155 assert_equal current_pid, File.read(pid_file).to_i
157 tries = DEFAULT_TRIES
158 while File.exist?(old_file)
159 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
161 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
162 port2 = unused_port(@addr)
167 ucfg.syswrite("listen %(#@addr:#@port)\n")
168 ucfg.syswrite("listen %(#@addr:#{port2})\n")
169 ucfg.syswrite("pid %(#{pid_file})\n")
170 assert_nothing_raised { Process.kill(:USR2, current_pid) }
172 wait_for_file(old_file)
173 wait_for_file(pid_file)
174 new_pid = File.read(pid_file).to_i
175 assert_not_equal current_pid, new_pid
176 assert_equal current_pid, File.read(old_file).to_i
177 results = retry_hit(["http://#{@addr}:#{@port}/",
178 "http://#{@addr}:#{port2}/"])
179 assert_equal String, results[0].class
180 assert_equal String, results[1].class
182 assert_nothing_raised do
183 Process.kill(:QUIT, current_pid)
184 Process.kill(:QUIT, new_pid)
188 def test_broken_reexec_ru
189 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
190 pid_file = "#{@tmpdir}/test.pid"
191 old_file = "#{pid_file}.oldbin"
192 ucfg = Tempfile.new('unicorn_test_config')
193 ucfg.syswrite("pid %(#{pid_file})\n")
194 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
197 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
200 results = retry_hit(["http://#{@addr}:#{@port}/"])
201 assert_equal String, results[0].class
203 wait_for_file(pid_file)
205 Process.kill(:USR2, File.read(pid_file).to_i)
206 wait_for_file(old_file)
207 wait_for_file(pid_file)
208 old_pid = File.read(old_file).to_i
209 Process.kill(:QUIT, old_pid)
210 wait_for_death(old_pid)
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
226 tries = DEFAULT_TRIES
227 while File.exist?(old_file)
228 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
230 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
231 assert_equal current_pid, File.read(pid_file).to_i
234 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
235 assert_nothing_raised { 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_listener_swap
251 port_cli = unused_port
252 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
253 ucfg = Tempfile.new('unicorn_test_config')
254 ucfg.syswrite("listen '#@addr:#@port'\n")
257 exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
260 results = retry_hit(["http://#@addr:#{port_cli}/"])
261 assert_equal String, results[0].class
262 results = retry_hit(["http://#@addr:#@port/"])
263 assert_equal String, results[0].class
265 port2 = unused_port(@addr)
268 ucfg.syswrite("listen '#@addr:#{port2}'\n")
269 Process.kill(:HUP, pid)
271 results = retry_hit(["http://#@addr:#{port2}/"])
272 assert_equal String, results[0].class
273 results = retry_hit(["http://#@addr:#{port_cli}/"])
274 assert_equal String, results[0].class
275 assert_nothing_raised do
276 reuse = TCPServer.new(@addr, @port)
282 def test_unicorn_config_listen_with_options
283 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
284 ucfg = Tempfile.new('unicorn_test_config')
285 ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
286 ucfg.syswrite(" :rcvbuf => 4096,\n")
287 ucfg.syswrite(" :sndbuf => 4096\n")
289 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
291 results = retry_hit(["http://#{@addr}:#{@port}/"])
292 assert_equal String, results[0].class
296 def test_unicorn_config_per_worker_listen
298 pid_spit = 'use Rack::ContentLength;' \
299 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
300 File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
301 tmp = Tempfile.new('test.socket')
302 File.unlink(tmp.path)
303 ucfg = Tempfile.new('unicorn_test_config')
304 ucfg.syswrite("listen '#@addr:#@port'\n")
305 ucfg.syswrite("before_fork { |s,w|\n")
306 ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
307 ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
308 ucfg.syswrite("\n}\n")
310 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
312 results = retry_hit(["http://#{@addr}:#{@port}/"])
313 assert_equal String, results[0].class
314 worker_pid = results[0].to_i
315 assert_not_equal pid, worker_pid
316 s = UNIXSocket.new(tmp.path)
317 s.syswrite("GET / HTTP/1.0\r\n\r\n")
319 loop { results << s.sysread(4096) } rescue nil
320 assert_nothing_raised { s.close }
321 assert_equal worker_pid, results.split(/\r\n/).last.to_i
322 results = hit(["http://#@addr:#{port2}/"])
323 assert_equal String, results[0].class
324 assert_equal worker_pid, results[0].to_i
328 def test_unicorn_config_listen_augments_cli
329 port2 = unused_port(@addr)
330 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
331 ucfg = Tempfile.new('unicorn_test_config')
332 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
335 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
338 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
339 results = retry_hit(uris)
340 assert_equal results.size, uris.size
341 assert_equal String, results[0].class
342 assert_equal String, results[1].class
346 def test_weird_config_settings
347 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
348 ucfg = Tempfile.new('unicorn_test_config')
349 ucfg.syswrite(HEAVY_CFG)
352 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
356 results = retry_hit(["http://#{@addr}:#{@port}/"])
357 assert_equal String, results[0].class
358 wait_master_ready(COMMON_TMP.path)
359 wait_workers_ready(COMMON_TMP.path, 4)
360 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
361 assert_equal 4, bf.size
362 rotate = Tempfile.new('unicorn_rotate')
363 assert_nothing_raised do
364 File.rename(COMMON_TMP.path, rotate.path)
365 Process.kill(:USR1, pid)
367 wait_for_file(COMMON_TMP.path)
368 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
369 # USR1 should've been passed to all workers
370 tries = DEFAULT_TRIES
371 log = File.readlines(rotate.path)
372 while (tries -= 1) > 0 &&
373 log.grep(/reopening logs\.\.\./).size < 5
375 log = File.readlines(rotate.path)
377 assert_equal 5, log.grep(/reopening logs\.\.\./).size
378 assert_equal 0, log.grep(/done reopening logs/).size
380 tries = DEFAULT_TRIES
381 log = File.readlines(COMMON_TMP.path)
382 while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
384 log = File.readlines(COMMON_TMP.path)
386 assert_equal 5, log.grep(/done reopening logs/).size
387 assert_equal 0, log.grep(/reopening logs\.\.\./).size
388 assert_nothing_raised { Process.kill(:QUIT, pid) }
390 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
391 assert status.success?, "exited successfully"
394 def test_read_embedded_cli_switches
395 File.open("config.ru", "wb") do |fp|
396 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
399 pid = fork { redirect_test_io { exec($unicorn_bin) } }
400 results = retry_hit(["http://#{@addr}:#{@port}/"])
401 assert_equal String, results[0].class
405 def test_config_ru_alt_path
406 config_path = "#{@tmpdir}/foo.ru"
407 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
411 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
414 results = retry_hit(["http://#{@addr}:#{@port}/"])
415 assert_equal String, results[0].class
420 libdir = "#{@tmpdir}/lib"
421 FileUtils.mkpath([ libdir ])
422 config_path = "#{libdir}/hello.rb"
423 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
427 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
430 results = retry_hit(["http://#{@addr}:#{@port}/"])
431 assert_equal String, results[0].class
436 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
437 pid_file = "#{@tmpdir}/test.pid"
440 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
443 reexec_basic_test(pid, pid_file)
446 def test_reexec_alt_config
447 config_file = "#{@tmpdir}/foo.ru"
448 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
449 pid_file = "#{@tmpdir}/test.pid"
452 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
455 reexec_basic_test(pid, pid_file)
458 def test_socket_unlinked_restore
460 sock = Tempfile.new('unicorn_test_sock')
461 sock_path = sock.path
462 @sockets << sock_path
464 ucfg = Tempfile.new('unicorn_test_config')
465 ucfg.syswrite("listen \"#{sock_path}\"\n")
467 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
468 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
469 wait_for_file(sock_path)
470 assert File.socket?(sock_path)
471 assert_nothing_raised do
472 sock = UNIXSocket.new(sock_path)
473 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
474 results = sock.sysread(4096)
476 assert_equal String, results.class
477 assert_nothing_raised do
478 File.unlink(sock_path)
479 Process.kill(:HUP, pid)
481 wait_for_file(sock_path)
482 assert File.socket?(sock_path)
483 assert_nothing_raised do
484 sock = UNIXSocket.new(sock_path)
485 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
486 results = sock.sysread(4096)
488 assert_equal String, results.class
491 def test_unicorn_config_file
492 pid_file = "#{@tmpdir}/test.pid"
493 sock = Tempfile.new('unicorn_test_sock')
494 sock_path = sock.path
496 @sockets << sock_path
498 log = Tempfile.new('unicorn_test_log')
499 ucfg = Tempfile.new('unicorn_test_config')
500 ucfg.syswrite("listen \"#{sock_path}\"\n")
501 ucfg.syswrite("pid \"#{pid_file}\"\n")
502 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
505 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
508 exec($unicorn_bin, "-l#{@addr}:#{@port}",
509 "-P#{pid_file}", "-c#{ucfg.path}")
512 results = retry_hit(["http://#{@addr}:#{@port}/"])
513 assert_equal String, results[0].class
514 wait_master_ready(log.path)
515 assert File.exist?(pid_file), "pid_file created"
516 assert_equal pid, File.read(pid_file).to_i
517 assert File.socket?(sock_path), "socket created"
518 assert_nothing_raised do
519 sock = UNIXSocket.new(sock_path)
520 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
521 results = sock.sysread(4096)
523 assert_equal String, results.class
525 # try reloading the config
526 sock = Tempfile.new('new_test_sock')
527 new_sock_path = sock.path
528 @sockets << new_sock_path
530 new_log = Tempfile.new('unicorn_test_log')
532 assert_equal 0, new_log.size
534 assert_nothing_raised do
535 ucfg = File.open(ucfg.path, "wb")
536 ucfg.syswrite("listen \"#{sock_path}\"\n")
537 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
538 ucfg.syswrite("pid \"#{pid_file}\"\n")
539 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
541 Process.kill(:HUP, pid)
544 wait_for_file(new_sock_path)
545 assert File.socket?(new_sock_path), "socket exists"
546 @sockets.each do |path|
547 assert_nothing_raised do
548 sock = UNIXSocket.new(path)
549 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
550 results = sock.sysread(4096)
552 assert_equal String, results.class
555 assert_not_equal 0, new_log.size
556 reexec_usr2_quit_test(pid, pid_file)
559 def test_daemonize_reexec
560 pid_file = "#{@tmpdir}/test.pid"
561 log = Tempfile.new('unicorn_test_log')
562 ucfg = Tempfile.new('unicorn_test_config')
563 ucfg.syswrite("pid \"#{pid_file}\"\n")
564 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
567 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
570 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
573 results = retry_hit(["http://#{@addr}:#{@port}/"])
574 assert_equal String, results[0].class
575 wait_for_file(pid_file)
576 new_pid = File.read(pid_file).to_i
577 assert_not_equal pid, new_pid
578 pid, status = Process.waitpid2(pid)
579 assert status.success?, "original process exited successfully"
580 assert_nothing_raised { Process.kill(0, new_pid) }
581 reexec_usr2_quit_test(new_pid, pid_file)
584 def test_reexec_fd_leak
585 unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
586 warn "FD leak test only works on Linux at the moment"
589 pid_file = "#{@tmpdir}/test.pid"
590 log = Tempfile.new('unicorn_test_log')
592 ucfg = Tempfile.new('unicorn_test_config')
593 ucfg.syswrite("pid \"#{pid_file}\"\n")
594 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
595 ucfg.syswrite("stderr_path '#{log.path}'\n")
596 ucfg.syswrite("stdout_path '#{log.path}'\n")
599 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
602 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
606 wait_master_ready(log.path)
607 File.truncate(log.path, 0)
608 wait_for_file(pid_file)
609 orig_pid = pid = File.read(pid_file).to_i
610 orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
612 expect_size = orig_fds.size
614 assert_nothing_raised do
615 Process.kill(:USR2, pid)
616 wait_for_file("#{pid_file}.oldbin")
617 Process.kill(:QUIT, pid)
621 wait_master_ready(log.path)
622 File.truncate(log.path, 0)
623 wait_for_file(pid_file)
624 pid = File.read(pid_file).to_i
625 assert_not_equal orig_pid, pid
626 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
629 # we could've inherited descriptors the first time around
630 assert expect_size >= curr_fds.size, curr_fds.inspect
631 expect_size = curr_fds.size
633 assert_nothing_raised do
634 Process.kill(:USR2, pid)
635 wait_for_file("#{pid_file}.oldbin")
636 Process.kill(:QUIT, pid)
640 wait_master_ready(log.path)
641 File.truncate(log.path, 0)
642 wait_for_file(pid_file)
643 pid = File.read(pid_file).to_i
644 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
646 assert_equal expect_size, curr_fds.size, curr_fds.inspect
648 Process.kill(:QUIT, pid)