Transfer-Encoding: chunked streaming input support
[unicorn.git] / test / test_helper.rb
blob0f2f3113ba73bca3961047eb35a3652617b707d2
1 # Copyright (c) 2005 Zed A. Shaw 
2 # You can redistribute it and/or modify it under the same terms as Ruby.
4 # Additional work donated by contributors.  See http://mongrel.rubyforge.org/attributions.html 
5 # for more information.
7 STDIN.sync = STDOUT.sync = STDERR.sync = true # buffering makes debugging hard
9 # Some tests watch a log file or a pid file to spring up to check state
10 # Can't rely on inotify on non-Linux and logging to a pipe makes things
11 # more complicated
12 DEFAULT_TRIES = 1000
13 DEFAULT_RES = 0.2
15 HERE = File.dirname(__FILE__) unless defined?(HERE)
16 %w(lib ext).each do |dir|
17   $LOAD_PATH.unshift "#{HERE}/../#{dir}"
18 end
20 require 'test/unit'
21 require 'net/http'
22 require 'digest/sha1'
23 require 'uri'
24 require 'stringio'
25 require 'pathname'
26 require 'tempfile'
27 require 'fileutils'
28 require 'logger'
29 require 'unicorn'
30 require 'unicorn/http11'
32 if ENV['DEBUG']
33   require 'ruby-debug'
34   Debugger.start
35 end
37 def redirect_test_io
38   orig_err = STDERR.dup
39   orig_out = STDOUT.dup
40   STDERR.reopen("test_stderr.#{$$}.log", "a")
41   STDOUT.reopen("test_stdout.#{$$}.log", "a")
42   STDERR.sync = STDOUT.sync = true
44   at_exit do
45     File.unlink("test_stderr.#{$$}.log") rescue nil
46     File.unlink("test_stdout.#{$$}.log") rescue nil
47   end
49   begin
50     yield
51   ensure
52     STDERR.reopen(orig_err)
53     STDOUT.reopen(orig_out)
54   end
55 end
57 # which(1) exit codes cannot be trusted on some systems
58 # We use UNIX shell utilities in some tests because we don't trust
59 # ourselves to write Ruby 100% correctly :)
60 def which(bin)
61   ex = ENV['PATH'].split(/:/).detect do |x|
62     x << "/#{bin}"
63     File.executable?(x)
64   end or warn "`#{bin}' not found in PATH=#{ENV['PATH']}"
65   ex
66 end
68 # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where
69 # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.)
70 def hit(uris)
71   results = []
72   uris.each do |u|
73     res = nil
75     if u.kind_of? String
76       res = Net::HTTP.get(URI.parse(u))
77     else
78       url = URI.parse(u[0])
79       res = Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) }
80     end
82     assert res != nil, "Didn't get a response: #{u}"
83     results << res
84   end
86   return results
87 end
89 # unused_port provides an unused port on +addr+ usable for TCP that is
90 # guaranteed to be unused across all unicorn builds on that system.  It
91 # prevents race conditions by using a lock file other unicorn builds
92 # will see.  This is required if you perform several builds in parallel
93 # with a continuous integration system or run tests in parallel via
94 # gmake.  This is NOT guaranteed to be race-free if you run other
95 # processes that bind to random ports for testing (but the window
96 # for a race condition is very small).  You may also set UNICORN_TEST_ADDR
97 # to override the default test address (127.0.0.1).
98 def unused_port(addr = '127.0.0.1')
99   retries = 100
100   base = 5000
101   port = sock = nil
102   begin
103     begin
104       port = base + rand(32768 - base)
105       sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
106       sock.bind(Socket.pack_sockaddr_in(port, addr))
107       sock.listen(5)
108     rescue Errno::EADDRINUSE, Errno::EACCES
109       sock.close rescue nil
110       retry if (retries -= 1) >= 0
111     end
113     # since we'll end up closing the random port we just got, there's a race
114     # condition could allow the random port we just chose to reselect itself
115     # when running tests in parallel with gmake.  Create a lock file while
116     # we have the port here to ensure that does not happen .
117     lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
118     lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
119     at_exit { File.unlink(lock_path) rescue nil }
120   rescue Errno::EEXIST
121     sock.close rescue nil
122     retry
123   end
124   sock.close rescue nil
125   port
128 def try_require(lib)
129   begin
130     require lib
131     true
132   rescue LoadError
133     false
134   end
137 # sometimes the server may not come up right away
138 def retry_hit(uris = [])
139   tries = DEFAULT_TRIES
140   begin
141     hit(uris)
142   rescue Errno::ECONNREFUSED => err
143     if (tries -= 1) > 0
144       sleep DEFAULT_RES
145       retry
146     end
147     raise err
148   end
151 def assert_shutdown(pid)
152   wait_master_ready("test_stderr.#{pid}.log")
153   assert_nothing_raised { Process.kill(:QUIT, pid) }
154   status = nil
155   assert_nothing_raised { pid, status = Process.waitpid2(pid) }
156   assert status.success?, "exited successfully"
159 def wait_workers_ready(path, nr_workers)
160   tries = DEFAULT_TRIES
161   lines = []
162   while (tries -= 1) > 0
163     begin
164       lines = File.readlines(path).grep(/worker=\d+ ready/)
165       lines.size == nr_workers and return
166     rescue Errno::ENOENT
167     end
168     sleep DEFAULT_RES
169   end
170   raise "#{nr_workers} workers never became ready:" \
171         "\n\t#{lines.join("\n\t")}\n"
174 def wait_master_ready(master_log)
175   tries = DEFAULT_TRIES
176   while (tries -= 1) > 0
177     begin
178       File.readlines(master_log).grep(/master process ready/)[0] and return
179     rescue Errno::ENOENT
180     end
181     sleep DEFAULT_RES
182   end
183   raise "master process never became ready"
186 def reexec_usr2_quit_test(pid, pid_file)
187   assert File.exist?(pid_file), "pid file OK"
188   assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
189   assert_nothing_raised { Process.kill(:USR2, pid) }
190   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
191   wait_for_file("#{pid_file}.oldbin")
192   wait_for_file(pid_file)
194   old_pid = File.read("#{pid_file}.oldbin").to_i
195   new_pid = File.read(pid_file).to_i
197   # kill old master process
198   assert_not_equal pid, new_pid
199   assert_equal pid, old_pid
200   assert_nothing_raised { Process.kill(:QUIT, old_pid) }
201   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
202   wait_for_death(old_pid)
203   assert_equal new_pid, File.read(pid_file).to_i
204   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
205   assert_nothing_raised { Process.kill(:QUIT, new_pid) }
208 def reexec_basic_test(pid, pid_file)
209   results = retry_hit(["http://#{@addr}:#{@port}/"])
210   assert_equal String, results[0].class
211   assert_nothing_raised { Process.kill(0, pid) }
212   master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
213   wait_master_ready(master_log)
214   File.truncate(master_log, 0)
215   nr = 50
216   kill_point = 2
217   assert_nothing_raised do
218     nr.times do |i|
219       hit(["http://#{@addr}:#{@port}/#{i}"])
220       i == kill_point and Process.kill(:HUP, pid)
221     end
222   end
223   wait_master_ready(master_log)
224   assert File.exist?(pid_file), "pid=#{pid_file} exists"
225   new_pid = File.read(pid_file).to_i
226   assert_not_equal pid, new_pid
227   assert_nothing_raised { Process.kill(0, new_pid) }
228   assert_nothing_raised { Process.kill(:QUIT, new_pid) }
231 def wait_for_file(path)
232   tries = DEFAULT_TRIES
233   while (tries -= 1) > 0 && ! File.exist?(path)
234     sleep DEFAULT_RES
235   end
236   assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
239 def xfork(&block)
240   fork do
241     ObjectSpace.each_object(Tempfile) do |tmp|
242       ObjectSpace.undefine_finalizer(tmp)
243     end
244     yield
245   end
248 # can't waitpid on detached processes
249 def wait_for_death(pid)
250   tries = DEFAULT_TRIES
251   while (tries -= 1) > 0
252     begin
253       Process.kill(0, pid)
254       begin
255         Process.waitpid(pid, Process::WNOHANG)
256       rescue Errno::ECHILD
257       end
258       sleep(DEFAULT_RES)
259     rescue Errno::ESRCH
260       return
261     end
262   end
263   raise "PID:#{pid} never died!"
266 # executes +cmd+ and chunks its STDOUT
267 def chunked_spawn(stdout, *cmd)
268   fork {
269     crd, cwr = IO.pipe
270     crd.binmode
271     cwr.binmode
272     crd.sync = cwr.sync = true
274     pid = fork {
275       STDOUT.reopen(cwr)
276       crd.close
277       cwr.close
278       exec(*cmd)
279     }
280     cwr.close
281     begin
282       buf = crd.readpartial(16384)
283       stdout.write("#{'%x' % buf.size}\r\n#{buf}")
284     rescue EOFError
285       stdout.write("0\r\n")
286       pid, status = Process.waitpid(pid)
287       exit status.exitstatus
288     end while true
289   }