test_helper: ensure test client connects to valid address
[unicorn.git] / test / test_helper.rb
blob11dd5eedacffb43ab1e4afa9da12658b3c2a299c
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 1.8 or
5 # the GPLv3
7 # Additional work donated by contributors.  See http://mongrel.rubyforge.org/attributions.html 
8 # for more information.
10 STDIN.sync = STDOUT.sync = STDERR.sync = true # buffering makes debugging hard
12 # FIXME: move curl-dependent tests into t/
13 ENV['NO_PROXY'] ||= ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
15 # Some tests watch a log file or a pid file to spring up to check state
16 # Can't rely on inotify on non-Linux and logging to a pipe makes things
17 # more complicated
18 DEFAULT_TRIES = 1000
19 DEFAULT_RES = 0.2
21 require 'test/unit'
22 require 'net/http'
23 require 'digest/sha1'
24 require 'uri'
25 require 'stringio'
26 require 'pathname'
27 require 'tempfile'
28 require 'fileutils'
29 require 'logger'
30 require 'unicorn'
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       u = 'http://127.0.0.1:8080/' if u == 'http://0.0.0.0:8080/'
77       res = Net::HTTP.get(URI.parse(u))
78     else
79       url = URI.parse(u[0])
80       res = Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) }
81     end
83     assert res != nil, "Didn't get a response: #{u}"
84     results << res
85   end
87   return results
88 end
90 # unused_port provides an unused port on +addr+ usable for TCP that is
91 # guaranteed to be unused across all unicorn builds on that system.  It
92 # prevents race conditions by using a lock file other unicorn builds
93 # will see.  This is required if you perform several builds in parallel
94 # with a continuous integration system or run tests in parallel via
95 # gmake.  This is NOT guaranteed to be race-free if you run other
96 # processes that bind to random ports for testing (but the window
97 # for a race condition is very small).  You may also set UNICORN_TEST_ADDR
98 # to override the default test address (127.0.0.1).
99 def unused_port(addr = '127.0.0.1')
100   retries = 100
101   base = 5000
102   port = sock = nil
103   begin
104     begin
105       port = base + rand(32768 - base)
106       while port == Unicorn::Const::DEFAULT_PORT
107         port = base + rand(32768 - base)
108       end
110       sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
111       sock.bind(Socket.pack_sockaddr_in(port, addr))
112       sock.listen(5)
113     rescue Errno::EADDRINUSE, Errno::EACCES
114       sock.close rescue nil
115       retry if (retries -= 1) >= 0
116     end
118     # since we'll end up closing the random port we just got, there's a race
119     # condition could allow the random port we just chose to reselect itself
120     # when running tests in parallel with gmake.  Create a lock file while
121     # we have the port here to ensure that does not happen .
122     lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
123     File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600).close
124     at_exit { File.unlink(lock_path) rescue nil }
125   rescue Errno::EEXIST
126     sock.close rescue nil
127     retry
128   end
129   sock.close rescue nil
130   port
133 def try_require(lib)
134   begin
135     require lib
136     true
137   rescue LoadError
138     false
139   end
142 # sometimes the server may not come up right away
143 def retry_hit(uris = [])
144   tries = DEFAULT_TRIES
145   begin
146     hit(uris)
147   rescue Errno::EINVAL, Errno::ECONNREFUSED => err
148     if (tries -= 1) > 0
149       sleep DEFAULT_RES
150       retry
151     end
152     raise err
153   end
156 def assert_shutdown(pid)
157   wait_master_ready("test_stderr.#{pid}.log")
158   assert_nothing_raised { Process.kill(:QUIT, pid) }
159   status = nil
160   assert_nothing_raised { pid, status = Process.waitpid2(pid) }
161   assert status.success?, "exited successfully"
164 def wait_workers_ready(path, nr_workers)
165   tries = DEFAULT_TRIES
166   lines = []
167   while (tries -= 1) > 0
168     begin
169       lines = File.readlines(path).grep(/worker=\d+ ready/)
170       lines.size == nr_workers and return
171     rescue Errno::ENOENT
172     end
173     sleep DEFAULT_RES
174   end
175   raise "#{nr_workers} workers never became ready:" \
176         "\n\t#{lines.join("\n\t")}\n"
179 def wait_master_ready(master_log)
180   tries = DEFAULT_TRIES
181   while (tries -= 1) > 0
182     begin
183       File.readlines(master_log).grep(/master process ready/)[0] and return
184     rescue Errno::ENOENT
185     end
186     sleep DEFAULT_RES
187   end
188   raise "master process never became ready"
191 def reexec_usr2_quit_test(pid, pid_file)
192   assert File.exist?(pid_file), "pid file OK"
193   assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
194   assert_nothing_raised { Process.kill(:USR2, pid) }
195   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
196   wait_for_file("#{pid_file}.oldbin")
197   wait_for_file(pid_file)
199   old_pid = File.read("#{pid_file}.oldbin").to_i
200   new_pid = File.read(pid_file).to_i
202   # kill old master process
203   assert_not_equal pid, new_pid
204   assert_equal pid, old_pid
205   assert_nothing_raised { Process.kill(:QUIT, old_pid) }
206   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
207   wait_for_death(old_pid)
208   assert_equal new_pid, File.read(pid_file).to_i
209   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
210   assert_nothing_raised { Process.kill(:QUIT, new_pid) }
213 def reexec_basic_test(pid, pid_file)
214   results = retry_hit(["http://#{@addr}:#{@port}/"])
215   assert_equal String, results[0].class
216   assert_nothing_raised { Process.kill(0, pid) }
217   master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
218   wait_master_ready(master_log)
219   File.truncate(master_log, 0)
220   nr = 50
221   kill_point = 2
222   assert_nothing_raised do
223     nr.times do |i|
224       hit(["http://#{@addr}:#{@port}/#{i}"])
225       i == kill_point and Process.kill(:HUP, pid)
226     end
227   end
228   wait_master_ready(master_log)
229   assert File.exist?(pid_file), "pid=#{pid_file} exists"
230   new_pid = File.read(pid_file).to_i
231   assert_not_equal pid, new_pid
232   assert_nothing_raised { Process.kill(0, new_pid) }
233   assert_nothing_raised { Process.kill(:QUIT, new_pid) }
236 def wait_for_file(path)
237   tries = DEFAULT_TRIES
238   while (tries -= 1) > 0 && ! File.exist?(path)
239     sleep DEFAULT_RES
240   end
241   assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
244 def xfork(&block)
245   fork do
246     ObjectSpace.each_object(Tempfile) do |tmp|
247       ObjectSpace.undefine_finalizer(tmp)
248     end
249     yield
250   end
253 # can't waitpid on detached processes
254 def wait_for_death(pid)
255   tries = DEFAULT_TRIES
256   while (tries -= 1) > 0
257     begin
258       Process.kill(0, pid)
259       begin
260         Process.waitpid(pid, Process::WNOHANG)
261       rescue Errno::ECHILD
262       end
263       sleep(DEFAULT_RES)
264     rescue Errno::ESRCH
265       return
266     end
267   end
268   raise "PID:#{pid} never died!"
271 # executes +cmd+ and chunks its STDOUT
272 def chunked_spawn(stdout, *cmd)
273   fork {
274     crd, cwr = IO.pipe
275     crd.binmode
276     cwr.binmode
277     crd.sync = cwr.sync = true
279     pid = fork {
280       STDOUT.reopen(cwr)
281       crd.close
282       cwr.close
283       exec(*cmd)
284     }
285     cwr.close
286     begin
287       buf = crd.readpartial(16384)
288       stdout.write("#{'%x' % buf.size}\r\n#{buf}")
289     rescue EOFError
290       stdout.write("0\r\n")
291       pid, status = Process.waitpid(pid)
292       exit status.exitstatus
293     end while true
294   }
297 def reset_sig_handlers
298   sigs = %w(CHLD).concat(Unicorn::HttpServer::QUEUE_SIGS)
299   sigs.each { |sig| trap(sig, "DEFAULT") }