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_nr|
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 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
78 redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
80 results = retry_hit(["http://#{@addr}:#{@port}/"])
81 assert_equal String, results[0].class
87 assert(system($unicorn_bin, "-h"), "help text returns true")
89 assert_equal 0, File.stat("test_stderr.#$$.log").size
90 assert_not_equal 0, File.stat("test_stdout.#$$.log").size
91 lines = File.readlines("test_stdout.#$$.log")
93 # Be considerate of the on-call technician working from their
94 # mobile phone or netbook on a slow connection :)
95 assert lines.size <= 24, "help height fits in an ANSI terminal window"
97 assert line.size <= 80, "help width fits in an ANSI terminal window"
101 def test_broken_reexec_config
102 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
103 pid_file = "#{@tmpdir}/test.pid"
104 old_file = "#{pid_file}.oldbin"
105 ucfg = Tempfile.new('unicorn_test_config')
106 ucfg.syswrite("listen %(#@addr:#@port)\n")
107 ucfg.syswrite("pid %(#{pid_file})\n")
108 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
111 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
114 results = retry_hit(["http://#{@addr}:#{@port}/"])
115 assert_equal String, results[0].class
117 wait_for_file(pid_file)
119 Process.kill(:USR2, File.read(pid_file).to_i)
120 wait_for_file(old_file)
121 wait_for_file(pid_file)
122 old_pid = File.read(old_file).to_i
123 Process.kill(:QUIT, old_pid)
124 wait_for_death(old_pid)
126 ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
127 current_pid = File.read(pid_file).to_i
128 Process.kill(:USR2, current_pid)
130 # wait for pid_file to restore itself
131 tries = DEFAULT_TRIES
133 while current_pid != File.read(pid_file).to_i
134 sleep(DEFAULT_RES) and (tries -= 1) > 0
137 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
139 assert_equal current_pid, File.read(pid_file).to_i
141 tries = DEFAULT_TRIES
142 while File.exist?(old_file)
143 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
145 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
146 port2 = unused_port(@addr)
151 ucfg.syswrite("listen %(#@addr:#@port)\n")
152 ucfg.syswrite("listen %(#@addr:#{port2})\n")
153 ucfg.syswrite("pid %(#{pid_file})\n")
154 assert_nothing_raised { Process.kill(:USR2, current_pid) }
156 wait_for_file(old_file)
157 wait_for_file(pid_file)
158 new_pid = File.read(pid_file).to_i
159 assert_not_equal current_pid, new_pid
160 assert_equal current_pid, File.read(old_file).to_i
161 results = retry_hit(["http://#{@addr}:#{@port}/",
162 "http://#{@addr}:#{port2}/"])
163 assert_equal String, results[0].class
164 assert_equal String, results[1].class
166 assert_nothing_raised do
167 Process.kill(:QUIT, current_pid)
168 Process.kill(:QUIT, new_pid)
172 def test_broken_reexec_ru
173 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
174 pid_file = "#{@tmpdir}/test.pid"
175 old_file = "#{pid_file}.oldbin"
176 ucfg = Tempfile.new('unicorn_test_config')
177 ucfg.syswrite("pid %(#{pid_file})\n")
178 ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
181 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
184 results = retry_hit(["http://#{@addr}:#{@port}/"])
185 assert_equal String, results[0].class
187 wait_for_file(pid_file)
189 Process.kill(:USR2, File.read(pid_file).to_i)
190 wait_for_file(old_file)
191 wait_for_file(pid_file)
192 old_pid = File.read(old_file).to_i
193 Process.kill(:QUIT, old_pid)
194 wait_for_death(old_pid)
196 File.unlink("config.ru") # break reloading
197 current_pid = File.read(pid_file).to_i
198 Process.kill(:USR2, current_pid)
200 # wait for pid_file to restore itself
201 tries = DEFAULT_TRIES
203 while current_pid != File.read(pid_file).to_i
204 sleep(DEFAULT_RES) and (tries -= 1) > 0
207 (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
210 tries = DEFAULT_TRIES
211 while File.exist?(old_file)
212 (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
214 assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
215 assert_equal current_pid, File.read(pid_file).to_i
218 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
219 assert_nothing_raised { Process.kill(:USR2, current_pid) }
220 wait_for_file(old_file)
221 wait_for_file(pid_file)
222 new_pid = File.read(pid_file).to_i
223 assert_not_equal current_pid, new_pid
224 assert_equal current_pid, File.read(old_file).to_i
225 results = retry_hit(["http://#{@addr}:#{@port}/"])
226 assert_equal String, results[0].class
228 assert_nothing_raised do
229 Process.kill(:QUIT, current_pid)
230 Process.kill(:QUIT, new_pid)
234 def test_unicorn_config_listener_swap
235 port_cli = unused_port
236 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
237 ucfg = Tempfile.new('unicorn_test_config')
238 ucfg.syswrite("listen '#@addr:#@port'\n")
241 exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
244 results = retry_hit(["http://#@addr:#{port_cli}/"])
245 assert_equal String, results[0].class
246 results = retry_hit(["http://#@addr:#@port/"])
247 assert_equal String, results[0].class
249 port2 = unused_port(@addr)
252 ucfg.syswrite("listen '#@addr:#{port2}'\n")
253 Process.kill(:HUP, pid)
255 results = retry_hit(["http://#@addr:#{port2}/"])
256 assert_equal String, results[0].class
257 results = retry_hit(["http://#@addr:#{port_cli}/"])
258 assert_equal String, results[0].class
259 assert_nothing_raised do
260 reuse = TCPServer.new(@addr, @port)
266 def test_unicorn_config_listen_with_options
267 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
268 ucfg = Tempfile.new('unicorn_test_config')
269 ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
270 ucfg.syswrite(" :rcvbuf => 4096,\n")
271 ucfg.syswrite(" :sndbuf => 4096\n")
273 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
275 results = retry_hit(["http://#{@addr}:#{@port}/"])
276 assert_equal String, results[0].class
280 def test_unicorn_config_per_worker_listen
282 pid_spit = 'use Rack::ContentLength;' \
283 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
284 File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
285 tmp = Tempfile.new('test.socket')
286 File.unlink(tmp.path)
287 ucfg = Tempfile.new('unicorn_test_config')
288 ucfg.syswrite("listen '#@addr:#@port'\n")
289 ucfg.syswrite("before_fork { |s,nr|\n")
290 ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
291 ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
292 ucfg.syswrite("\n}\n")
294 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
296 results = retry_hit(["http://#{@addr}:#{@port}/"])
297 assert_equal String, results[0].class
298 worker_pid = results[0].to_i
299 assert_not_equal pid, worker_pid
300 s = UNIXSocket.new(tmp.path)
301 s.syswrite("GET / HTTP/1.0\r\n\r\n")
303 loop { results << s.sysread(4096) } rescue nil
304 assert_nothing_raised { s.close }
305 assert_equal worker_pid, results.split(/\r\n/).last.to_i
306 results = hit(["http://#@addr:#{port2}/"])
307 assert_equal String, results[0].class
308 assert_equal worker_pid, results[0].to_i
312 def test_unicorn_config_listen_augments_cli
313 port2 = unused_port(@addr)
314 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
315 ucfg = Tempfile.new('unicorn_test_config')
316 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
319 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
322 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
323 results = retry_hit(uris)
324 assert_equal results.size, uris.size
325 assert_equal String, results[0].class
326 assert_equal String, results[1].class
330 def test_weird_config_settings
331 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
332 ucfg = Tempfile.new('unicorn_test_config')
333 ucfg.syswrite(HEAVY_CFG)
336 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
340 results = retry_hit(["http://#{@addr}:#{@port}/"])
341 assert_equal String, results[0].class
342 wait_master_ready(COMMON_TMP.path)
343 wait_workers_ready(COMMON_TMP.path, 4)
344 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
345 assert_equal 4, bf.size
346 rotate = Tempfile.new('unicorn_rotate')
347 assert_nothing_raised do
348 File.rename(COMMON_TMP.path, rotate.path)
349 Process.kill(:USR1, pid)
351 wait_for_file(COMMON_TMP.path)
352 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
353 # USR1 should've been passed to all workers
354 tries = DEFAULT_TRIES
355 log = File.readlines(rotate.path)
356 while (tries -= 1) > 0 &&
357 log.grep(/rotating logs\.\.\./).size < 5
359 log = File.readlines(rotate.path)
361 assert_equal 5, log.grep(/rotating logs\.\.\./).size
362 assert_equal 0, log.grep(/done rotating logs/).size
364 tries = DEFAULT_TRIES
365 log = File.readlines(COMMON_TMP.path)
366 while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 5
368 log = File.readlines(COMMON_TMP.path)
370 assert_equal 5, log.grep(/done rotating logs/).size
371 assert_equal 0, log.grep(/rotating logs\.\.\./).size
372 assert_nothing_raised { Process.kill(:QUIT, pid) }
374 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
375 assert status.success?, "exited successfully"
378 def test_read_embedded_cli_switches
379 File.open("config.ru", "wb") do |fp|
380 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
383 pid = fork { redirect_test_io { exec($unicorn_bin) } }
384 results = retry_hit(["http://#{@addr}:#{@port}/"])
385 assert_equal String, results[0].class
389 def test_config_ru_alt_path
390 config_path = "#{@tmpdir}/foo.ru"
391 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
395 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
398 results = retry_hit(["http://#{@addr}:#{@port}/"])
399 assert_equal String, results[0].class
404 libdir = "#{@tmpdir}/lib"
405 FileUtils.mkpath([ libdir ])
406 config_path = "#{libdir}/hello.rb"
407 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
411 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
414 results = retry_hit(["http://#{@addr}:#{@port}/"])
415 assert_equal String, results[0].class
420 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
421 pid_file = "#{@tmpdir}/test.pid"
424 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
427 reexec_basic_test(pid, pid_file)
430 def test_reexec_alt_config
431 config_file = "#{@tmpdir}/foo.ru"
432 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
433 pid_file = "#{@tmpdir}/test.pid"
436 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
439 reexec_basic_test(pid, pid_file)
442 def test_socket_unlinked_restore
444 sock = Tempfile.new('unicorn_test_sock')
445 sock_path = sock.path
447 ucfg = Tempfile.new('unicorn_test_config')
448 ucfg.syswrite("listen \"#{sock_path}\"\n")
450 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
451 pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
452 wait_for_file(sock_path)
453 assert File.socket?(sock_path)
454 assert_nothing_raised do
455 sock = UNIXSocket.new(sock_path)
456 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
457 results = sock.sysread(4096)
459 assert_equal String, results.class
460 assert_nothing_raised do
461 File.unlink(sock_path)
462 Process.kill(:HUP, pid)
464 wait_for_file(sock_path)
465 assert File.socket?(sock_path)
466 assert_nothing_raised do
467 sock = UNIXSocket.new(sock_path)
468 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
469 results = sock.sysread(4096)
471 assert_equal String, results.class
474 def test_unicorn_config_file
475 pid_file = "#{@tmpdir}/test.pid"
476 sock = Tempfile.new('unicorn_test_sock')
477 sock_path = sock.path
479 @sockets << sock_path
481 log = Tempfile.new('unicorn_test_log')
482 ucfg = Tempfile.new('unicorn_test_config')
483 ucfg.syswrite("listen \"#{sock_path}\"\n")
484 ucfg.syswrite("pid \"#{pid_file}\"\n")
485 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
488 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
491 exec($unicorn_bin, "-l#{@addr}:#{@port}",
492 "-P#{pid_file}", "-c#{ucfg.path}")
495 results = retry_hit(["http://#{@addr}:#{@port}/"])
496 assert_equal String, results[0].class
497 wait_master_ready(log.path)
498 assert File.exist?(pid_file), "pid_file created"
499 assert_equal pid, File.read(pid_file).to_i
500 assert File.socket?(sock_path), "socket created"
501 assert_nothing_raised do
502 sock = UNIXSocket.new(sock_path)
503 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
504 results = sock.sysread(4096)
506 assert_equal String, results.class
508 # try reloading the config
509 sock = Tempfile.new('new_test_sock')
510 new_sock_path = sock.path
511 @sockets << new_sock_path
513 new_log = Tempfile.new('unicorn_test_log')
515 assert_equal 0, new_log.size
517 assert_nothing_raised do
518 ucfg = File.open(ucfg.path, "wb")
519 ucfg.syswrite("listen \"#{sock_path}\"\n")
520 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
521 ucfg.syswrite("pid \"#{pid_file}\"\n")
522 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
524 Process.kill(:HUP, pid)
527 wait_for_file(new_sock_path)
528 assert File.socket?(new_sock_path), "socket exists"
529 @sockets.each do |path|
530 assert_nothing_raised do
531 sock = UNIXSocket.new(path)
532 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
533 results = sock.sysread(4096)
535 assert_equal String, results.class
538 assert_not_equal 0, new_log.size
539 reexec_usr2_quit_test(pid, pid_file)
542 def test_daemonize_reexec
543 pid_file = "#{@tmpdir}/test.pid"
544 log = Tempfile.new('unicorn_test_log')
545 ucfg = Tempfile.new('unicorn_test_config')
546 ucfg.syswrite("pid \"#{pid_file}\"\n")
547 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
550 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
553 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
556 results = retry_hit(["http://#{@addr}:#{@port}/"])
557 assert_equal String, results[0].class
558 wait_for_file(pid_file)
559 new_pid = File.read(pid_file).to_i
560 assert_not_equal pid, new_pid
561 pid, status = Process.waitpid2(pid)
562 assert status.success?, "original process exited successfully"
563 assert_nothing_raised { Process.kill(0, new_pid) }
564 reexec_usr2_quit_test(new_pid, pid_file)
567 def test_reexec_fd_leak
568 unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
569 warn "FD leak test only works on Linux at the moment"
572 pid_file = "#{@tmpdir}/test.pid"
573 log = Tempfile.new('unicorn_test_log')
575 ucfg = Tempfile.new('unicorn_test_config')
576 ucfg.syswrite("pid \"#{pid_file}\"\n")
577 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
578 ucfg.syswrite("stderr_path '#{log.path}'\n")
579 ucfg.syswrite("stdout_path '#{log.path}'\n")
582 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
585 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
589 wait_master_ready(log.path)
590 File.truncate(log.path, 0)
591 wait_for_file(pid_file)
592 orig_pid = pid = File.read(pid_file).to_i
593 orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
595 expect_size = orig_fds.size
597 assert_nothing_raised do
598 Process.kill(:USR2, pid)
599 wait_for_file("#{pid_file}.oldbin")
600 Process.kill(:QUIT, pid)
604 wait_master_ready(log.path)
605 File.truncate(log.path, 0)
606 wait_for_file(pid_file)
607 pid = File.read(pid_file).to_i
608 assert_not_equal orig_pid, pid
609 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
612 # we could've inherited descriptors the first time around
613 assert expect_size >= curr_fds.size, curr_fds.inspect
614 expect_size = curr_fds.size
616 assert_nothing_raised do
617 Process.kill(:USR2, pid)
618 wait_for_file("#{pid_file}.oldbin")
619 Process.kill(:QUIT, pid)
623 wait_master_ready(log.path)
624 File.truncate(log.path, 0)
625 wait_for_file(pid_file)
626 pid = File.read(pid_file).to_i
627 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
629 assert_equal expect_size, curr_fds.size, curr_fds.inspect
631 Process.kill(:QUIT, pid)