refactor backend socket/connection handling
[ruby-mogilefs-client.git] / test / test_backend.rb
blob7858af6137fd28219f0eabd634918e2e79c0533a
1 # -*- encoding: binary -*-
2 require 'test/unit'
3 require 'test/setup'
5 $TESTING = true
7 require 'mogilefs/backend'
9 class MogileFS::Backend
11   attr_accessor :hosts
12   attr_reader :timeout, :dead
13   attr_writer :lasterr, :lasterrstr, :socket
15 end
17 class TestBackend < Test::Unit::TestCase
19   def setup
20     @backend = MogileFS::Backend.new :hosts => ['localhost:1']
21   end
23   def test_initialize
24     assert_raises ArgumentError do MogileFS::Backend.new end
25     assert_raises ArgumentError do MogileFS::Backend.new :hosts => [] end
26     assert_raises ArgumentError do MogileFS::Backend.new :hosts => [''] end
28     assert_equal ['localhost:1'], @backend.hosts
29     assert_equal 3, @backend.timeout
30     assert_equal nil, @backend.lasterr
31     assert_equal nil, @backend.lasterrstr
32     assert_equal({}, @backend.dead)
34     @backend = MogileFS::Backend.new :hosts => ['localhost:6001'], :timeout => 1
35     assert_equal 1, @backend.timeout
36   end
38   def test_do_request
39     received = Tempfile.new('received')
40     tmp = TempServer.new(Proc.new do |serv, port|
41       client, client_addr = serv.accept
42       client.sync = true
43       received.syswrite(client.recv(4096))
44       client.send "OK 1 you=win\r\n", 0
45     end)
47     @backend.hosts = [ "127.0.0.1:#{tmp.port}" ]
49     assert_equal({'you' => 'win'},
50                  @backend.do_request('go!', { 'fight' => 'team fight!' }))
51     received.sysseek(0)
52     assert_equal "go! fight=team+fight%21\r\n", received.sysread(4096)
53     ensure
54       TempServer.destroy_all!
55   end
57   def test_do_request_send_error
58     socket_request = ''
59     socket = Object.new
60     def socket.closed?() false end
61     def socket.write(request) raise SystemCallError, 'dummy' end
63     @backend.instance_variable_set '@socket', socket
65     assert_raises MogileFS::UnreachableBackendError do
66       @backend.do_request 'go!', { 'fight' => 'team fight!' }
67     end
69     assert_equal nil, @backend.instance_variable_get('@socket')
70   end
72   def test_automatic_exception
73     assert ! MogileFS::Backend.const_defined?('PebkacError')
74     assert @backend.error('pebkac')
75     assert_equal MogileFS::Error, @backend.error('PebkacError').superclass
76     assert MogileFS::Backend.const_defined?('PebkacError')
78     assert ! MogileFS::Backend.const_defined?('PebKacError')
79     assert @backend.error('peb_kac')
80     assert_equal MogileFS::Error, @backend.error('PebKacError').superclass
81     assert MogileFS::Backend.const_defined?('PebKacError')
82   end
84   def test_size_verify_error_defined
85     # "ErrorError" just looks dumb, but we used to get it
86     # since mogilefs would send us "size_verify_error" and we'd
87     # blindly append "Error" to the exception
88     assert ! MogileFS::Backend.const_defined?('SizeVerifyErrorError')
89     assert MogileFS::Backend.const_defined?('SizeVerifyError')
90   end
92   def test_do_request_truncated
93     socket_request = ''
94     socket = Object.new
95     def socket.closed?() false end
96     def socket.write(request) return request.length - 1 end
98     @backend.instance_variable_set '@socket', socket
100     assert_raises MogileFS::RequestTruncatedError do
101       @backend.do_request 'go!', { 'fight' => 'team fight!' }
102     end
103   end
105   def test_make_request
106     assert_equal "go! fight=team+fight%21\r\n",
107                  @backend.make_request('go!', { 'fight' => 'team fight!' })
108   end
110   def test_parse_response
111     assert_equal({'foo' => 'bar', 'baz' => 'hoge'},
112                  @backend.parse_response('OK 1 foo=bar&baz=hoge'))
114     err = nil
115     begin
116       @backend.parse_response('ERR you totally suck')
117     rescue MogileFS::Error => err
118       assert_equal 'MogileFS::Backend::YouError', err.class.to_s
119       assert_equal 'totally suck', err.message
120     end
121     assert_equal 'MogileFS::Backend::YouError', err.class.to_s
123     assert_equal 'you', @backend.lasterr
124     assert_equal 'totally suck', @backend.lasterrstr
126     assert_raises MogileFS::InvalidResponseError do
127       @backend.parse_response 'garbage'
128     end
129   end
131   def test_parse_response_newline
132     begin
133       @backend.parse_response("ERR you totally suck\r\n")
134     rescue MogileFS::Error => err
135       assert_equal 'MogileFS::Backend::YouError', err.class.to_s
136       assert_equal 'totally suck', err.message
137     end
139     assert_equal 'you', @backend.lasterr
140     assert_equal 'totally suck', @backend.lasterrstr
141   end
143   def test_readable_eh_not_readable
144     tmp = TempServer.new(Proc.new { |serv,port| serv.accept; sleep })
145     @backend = MogileFS::Backend.new(:hosts => [ "127.0.0.1:#{tmp.port}" ],
146                                      :timeout => 0.5)
147     begin
148       @backend.do_request 'foo', {}
149     rescue MogileFS::UnreadableSocketError => e
150       assert_equal "127.0.0.1:#{tmp.port} never became readable", e.message
151     rescue Exception => err
152       flunk "MogileFS::UnreadableSocketError not raised #{err} #{err.backtrace}"
153     else
154       flunk "MogileFS::UnreadableSocketError not raised"
155     ensure
156       TempServer.destroy_all!
157     end
158   end
160   def test_socket
161     assert_equal({}, @backend.dead)
162     assert_raises MogileFS::UnreachableBackendError do @backend.socket end
163     assert_equal(['localhost:1'], @backend.dead.keys)
164   end
166   def test_socket_robust
167     bad_accept = Tempfile.new('bad_accept')
168     accept = Tempfile.new('accept')
169     bad = Proc.new do |serv,port|
170       client, client_addr = serv.accept
171       bad_accept.syswrite('!')
172     end
173     good = Proc.new do |serv,port|
174       client, client_addr = serv.accept
175       accept.syswrite('.')
176       client.syswrite('.')
177       client.close
178       sleep
179     end
180     nr = 10
182     nr.times do
183       begin
184         t1 = TempServer.new(bad, ENV['TEST_DEAD_PORT'])
185         t2 = TempServer.new(good)
186         hosts = ["127.0.0.1:#{t1.port}", "127.0.0.1:#{t2.port}"]
187         @backend = MogileFS::Backend.new(:hosts => hosts.dup)
188         assert_equal({}, @backend.dead)
189         old_chld_handler = trap('CHLD', 'DEFAULT')
190         t1.destroy!
191         Process.waitpid(t1.pid)
192         trap('CHLD', old_chld_handler)
193         sock = @backend.socket
194         assert_equal Socket, sock.class
195         port = Socket.unpack_sockaddr_in(sock.getpeername).first
196         # p [ 'ports', "port=#{port}", "t1=#{t1.port}", "t2=#{t2.port}" ]
197         assert_equal t2.port, port
198         IO.select([sock])
199         assert_equal '.', sock.sysread(1)
200       ensure
201         TempServer.destroy_all!
202       end
203     end # nr.times
204     assert_equal 0, bad_accept.stat.size
205     assert_equal nr, accept.stat.size
206   end
208   def test_shutdown
209     accept_nr = 0
210     tmp = TempServer.new(Proc.new do |serv,port|
211       client, client_addr = serv.accept
212       accept_nr += 1
213       r = IO.select([client], [client])
214       client.syswrite(accept_nr.to_s)
215       sleep
216     end)
217     @backend = MogileFS::Backend.new :hosts => [ "127.0.0.1:#{tmp.port}" ]
218     assert @backend.socket
219     assert ! @backend.socket.closed?
220     IO.select([@backend.socket])
221     resp = @backend.socket.sysread(4096)
222     @backend.shutdown
223     assert_equal nil, @backend.instance_variable_get(:@socket)
224     assert_equal 1, resp.to_i
225     ensure
226       TempServer.destroy_all!
227   end
229   def test_url_decode
230     assert_equal({"\272z" => "\360opy", "f\000" => "\272r"},
231                  @backend.url_decode("%baz=%f0opy&f%00=%bar"))
232     assert_equal({}, @backend.url_decode(''))
233   end
235   def test_url_encode
236     params = [["f\000", "\272r"], ["\272z", "\360opy"]]
237     assert_equal "f%00=%bar&%baz=%f0opy", @backend.url_encode(params)
238   end
240   def test_url_escape # \n for unit_diff
241     actual = (0..255).map { |c| @backend.url_escape c.chr }.join "\n"
243     expected = []
244     expected.push(*(0..0x1f).map { |c| "%%%0.2x" % c })
245     expected << '+'
246     expected.push(*(0x21..0x2b).map { |c| "%%%0.2x" % c })
247     expected.push(*%w[, - . /])
248     expected.push(*('0'..'9'))
249     expected.push(*%w[: %3b %3c %3d %3e %3f %40])
250     expected.push(*('A'..'Z'))
251     expected.push(*%w[%5b \\ %5d %5e _ %60])
252     expected.push(*('a'..'z'))
253     expected.push(*(0x7b..0xff).map { |c| "%%%0.2x" % c })
255     expected = expected.join "\n"
257     assert_equal expected, actual
258   end
260   def test_url_unescape
261     input = []
262     input.push(*(0..0x1f).map { |c| "%%%0.2x" % c })
263     input << '+'
264     input.push(*(0x21..0x2b).map { |c| "%%%0.2x" % c })
265     input.push(*%w[, - . /])
266     input.push(*('0'..'9'))
267     input.push(*%w[: %3b %3c %3d %3e %3f %40])
268     input.push(*('A'..'Z'))
269     input.push(*%w[%5b \\ %5d %5e _ %60])
270     input.push(*('a'..'z'))
271     input.push(*(0x7b..0xff).map { |c| "%%%0.2x" % c })
273     actual = input.map { |c| @backend.url_unescape c }.join "\n"
275     expected = (0..255).map { |c| c.chr }.join "\n"
276     expected.sub! '+', ' '
278     assert_equal expected, actual
279   end