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