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