1 # -*- encoding: binary -*-
3 # Copyright (c) 2005 Zed A. Shaw
4 # You can redistribute it and/or modify it under the same terms as Ruby.
6 # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
7 # for more information.
9 STDIN.sync = STDOUT.sync = STDERR.sync = true # buffering makes debugging hard
11 # Some tests watch a log file or a pid file to spring up to check state
12 # Can't rely on inotify on non-Linux and logging to a pipe makes things
17 HERE = File.dirname(__FILE__) unless defined?(HERE)
18 %w(lib ext).each do |dir|
19 $LOAD_PATH.unshift "#{HERE}/../#{dir}"
32 require 'unicorn_http'
42 STDERR.reopen("test_stderr.#{$$}.log", "a")
43 STDOUT.reopen("test_stdout.#{$$}.log", "a")
44 STDERR.sync = STDOUT.sync = true
47 File.unlink("test_stderr.#{$$}.log") rescue nil
48 File.unlink("test_stdout.#{$$}.log") rescue nil
54 STDERR.reopen(orig_err)
55 STDOUT.reopen(orig_out)
59 # which(1) exit codes cannot be trusted on some systems
60 # We use UNIX shell utilities in some tests because we don't trust
61 # ourselves to write Ruby 100% correctly :)
63 ex = ENV['PATH'].split(/:/).detect do |x|
66 end or warn "`#{bin}' not found in PATH=#{ENV['PATH']}"
70 # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where
71 # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.)
78 res = Net::HTTP.get(URI.parse(u))
81 res = Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) }
84 assert res != nil, "Didn't get a response: #{u}"
91 # unused_port provides an unused port on +addr+ usable for TCP that is
92 # guaranteed to be unused across all unicorn builds on that system. It
93 # prevents race conditions by using a lock file other unicorn builds
94 # will see. This is required if you perform several builds in parallel
95 # with a continuous integration system or run tests in parallel via
96 # gmake. This is NOT guaranteed to be race-free if you run other
97 # processes that bind to random ports for testing (but the window
98 # for a race condition is very small). You may also set UNICORN_TEST_ADDR
99 # to override the default test address (127.0.0.1).
100 def unused_port(addr = '127.0.0.1')
106 port = base + rand(32768 - base)
107 while port == Unicorn::Const::DEFAULT_PORT
108 port = base + rand(32768 - base)
111 sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
112 sock.bind(Socket.pack_sockaddr_in(port, addr))
114 rescue Errno::EADDRINUSE, Errno::EACCES
115 sock.close rescue nil
116 retry if (retries -= 1) >= 0
119 # since we'll end up closing the random port we just got, there's a race
120 # condition could allow the random port we just chose to reselect itself
121 # when running tests in parallel with gmake. Create a lock file while
122 # we have the port here to ensure that does not happen .
123 lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
124 lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
125 at_exit { File.unlink(lock_path) rescue nil }
127 sock.close rescue nil
130 sock.close rescue nil
143 # sometimes the server may not come up right away
144 def retry_hit(uris = [])
145 tries = DEFAULT_TRIES
148 rescue Errno::EINVAL, Errno::ECONNREFUSED => err
157 def assert_shutdown(pid)
158 wait_master_ready("test_stderr.#{pid}.log")
159 assert_nothing_raised { Process.kill(:QUIT, pid) }
161 assert_nothing_raised { pid, status = Process.waitpid2(pid) }
162 assert status.success?, "exited successfully"
165 def wait_workers_ready(path, nr_workers)
166 tries = DEFAULT_TRIES
168 while (tries -= 1) > 0
170 lines = File.readlines(path).grep(/worker=\d+ ready/)
171 lines.size == nr_workers and return
176 raise "#{nr_workers} workers never became ready:" \
177 "\n\t#{lines.join("\n\t")}\n"
180 def wait_master_ready(master_log)
181 tries = DEFAULT_TRIES
182 while (tries -= 1) > 0
184 File.readlines(master_log).grep(/master process ready/)[0] and return
189 raise "master process never became ready"
192 def reexec_usr2_quit_test(pid, pid_file)
193 assert File.exist?(pid_file), "pid file OK"
194 assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
195 assert_nothing_raised { Process.kill(:USR2, pid) }
196 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
197 wait_for_file("#{pid_file}.oldbin")
198 wait_for_file(pid_file)
200 old_pid = File.read("#{pid_file}.oldbin").to_i
201 new_pid = File.read(pid_file).to_i
203 # kill old master process
204 assert_not_equal pid, new_pid
205 assert_equal pid, old_pid
206 assert_nothing_raised { Process.kill(:QUIT, old_pid) }
207 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
208 wait_for_death(old_pid)
209 assert_equal new_pid, File.read(pid_file).to_i
210 assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
211 assert_nothing_raised { Process.kill(:QUIT, new_pid) }
214 def reexec_basic_test(pid, pid_file)
215 results = retry_hit(["http://#{@addr}:#{@port}/"])
216 assert_equal String, results[0].class
217 assert_nothing_raised { Process.kill(0, pid) }
218 master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
219 wait_master_ready(master_log)
220 File.truncate(master_log, 0)
223 assert_nothing_raised do
225 hit(["http://#{@addr}:#{@port}/#{i}"])
226 i == kill_point and Process.kill(:HUP, pid)
229 wait_master_ready(master_log)
230 assert File.exist?(pid_file), "pid=#{pid_file} exists"
231 new_pid = File.read(pid_file).to_i
232 assert_not_equal pid, new_pid
233 assert_nothing_raised { Process.kill(0, new_pid) }
234 assert_nothing_raised { Process.kill(:QUIT, new_pid) }
237 def wait_for_file(path)
238 tries = DEFAULT_TRIES
239 while (tries -= 1) > 0 && ! File.exist?(path)
242 assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
247 ObjectSpace.each_object(Tempfile) do |tmp|
248 ObjectSpace.undefine_finalizer(tmp)
254 # can't waitpid on detached processes
255 def wait_for_death(pid)
256 tries = DEFAULT_TRIES
257 while (tries -= 1) > 0
261 Process.waitpid(pid, Process::WNOHANG)
269 raise "PID:#{pid} never died!"
272 # executes +cmd+ and chunks its STDOUT
273 def chunked_spawn(stdout, *cmd)
278 crd.sync = cwr.sync = true
288 buf = crd.readpartial(16384)
289 stdout.write("#{'%x' % buf.size}\r\n#{buf}")
291 stdout.write("0\r\n")
292 pid, status = Process.waitpid(pid)
293 exit status.exitstatus