├── Gemfile ├── .gitignore ├── test ├── test_helper.rb ├── request_test.rb ├── mod_test.rb ├── error_test.rb ├── service_test.rb ├── encodes_test.rb └── action_test.rb ├── Rakefile ├── lib ├── banana_phone │ ├── mod.rb │ ├── request.rb │ ├── service.rb │ ├── encodes.rb │ ├── errors.rb │ └── action.rb └── banana_phone.rb ├── README.md ├── banana_phone.gemspec ├── Gemfile.lock └── LICENSE /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /bin 3 | /vendor/gems 4 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require "minitest/autorun" 3 | require "minitest/should" 4 | require 'mocha' 5 | 6 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 7 | require 'banana_phone' 8 | 9 | class Enc 10 | include BananaPhone::Encodes 11 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | Rake::TestTask.new('test') do |t| 3 | t.test_files = FileList['test/*_test.rb'] 4 | t.ruby_opts += ['-rubygems -Itest'] if defined? Gem 5 | end 6 | 7 | task :default => :test 8 | 9 | task :console do 10 | exec('irb -Ilib -rbanana_phone') 11 | end -------------------------------------------------------------------------------- /lib/banana_phone/mod.rb: -------------------------------------------------------------------------------- 1 | module BananaPhone 2 | class Mod 3 | def initialize(svc, req, mod) 4 | @svc = svc 5 | @req = req 6 | @mod = mod 7 | end 8 | 9 | def method_missing(cmd, *args) 10 | args = [*args] 11 | Action.new(@svc, @req, @mod, cmd, args).execute 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BananaPhone 2 | 3 | BananaPhone is RPC for BananaPack 4 | 5 | BananaPhone is exactly like [BERTRPC](https://github.com/mojombo/bertrpc) except 6 | it uses BananaPack as its serialization format instead of BERT. 7 | 8 | The API is exactly the same as BERTRPC but the high level module is named 9 | BananaPack instead. 10 | -------------------------------------------------------------------------------- /lib/banana_phone/request.rb: -------------------------------------------------------------------------------- 1 | module BananaPhone 2 | class Request 3 | attr_accessor :kind, :options 4 | 5 | def initialize(svc, kind, options) 6 | @svc = svc 7 | @kind = kind 8 | @options = options 9 | end 10 | 11 | def method_missing(cmd, *args) 12 | Mod.new(@svc, self, cmd) 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /lib/banana_phone.rb: -------------------------------------------------------------------------------- 1 | require 'mochilo' 2 | require 'socket' 3 | require 'net/protocol' 4 | 5 | require 'banana_phone/service' 6 | require 'banana_phone/request' 7 | require 'banana_phone/mod' 8 | require 'banana_phone/encodes' 9 | require 'banana_phone/action' 10 | require 'banana_phone/errors' 11 | 12 | module BananaPhone 13 | def self.version 14 | File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION])).chomp 15 | rescue 16 | 'unknown' 17 | end 18 | 19 | VERSION = self.version 20 | end -------------------------------------------------------------------------------- /test/request_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RequestTest < MiniTest::Should::TestCase 4 | context "A Request" do 5 | setup do 6 | @svc = BananaPhone::Service.new('localhost', 9941) 7 | end 8 | 9 | should "be created with a Service and type" do 10 | assert BananaPhone::Request.new(@svc, :call, nil).is_a?(BananaPhone::Request) 11 | end 12 | end 13 | 14 | context "A Request instance" do 15 | setup do 16 | svc = BananaPhone::Service.new('localhost', 9941) 17 | @req = BananaPhone::Request.new(@svc, :call, nil) 18 | end 19 | 20 | should "return a Mod instance" do 21 | assert @req.myfun.is_a?(BananaPhone::Mod) 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /banana_phone.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = %q{banana_phone} 3 | s.version = "0.1.3" 4 | s.authors = ["Tom Preston-Werner", "Brian Lopez"] 5 | s.email = %q{tom@mojombo.com seniorlopez@gmail.com} 6 | s.files = `git ls-files`.split("\n") 7 | s.homepage = %q{http://github.com/github/banana_phone} 8 | s.rdoc_options = ["--charset=UTF-8"] 9 | s.require_paths = ["lib"] 10 | s.rubygems_version = %q{1.4.2} 11 | s.summary = %q{BananaPhone is a RPC client for BananaPack} 12 | s.test_files = `git ls-files spec`.split("\n") 13 | 14 | s.add_runtime_dependency 'mochilo', ">= 0.6" 15 | 16 | # tests 17 | s.add_development_dependency 'rake' 18 | s.add_development_dependency 'minitest' 19 | s.add_development_dependency 'minitest_should' 20 | s.add_development_dependency 'shoulda' 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | banana_phone (0.1.2) 5 | mochilo (>= 0.6) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | activesupport (3.2.11) 11 | i18n (~> 0.6) 12 | multi_json (~> 1.0) 13 | bourne (1.1.2) 14 | mocha (= 0.10.5) 15 | i18n (0.6.1) 16 | metaclass (0.0.1) 17 | minitest (4.5.0) 18 | minitest_should (0.3.1) 19 | mocha (0.10.5) 20 | metaclass (~> 0.0.1) 21 | mochilo (0.6) 22 | multi_json (1.5.0) 23 | rake (10.0.3) 24 | shoulda (3.3.2) 25 | shoulda-context (~> 1.0.1) 26 | shoulda-matchers (~> 1.4.1) 27 | shoulda-context (1.0.2) 28 | shoulda-matchers (1.4.2) 29 | activesupport (>= 3.0.0) 30 | bourne (~> 1.1.2) 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | banana_phone! 37 | minitest 38 | minitest_should 39 | rake 40 | shoulda 41 | -------------------------------------------------------------------------------- /lib/banana_phone/service.rb: -------------------------------------------------------------------------------- 1 | module BananaPhone 2 | class Service 3 | attr_accessor :host, :port, :timeout 4 | 5 | def initialize(host, port, timeout = nil) 6 | @host = host 7 | @port = port 8 | @timeout = timeout 9 | end 10 | 11 | def call(options = nil) 12 | verify_options(options) 13 | Request.new(self, :call, options) 14 | end 15 | 16 | def cast(options = nil) 17 | verify_options(options) 18 | Request.new(self, :cast, options) 19 | end 20 | 21 | # private 22 | 23 | def verify_options(options) 24 | if options 25 | if cache = options[:cache] 26 | unless cache[0] == :validation && cache[1].is_a?(String) 27 | raise InvalidOption.new("Valid :cache args are [:validation, String]") 28 | end 29 | else 30 | raise InvalidOption.new("Valid options are :cache") 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/mod_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ModTest < MiniTest::Should::TestCase 4 | context "A Mod" do 5 | setup do 6 | @svc = BananaPhone::Service.new('localhost', 9941) 7 | @req = @svc.call 8 | end 9 | 10 | should "be created with a Service, request and module name" do 11 | assert BananaPhone::Mod.new(@svc, @req, :mymod).is_a?(BananaPhone::Mod) 12 | end 13 | end 14 | 15 | context "A Mod instance" do 16 | setup do 17 | @svc = BananaPhone::Service.new('localhost', 9941) 18 | @req = @svc.call 19 | @mod = BananaPhone::Mod.new(@svc, @req, :mymod) 20 | end 21 | 22 | should "call execute on the type" do 23 | m = mock(:execute => nil) 24 | BananaPhone::Action.expects(:new).with(@svc, @req, :mymod, :myfun, [1, 2, 3]).returns(m) 25 | @mod.myfun(1, 2, 3) 26 | 27 | m = mock(:execute => nil) 28 | BananaPhone::Action.expects(:new).with(@svc, @req, :mymod, :myfun, [1]).returns(m) 29 | @mod.myfun(1) 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /test/error_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ErrorTest < MiniTest::Should::TestCase 4 | context "Errors in general" do 5 | should "be creatable with just a message string" do 6 | begin 7 | raise BananaPhone::Error.new('msg') 8 | rescue Object => e 9 | assert_equal "msg", e.message 10 | assert_equal 0, e.code 11 | end 12 | end 13 | 14 | should "be creatable with a [code, message] array" do 15 | begin 16 | raise BananaPhone::Error.new([7, 'msg']) 17 | rescue Object => e 18 | assert_equal "msg", e.message 19 | assert_equal 7, e.code 20 | end 21 | end 22 | 23 | should "record the original exception" do 24 | begin 25 | raise BananaPhone::Error.new('msg', 'Error', ['foo', 'bar']) 26 | rescue Object => e 27 | assert_equal "msg", e.message 28 | assert_equal "Error: msg", e.original_exception.message 29 | assert_equal ['foo', 'bar'], e.original_exception.backtrace 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/banana_phone/encodes.rb: -------------------------------------------------------------------------------- 1 | module BananaPhone 2 | module Encodes 3 | def encode_ruby_request(ruby_request) 4 | Mochilo.pack(ruby_request) 5 | end 6 | 7 | def decode_bert_response(bert_response) 8 | ruby_response = Mochilo.unpack_unsafe(bert_response) 9 | case ruby_response[0] 10 | when :reply 11 | ruby_response[1] 12 | when :noreply 13 | nil 14 | when :error 15 | error(ruby_response[1]) 16 | else 17 | raise 18 | end 19 | end 20 | 21 | def error(err) 22 | level, code, klass, message, backtrace = err 23 | 24 | case level 25 | when :protocol 26 | raise ProtocolError.new([code, message], klass, backtrace) 27 | when :server 28 | raise ServerError.new([code, message], klass, backtrace) 29 | when :user 30 | raise UserError.new([code, message], klass, backtrace) 31 | when :proxy 32 | raise ProxyError.new([code, message], klass, backtrace) 33 | else 34 | raise 35 | end 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tom Preston-Werner, Brian Lopez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/service_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ServiceTest < MiniTest::Should::TestCase 4 | context "A Service" do 5 | should "be creatable with host and port" do 6 | svc = BananaPhone::Service.new('localhost', 9941) 7 | assert svc.is_a?(BananaPhone::Service) 8 | end 9 | 10 | should "be creatable with host, port, and timeout" do 11 | svc = BananaPhone::Service.new('localhost', 9941, 5) 12 | assert svc.is_a?(BananaPhone::Service) 13 | end 14 | end 15 | 16 | context "A Service Instance's" do 17 | setup do 18 | @svc = BananaPhone::Service.new('localhost', 9941) 19 | end 20 | 21 | context "accessors" do 22 | should "return it's host" do 23 | assert_equal 'localhost', @svc.host 24 | end 25 | 26 | should "return it's port" do 27 | assert_equal 9941, @svc.port 28 | end 29 | end 30 | 31 | context "call method" do 32 | should "return a call type Request" do 33 | req = @svc.call 34 | assert req.is_a?(BananaPhone::Request) 35 | assert_equal :call, req.kind 36 | end 37 | end 38 | 39 | context "cast method" do 40 | should "return a cast type Request" do 41 | req = @svc.cast 42 | assert req.is_a?(BananaPhone::Request) 43 | assert_equal :cast, req.kind 44 | end 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /lib/banana_phone/errors.rb: -------------------------------------------------------------------------------- 1 | module BananaPhone 2 | class Error < StandardError 3 | attr_accessor :code, :original_exception 4 | 5 | def initialize(msg = nil, klass = nil, bt = []) 6 | case msg 7 | when Array 8 | code, message = msg 9 | else 10 | code, message = [0, msg] 11 | end 12 | 13 | if klass 14 | self.original_exception = RemoteError.new("#{klass}: #{message}") 15 | self.original_exception.set_backtrace(bt) 16 | else 17 | self.original_exception = self 18 | end 19 | 20 | self.code = code 21 | super(message) 22 | end 23 | end 24 | 25 | class RemoteError < Error 26 | end 27 | 28 | class ConnectionError < Error 29 | attr_reader :host, :port 30 | def initialize(host, port) 31 | @host, @port = host, port 32 | super("Unable to connect to #{host}:#{port}") 33 | end 34 | end 35 | 36 | # Raised when we don't get a response from a server in a timely 37 | # manner. This typically occurs in spite of a successful connection. 38 | class ReadTimeoutError < Error 39 | attr_reader :host, :port, :timeout 40 | def initialize(host, port, timeout) 41 | @host, @port, @timeout = host, port, timeout 42 | super("No response from #{host}:#{port} in #{timeout}s") 43 | end 44 | end 45 | 46 | # Raised when unexpected EOF is reached on the socket. 47 | class ReadError < Error 48 | attr_reader :host, :port 49 | def initialize(host, port) 50 | @host, @port = host, port 51 | super("Unable to read from #{host}:#{port}") 52 | end 53 | end 54 | 55 | class ProtocolError < Error 56 | NO_HEADER = [0, "Unable to read length header from server."] 57 | NO_DATA = [1, "Unable to read data from server."] 58 | end 59 | 60 | class ServerError < Error 61 | end 62 | 63 | class UserError < Error 64 | end 65 | 66 | class ProxyError < Error 67 | end 68 | 69 | class InvalidOption < Error 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/encodes_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EncodesTest < MiniTest::Should::TestCase 4 | context "An Encodes includer" do 5 | setup do 6 | @enc = Enc.new 7 | end 8 | 9 | context "ruby request encoder" do 10 | should "return BananaPhone encoded request" do 11 | bert = "\x94\xD4\x04call\xD4\x05mymod\xD4\x05myfun\x93\x01\x02\x03" 12 | assert_equal bert, @enc.encode_ruby_request([:call, :mymod, :myfun, [1, 2, 3]]) 13 | end 14 | end 15 | 16 | context "bert response decoder" do 17 | should "return response when reply" do 18 | req = @enc.encode_ruby_request([:reply, [1, 2, 3]]) 19 | res = @enc.decode_bert_response(req) 20 | assert_equal [1, 2, 3], res 21 | end 22 | 23 | should "return nil when noreply" do 24 | req = @enc.encode_ruby_request([:noreply]) 25 | res = @enc.decode_bert_response(req) 26 | assert_equal nil, res 27 | end 28 | 29 | should "raise a ProtocolError error when protocol level error is returned" do 30 | req = @enc.encode_ruby_request([:error, [:protocol, 1, "class", "invalid", []]]) 31 | assert_raises(BananaPhone::ProtocolError) do 32 | @enc.decode_bert_response(req) 33 | end 34 | end 35 | 36 | should "raise a ServerError error when server level error is returned" do 37 | req = @enc.encode_ruby_request([:error, [:server, 1, "class", "invalid", []]]) 38 | assert_raises(BananaPhone::ServerError) do 39 | @enc.decode_bert_response(req) 40 | end 41 | end 42 | 43 | should "raise a UserError error when user level error is returned" do 44 | req = @enc.encode_ruby_request([:error, [:user, 1, "class", "invalid", []]]) 45 | assert_raises(BananaPhone::UserError) do 46 | @enc.decode_bert_response(req) 47 | end 48 | end 49 | 50 | should "raise a ProxyError error when proxy level error is returned" do 51 | req = @enc.encode_ruby_request([:error, [:proxy, 1, "class", "invalid", []]]) 52 | assert_raises(BananaPhone::ProxyError) do 53 | @enc.decode_bert_response(req) 54 | end 55 | end 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /lib/banana_phone/action.rb: -------------------------------------------------------------------------------- 1 | module BananaPhone 2 | class Action 3 | include Encodes 4 | 5 | def initialize(svc, req, mod, fun, args) 6 | @svc = svc 7 | @req = req 8 | @mod = mod 9 | @fun = fun 10 | @args = args 11 | end 12 | 13 | def execute 14 | bert_request = encode_ruby_request([@req.kind, @mod, @fun, @args]) 15 | bert_response = transaction(bert_request) 16 | decode_bert_response(bert_response) 17 | end 18 | 19 | #private 20 | 21 | def write(sock, bert) 22 | sock.write([bert.bytesize].pack("N")) 23 | sock.write(bert) 24 | end 25 | 26 | def read(sock, len, timeout) 27 | data, size = [], 0 28 | while size < len 29 | r, w, e = IO.select([sock], [], [], timeout) 30 | raise Errno::EAGAIN if r.nil? 31 | msg, sender = sock.recvfrom(len - size) 32 | raise Errno::ECONNRESET if msg.size == 0 33 | size += msg.size 34 | data << msg 35 | end 36 | data.join '' 37 | end 38 | 39 | def transaction(bert_request) 40 | timeout = @svc.timeout && Float(@svc.timeout) 41 | sock = connect_to(@svc.host, @svc.port, timeout) 42 | 43 | if @req.options 44 | if @req.options[:cache] && @req.options[:cache][0] == :validation 45 | token = @req.options[:cache][1] 46 | info_bert = encode_ruby_request([:info, :cache, [:validation, token]]) 47 | write(sock, info_bert) 48 | end 49 | end 50 | 51 | write(sock, bert_request) 52 | lenheader = read(sock, 4, timeout) 53 | raise ProtocolError.new(ProtocolError::NO_HEADER) unless lenheader 54 | len = lenheader.unpack('N').first 55 | bert_response = read(sock, len, timeout) 56 | raise ProtocolError.new(ProtocolError::NO_DATA) unless bert_response 57 | sock.close 58 | bert_response 59 | rescue Errno::ECONNREFUSED 60 | raise ConnectionError.new(@svc.host, @svc.port) 61 | rescue Errno::EAGAIN 62 | raise ReadTimeoutError.new(@svc.host, @svc.port, @svc.timeout) 63 | rescue Errno::ECONNRESET 64 | raise ReadError.new(@svc.host, @svc.port) 65 | end 66 | 67 | # Creates a socket object which does speedy, non-blocking reads 68 | # and can perform reliable read timeouts. 69 | # 70 | # Raises Timeout::Error on timeout. 71 | # 72 | # +host+ String address of the target TCP server 73 | # +port+ Integer port of the target TCP server 74 | # +timeout+ Optional Integer (in seconds) of the read timeout 75 | def connect_to(host, port, timeout = nil) 76 | addr = Socket.getaddrinfo(host, nil, Socket::AF_INET) 77 | sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0) 78 | sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1 79 | 80 | if timeout 81 | secs = Integer(timeout) 82 | usecs = Integer((timeout - secs) * 1_000_000) 83 | optval = [secs, usecs].pack("l_2") 84 | sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval 85 | sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval 86 | end 87 | 88 | sock.connect(Socket.pack_sockaddr_in(port, addr[0][3])) 89 | sock 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/action_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ActionTest < MiniTest::Should::TestCase 4 | context "An Action" do 5 | setup do 6 | @svc = BananaPhone::Service.new('localhost', 9941) 7 | @req = @svc.call 8 | end 9 | 10 | should "be created with a Service, module name, fun name, and args" do 11 | assert BananaPhone::Action.new(@svc, @req, :mymod, :myfun, [1, 2]).is_a?(BananaPhone::Action) 12 | end 13 | end 14 | 15 | context "An Action instance" do 16 | setup do 17 | @svc = BananaPhone::Service.new('localhost', 9941) 18 | @req = @svc.call 19 | @enc = Enc.new 20 | end 21 | 22 | should "call with single-arity" do 23 | req = @enc.encode_ruby_request([:call, :mymod, :myfun, [1]]) 24 | res = @enc.encode_ruby_request([:reply, 2]) 25 | call = BananaPhone::Action.new(@svc, @req, :mymod, :myfun, [1]) 26 | call.expects(:transaction).with(req).returns(res) 27 | assert_equal 2, call.execute 28 | end 29 | 30 | should "call with single-arity array" do 31 | req = @enc.encode_ruby_request([:call, :mymod, :myfun, [[1, 2, 3]]]) 32 | res = @enc.encode_ruby_request([:reply, [4, 5, 6]]) 33 | call = BananaPhone::Action.new(@svc, @req, :mymod, :myfun, [[1, 2, 3]]) 34 | call.expects(:transaction).with(req).returns(res) 35 | assert_equal [4, 5, 6], call.execute 36 | end 37 | 38 | should "call with multi-arity" do 39 | req = @enc.encode_ruby_request([:call, :mymod, :myfun, [1, 2, 3]]) 40 | res = @enc.encode_ruby_request([:reply, [4, 5, 6]]) 41 | call = BananaPhone::Action.new(@svc, @req, :mymod, :myfun, [1, 2, 3]) 42 | call.expects(:transaction).with(req).returns(res) 43 | assert_equal [4, 5, 6], call.execute 44 | end 45 | 46 | context "sync_request" do 47 | setup do 48 | @svc = BananaPhone::Service.new('localhost', 9941) 49 | @req = @svc.call 50 | @call = BananaPhone::Action.new(@svc, @req, :mymod, :myfun, []) 51 | end 52 | 53 | should "read and write BananaPack from the socket" do 54 | io = stub() 55 | io.expects(:write).with("\000\000\000\003") 56 | io.expects(:write).with("foo") 57 | @call.expects(:read).with(io, 4, nil).returns("\000\000\000\003") 58 | @call.expects(:read).with(io, 3, nil).returns("bar") 59 | io.expects(:close) 60 | @call.expects(:connect_to).returns(io) 61 | assert_equal "bar", @call.transaction("foo") 62 | end 63 | 64 | should "raise a ProtocolError when the length is invalid" do 65 | io = stub() 66 | io.expects(:write).with("\000\000\000\003") 67 | io.expects(:write).with("foo") 68 | @call.expects(:read).with(io, 4, nil).returns(nil) 69 | @call.expects(:connect_to).returns(io) 70 | begin 71 | @call.transaction("foo") 72 | fail "Should have thrown an error" 73 | rescue BananaPhone::ProtocolError => e 74 | assert_equal 0, e.code 75 | end 76 | end 77 | 78 | should "raise a ProtocolError when the data is invalid" do 79 | io = stub() 80 | io.expects(:write).with("\000\000\000\003") 81 | io.expects(:write).with("foo") 82 | @call.expects(:read).with(io, 4, nil).returns("\000\000\000\003") 83 | @call.expects(:read).with(io, 3, nil).returns(nil) 84 | @call.expects(:connect_to).returns(io) 85 | begin 86 | @call.transaction("foo") 87 | fail "Should have thrown an error" 88 | rescue BananaPhone::ProtocolError => e 89 | assert_equal 1, e.code 90 | end 91 | end 92 | 93 | should "raise a ReadTimeoutError when the connection times out" do 94 | io = stub() 95 | io.expects(:write).with("\000\000\000\003") 96 | io.expects(:write).with("foo") 97 | @call.expects(:read).with(io, 4, nil).raises(Errno::EAGAIN) 98 | @call.expects(:connect_to).returns(io) 99 | begin 100 | @call.transaction("foo") 101 | fail "Should have thrown an error" 102 | rescue BananaPhone::ReadTimeoutError => e 103 | assert_equal 0, e.code 104 | assert_equal 'localhost', e.host 105 | assert_equal 9941, e.port 106 | end 107 | end 108 | 109 | should "raise a ReadError when the socket becomes unreadable" do 110 | io = stub() 111 | io.expects(:write).with("\000\000\000\003") 112 | io.expects(:write).with("foo") 113 | @call.expects(:read).with(io, 4, nil).raises(Errno::ECONNRESET) 114 | @call.expects(:connect_to).returns(io) 115 | begin 116 | @call.transaction("foo") 117 | fail "Should have thrown an error" 118 | rescue BananaPhone::ReadError => e 119 | assert_equal 0, e.code 120 | assert_equal 'localhost', e.host 121 | assert_equal 9941, e.port 122 | end 123 | end 124 | end 125 | end 126 | end 127 | --------------------------------------------------------------------------------