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_listen_with_options
235 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
236 ucfg = Tempfile.new('unicorn_test_config')
237 ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
238 ucfg.syswrite(" :rcvbuf => 4096,\n")
239 ucfg.syswrite(" :sndbuf => 4096\n")
241 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
243 results = retry_hit(["http://#{@addr}:#{@port}/"])
244 assert_equal String, results[0].class
248 def test_unicorn_config_per_worker_listen
250 pid_spit = 'use Rack::ContentLength;' \
251 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
252 File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
253 tmp = Tempfile.new('test.socket')
254 File.unlink(tmp.path)
255 ucfg = Tempfile.new('unicorn_test_config')
256 ucfg.syswrite("listen '#@addr:#@port'\n")
257 ucfg.syswrite("before_fork { |s,nr|\n")
258 ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
259 ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
260 ucfg.syswrite("\n}\n")
262 redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
264 results = retry_hit(["http://#{@addr}:#{@port}/"])
265 assert_equal String, results[0].class
266 worker_pid = results[0].to_i
267 assert_not_equal pid, worker_pid
268 s = UNIXSocket.new(tmp.path)
269 s.syswrite("GET / HTTP/1.0\r\n\r\n")
271 loop { results << s.sysread(4096) } rescue nil
272 assert_nothing_raised { s.close }
273 assert_equal worker_pid, results.split(/\r\n/).last.to_i
274 results = hit(["http://#@addr:#{port2}/"])
275 assert_equal String, results[0].class
276 assert_equal worker_pid, results[0].to_i
280 def test_unicorn_config_listen_augments_cli
281 port2 = unused_port(@addr)
282 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
283 ucfg = Tempfile.new('unicorn_test_config')
284 ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
287 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
290 uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
291 results = retry_hit(uris)
292 assert_equal results.size, uris.size
293 assert_equal String, results[0].class
294 assert_equal String, results[1].class
298 def test_weird_config_settings
299 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
300 ucfg = Tempfile.new('unicorn_test_config')
301 ucfg.syswrite(HEAVY_CFG)
304 exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
308 results = retry_hit(["http://#{@addr}:#{@port}/"])
309 assert_equal String, results[0].class
310 wait_master_ready(COMMON_TMP.path)
311 wait_workers_ready(COMMON_TMP.path, 4)
312 bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
313 assert_equal 4, bf.size
314 rotate = Tempfile.new('unicorn_rotate')
315 assert_nothing_raised do
316 File.rename(COMMON_TMP.path, rotate.path)
317 Process.kill(:USR1, pid)
319 wait_for_file(COMMON_TMP.path)
320 assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
321 # USR1 should've been passed to all workers
322 tries = DEFAULT_TRIES
323 log = File.readlines(rotate.path)
324 while (tries -= 1) > 0 &&
325 log.grep(/rotating logs\.\.\./).size < 5
327 log = File.readlines(rotate.path)
329 assert_equal 5, log.grep(/rotating logs\.\.\./).size
330 assert_equal 0, log.grep(/done rotating logs/).size
332 tries = DEFAULT_TRIES
333 log = File.readlines(COMMON_TMP.path)
334 while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 5
336 log = File.readlines(COMMON_TMP.path)
338 assert_equal 5, log.grep(/done rotating logs/).size
339 assert_equal 0, log.grep(/rotating logs\.\.\./).size
340 assert_nothing_raised { Process.kill(:QUIT, pid) }
342 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
343 assert status.success?, "exited successfully"
346 def test_read_embedded_cli_switches
347 File.open("config.ru", "wb") do |fp|
348 fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
351 pid = fork { redirect_test_io { exec($unicorn_bin) } }
352 results = retry_hit(["http://#{@addr}:#{@port}/"])
353 assert_equal String, results[0].class
357 def test_config_ru_alt_path
358 config_path = "#{@tmpdir}/foo.ru"
359 File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
363 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
366 results = retry_hit(["http://#{@addr}:#{@port}/"])
367 assert_equal String, results[0].class
372 libdir = "#{@tmpdir}/lib"
373 FileUtils.mkpath([ libdir ])
374 config_path = "#{libdir}/hello.rb"
375 File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
379 exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
382 results = retry_hit(["http://#{@addr}:#{@port}/"])
383 assert_equal String, results[0].class
388 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
389 pid_file = "#{@tmpdir}/test.pid"
392 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
395 reexec_basic_test(pid, pid_file)
398 def test_reexec_alt_config
399 config_file = "#{@tmpdir}/foo.ru"
400 File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
401 pid_file = "#{@tmpdir}/test.pid"
404 exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
407 reexec_basic_test(pid, pid_file)
410 def test_unicorn_config_file
411 pid_file = "#{@tmpdir}/test.pid"
412 sock = Tempfile.new('unicorn_test_sock')
413 sock_path = sock.path
415 @sockets << sock_path
417 log = Tempfile.new('unicorn_test_log')
418 ucfg = Tempfile.new('unicorn_test_config')
419 ucfg.syswrite("listen \"#{sock_path}\"\n")
420 ucfg.syswrite("pid \"#{pid_file}\"\n")
421 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
424 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
427 exec($unicorn_bin, "-l#{@addr}:#{@port}",
428 "-P#{pid_file}", "-c#{ucfg.path}")
431 results = retry_hit(["http://#{@addr}:#{@port}/"])
432 assert_equal String, results[0].class
433 wait_master_ready(log.path)
434 assert File.exist?(pid_file), "pid_file created"
435 assert_equal pid, File.read(pid_file).to_i
436 assert File.socket?(sock_path), "socket created"
437 assert_nothing_raised do
438 sock = UNIXSocket.new(sock_path)
439 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
440 results = sock.sysread(4096)
442 assert_equal String, results.class
444 # try reloading the config
445 sock = Tempfile.new('unicorn_test_sock')
446 new_sock_path = sock.path
447 @sockets << new_sock_path
449 new_log = Tempfile.new('unicorn_test_log')
451 assert_equal 0, new_log.size
453 assert_nothing_raised do
454 ucfg = File.open(ucfg.path, "wb")
455 ucfg.syswrite("listen \"#{new_sock_path}\"\n")
456 ucfg.syswrite("pid \"#{pid_file}\"\n")
457 ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
459 Process.kill(:HUP, pid)
462 wait_for_file(new_sock_path)
463 assert File.socket?(new_sock_path), "socket exists"
464 @sockets.each do |path|
465 assert_nothing_raised do
466 sock = UNIXSocket.new(path)
467 sock.syswrite("GET / HTTP/1.0\r\n\r\n")
468 results = sock.sysread(4096)
470 assert_equal String, results.class
473 assert_not_equal 0, new_log.size
474 reexec_usr2_quit_test(pid, pid_file)
477 def test_daemonize_reexec
478 pid_file = "#{@tmpdir}/test.pid"
479 log = Tempfile.new('unicorn_test_log')
480 ucfg = Tempfile.new('unicorn_test_config')
481 ucfg.syswrite("pid \"#{pid_file}\"\n")
482 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
485 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
488 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
491 results = retry_hit(["http://#{@addr}:#{@port}/"])
492 assert_equal String, results[0].class
493 wait_for_file(pid_file)
494 new_pid = File.read(pid_file).to_i
495 assert_not_equal pid, new_pid
496 pid, status = Process.waitpid2(pid)
497 assert status.success?, "original process exited successfully"
498 assert_nothing_raised { Process.kill(0, new_pid) }
499 reexec_usr2_quit_test(new_pid, pid_file)
502 def test_reexec_fd_leak
503 unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
504 warn "FD leak test only works on Linux at the moment"
507 pid_file = "#{@tmpdir}/test.pid"
508 log = Tempfile.new('unicorn_test_log')
510 ucfg = Tempfile.new('unicorn_test_config')
511 ucfg.syswrite("pid \"#{pid_file}\"\n")
512 ucfg.syswrite("logger Logger.new('#{log.path}')\n")
513 ucfg.syswrite("stderr_path '#{log.path}'\n")
514 ucfg.syswrite("stdout_path '#{log.path}'\n")
517 File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
520 exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
524 wait_master_ready(log.path)
525 wait_for_file(pid_file)
526 orig_pid = pid = File.read(pid_file).to_i
527 orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
529 expect_size = orig_fds.size
531 assert_nothing_raised do
532 Process.kill(:USR2, pid)
533 wait_for_file("#{pid_file}.oldbin")
534 Process.kill(:QUIT, pid)
538 wait_for_file(pid_file)
539 pid = File.read(pid_file).to_i
540 assert_not_equal orig_pid, pid
541 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
544 # we could've inherited descriptors the first time around
545 assert expect_size >= curr_fds.size
546 expect_size = curr_fds.size
548 assert_nothing_raised do
549 Process.kill(:USR2, pid)
550 wait_for_file("#{pid_file}.oldbin")
551 Process.kill(:QUIT, pid)
555 wait_for_file(pid_file)
556 pid = File.read(pid_file).to_i
557 curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
559 assert_equal expect_size, curr_fds.size
561 Process.kill(:QUIT, pid)