test_helper: connect(2) may fail with EINVAL
[unicorn.git] / test / test_helper.rb
blob3bdbeb1b420f8475498a962583666e61040f6313
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
13 # more complicated
14 DEFAULT_TRIES = 1000
15 DEFAULT_RES = 0.2
17 HERE = File.dirname(__FILE__) unless defined?(HERE)
18 %w(lib ext).each do |dir|
19   $LOAD_PATH.unshift "#{HERE}/../#{dir}"
20 end
22 require 'test/unit'
23 require 'net/http'
24 require 'digest/sha1'
25 require 'uri'
26 require 'stringio'
27 require 'pathname'
28 require 'tempfile'
29 require 'fileutils'
30 require 'logger'
31 require 'unicorn'
32 require 'unicorn_http'
34 if ENV['DEBUG']
35   require 'ruby-debug'
36   Debugger.start
37 end
39 def redirect_test_io
40   orig_err = STDERR.dup
41   orig_out = STDOUT.dup
42   STDERR.reopen("test_stderr.#{$$}.log", "a")
43   STDOUT.reopen("test_stdout.#{$$}.log", "a")
44   STDERR.sync = STDOUT.sync = true
46   at_exit do
47     File.unlink("test_stderr.#{$$}.log") rescue nil
48     File.unlink("test_stdout.#{$$}.log") rescue nil
49   end
51   begin
52     yield
53   ensure
54     STDERR.reopen(orig_err)
55     STDOUT.reopen(orig_out)
56   end
57 end
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 :)
62 def which(bin)
63   ex = ENV['PATH'].split(/:/).detect do |x|
64     x << "/#{bin}"
65     File.executable?(x)
66   end or warn "`#{bin}' not found in PATH=#{ENV['PATH']}"
67   ex
68 end
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.)
72 def hit(uris)
73   results = []
74   uris.each do |u|
75     res = nil
77     if u.kind_of? String
78       res = Net::HTTP.get(URI.parse(u))
79     else
80       url = URI.parse(u[0])
81       res = Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) }
82     end
84     assert res != nil, "Didn't get a response: #{u}"
85     results << res
86   end
88   return results
89 end
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')
101   retries = 100
102   base = 5000
103   port = sock = nil
104   begin
105     begin
106       port = base + rand(32768 - base)
107       while port == Unicorn::Const::DEFAULT_PORT
108         port = base + rand(32768 - base)
109       end
111       sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
112       sock.bind(Socket.pack_sockaddr_in(port, addr))
113       sock.listen(5)
114     rescue Errno::EADDRINUSE, Errno::EACCES
115       sock.close rescue nil
116       retry if (retries -= 1) >= 0
117     end
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 }
126   rescue Errno::EEXIST
127     sock.close rescue nil
128     retry
129   end
130   sock.close rescue nil
131   port
134 def try_require(lib)
135   begin
136     require lib
137     true
138   rescue LoadError
139     false
140   end
143 # sometimes the server may not come up right away
144 def retry_hit(uris = [])
145   tries = DEFAULT_TRIES
146   begin
147     hit(uris)
148   rescue Errno::EINVAL, Errno::ECONNREFUSED => err
149     if (tries -= 1) > 0
150       sleep DEFAULT_RES
151       retry
152     end
153     raise err
154   end
157 def assert_shutdown(pid)
158   wait_master_ready("test_stderr.#{pid}.log")
159   assert_nothing_raised { Process.kill(:QUIT, pid) }
160   status = nil
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
167   lines = []
168   while (tries -= 1) > 0
169     begin
170       lines = File.readlines(path).grep(/worker=\d+ ready/)
171       lines.size == nr_workers and return
172     rescue Errno::ENOENT
173     end
174     sleep DEFAULT_RES
175   end
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
183     begin
184       File.readlines(master_log).grep(/master process ready/)[0] and return
185     rescue Errno::ENOENT
186     end
187     sleep DEFAULT_RES
188   end
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)
221   nr = 50
222   kill_point = 2
223   assert_nothing_raised do
224     nr.times do |i|
225       hit(["http://#{@addr}:#{@port}/#{i}"])
226       i == kill_point and Process.kill(:HUP, pid)
227     end
228   end
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)
240     sleep DEFAULT_RES
241   end
242   assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
245 def xfork(&block)
246   fork do
247     ObjectSpace.each_object(Tempfile) do |tmp|
248       ObjectSpace.undefine_finalizer(tmp)
249     end
250     yield
251   end
254 # can't waitpid on detached processes
255 def wait_for_death(pid)
256   tries = DEFAULT_TRIES
257   while (tries -= 1) > 0
258     begin
259       Process.kill(0, pid)
260       begin
261         Process.waitpid(pid, Process::WNOHANG)
262       rescue Errno::ECHILD
263       end
264       sleep(DEFAULT_RES)
265     rescue Errno::ESRCH
266       return
267     end
268   end
269   raise "PID:#{pid} never died!"
272 # executes +cmd+ and chunks its STDOUT
273 def chunked_spawn(stdout, *cmd)
274   fork {
275     crd, cwr = IO.pipe
276     crd.binmode
277     cwr.binmode
278     crd.sync = cwr.sync = true
280     pid = fork {
281       STDOUT.reopen(cwr)
282       crd.close
283       cwr.close
284       exec(*cmd)
285     }
286     cwr.close
287     begin
288       buf = crd.readpartial(16384)
289       stdout.write("#{'%x' % buf.size}\r\n#{buf}")
290     rescue EOFError
291       stdout.write("0\r\n")
292       pid, status = Process.waitpid(pid)
293       exit status.exitstatus
294     end while true
295   }