t: ensure SSL certificates exist on fresh test
[unicorn.git] / test / test_helper.rb
blob3471b550f9bce8e05469b0b037747054117ac190
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       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       while port == Unicorn::Const::DEFAULT_PORT
106         port = base + rand(32768 - base)
107       end
109       sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
110       sock.bind(Socket.pack_sockaddr_in(port, addr))
111       sock.listen(5)
112     rescue Errno::EADDRINUSE, Errno::EACCES
113       sock.close rescue nil
114       retry if (retries -= 1) >= 0
115     end
117     # since we'll end up closing the random port we just got, there's a race
118     # condition could allow the random port we just chose to reselect itself
119     # when running tests in parallel with gmake.  Create a lock file while
120     # we have the port here to ensure that does not happen .
121     lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
122     File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600).close
123     at_exit { File.unlink(lock_path) rescue nil }
124   rescue Errno::EEXIST
125     sock.close rescue nil
126     retry
127   end
128   sock.close rescue nil
129   port
132 def try_require(lib)
133   begin
134     require lib
135     true
136   rescue LoadError
137     false
138   end
141 # sometimes the server may not come up right away
142 def retry_hit(uris = [])
143   tries = DEFAULT_TRIES
144   begin
145     hit(uris)
146   rescue Errno::EINVAL, Errno::ECONNREFUSED => err
147     if (tries -= 1) > 0
148       sleep DEFAULT_RES
149       retry
150     end
151     raise err
152   end
155 def assert_shutdown(pid)
156   wait_master_ready("test_stderr.#{pid}.log")
157   assert_nothing_raised { Process.kill(:QUIT, pid) }
158   status = nil
159   assert_nothing_raised { pid, status = Process.waitpid2(pid) }
160   assert status.success?, "exited successfully"
163 def wait_workers_ready(path, nr_workers)
164   tries = DEFAULT_TRIES
165   lines = []
166   while (tries -= 1) > 0
167     begin
168       lines = File.readlines(path).grep(/worker=\d+ ready/)
169       lines.size == nr_workers and return
170     rescue Errno::ENOENT
171     end
172     sleep DEFAULT_RES
173   end
174   raise "#{nr_workers} workers never became ready:" \
175         "\n\t#{lines.join("\n\t")}\n"
178 def wait_master_ready(master_log)
179   tries = DEFAULT_TRIES
180   while (tries -= 1) > 0
181     begin
182       File.readlines(master_log).grep(/master process ready/)[0] and return
183     rescue Errno::ENOENT
184     end
185     sleep DEFAULT_RES
186   end
187   raise "master process never became ready"
190 def reexec_usr2_quit_test(pid, pid_file)
191   assert File.exist?(pid_file), "pid file OK"
192   assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
193   assert_nothing_raised { Process.kill(:USR2, pid) }
194   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
195   wait_for_file("#{pid_file}.oldbin")
196   wait_for_file(pid_file)
198   old_pid = File.read("#{pid_file}.oldbin").to_i
199   new_pid = File.read(pid_file).to_i
201   # kill old master process
202   assert_not_equal pid, new_pid
203   assert_equal pid, old_pid
204   assert_nothing_raised { Process.kill(:QUIT, old_pid) }
205   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
206   wait_for_death(old_pid)
207   assert_equal new_pid, File.read(pid_file).to_i
208   assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
209   assert_nothing_raised { Process.kill(:QUIT, new_pid) }
212 def reexec_basic_test(pid, pid_file)
213   results = retry_hit(["http://#{@addr}:#{@port}/"])
214   assert_equal String, results[0].class
215   assert_nothing_raised { Process.kill(0, pid) }
216   master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
217   wait_master_ready(master_log)
218   File.truncate(master_log, 0)
219   nr = 50
220   kill_point = 2
221   assert_nothing_raised do
222     nr.times do |i|
223       hit(["http://#{@addr}:#{@port}/#{i}"])
224       i == kill_point and Process.kill(:HUP, pid)
225     end
226   end
227   wait_master_ready(master_log)
228   assert File.exist?(pid_file), "pid=#{pid_file} exists"
229   new_pid = File.read(pid_file).to_i
230   assert_not_equal pid, new_pid
231   assert_nothing_raised { Process.kill(0, new_pid) }
232   assert_nothing_raised { Process.kill(:QUIT, new_pid) }
235 def wait_for_file(path)
236   tries = DEFAULT_TRIES
237   while (tries -= 1) > 0 && ! File.exist?(path)
238     sleep DEFAULT_RES
239   end
240   assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
243 def xfork(&block)
244   fork do
245     ObjectSpace.each_object(Tempfile) do |tmp|
246       ObjectSpace.undefine_finalizer(tmp)
247     end
248     yield
249   end
252 # can't waitpid on detached processes
253 def wait_for_death(pid)
254   tries = DEFAULT_TRIES
255   while (tries -= 1) > 0
256     begin
257       Process.kill(0, pid)
258       begin
259         Process.waitpid(pid, Process::WNOHANG)
260       rescue Errno::ECHILD
261       end
262       sleep(DEFAULT_RES)
263     rescue Errno::ESRCH
264       return
265     end
266   end
267   raise "PID:#{pid} never died!"
270 # executes +cmd+ and chunks its STDOUT
271 def chunked_spawn(stdout, *cmd)
272   fork {
273     crd, cwr = IO.pipe
274     crd.binmode
275     cwr.binmode
276     crd.sync = cwr.sync = true
278     pid = fork {
279       STDOUT.reopen(cwr)
280       crd.close
281       cwr.close
282       exec(*cmd)
283     }
284     cwr.close
285     begin
286       buf = crd.readpartial(16384)
287       stdout.write("#{'%x' % buf.size}\r\n#{buf}")
288     rescue EOFError
289       stdout.write("0\r\n")
290       pid, status = Process.waitpid(pid)
291       exit status.exitstatus
292     end while true
293   }
296 def reset_sig_handlers
297   sigs = %w(CHLD).concat(Unicorn::HttpServer::QUEUE_SIGS)
298   sigs.each { |sig| trap(sig, "DEFAULT") }