├── VERSION ├── .gitignore ├── .document ├── test ├── test_helper.rb ├── request_test.rb ├── mod_test.rb ├── error_test.rb ├── service_test.rb ├── encodes_test.rb └── action_test.rb ├── lib ├── bertrpc │ ├── mod.rb │ ├── request.rb │ ├── service.rb │ ├── encodes.rb │ ├── errors.rb │ └── action.rb └── bertrpc.rb ├── History.txt ├── LICENSE ├── README.md ├── Rakefile └── bertrpc.gemspec /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage 4 | rdoc 5 | pkg 6 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.md 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | require 'shoulda' 4 | require 'mocha' 5 | 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 8 | require 'bertrpc' 9 | 10 | class Enc 11 | include BERTRPC::Encodes 12 | end -------------------------------------------------------------------------------- /lib/bertrpc/mod.rb: -------------------------------------------------------------------------------- 1 | module BERTRPC 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 -------------------------------------------------------------------------------- /lib/bertrpc/request.rb: -------------------------------------------------------------------------------- 1 | module BERTRPC 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/bertrpc.rb: -------------------------------------------------------------------------------- 1 | require 'bert' 2 | require 'socket' 3 | require 'net/protocol' 4 | 5 | require 'bertrpc/service' 6 | require 'bertrpc/request' 7 | require 'bertrpc/mod' 8 | require 'bertrpc/encodes' 9 | require 'bertrpc/action' 10 | require 'bertrpc/errors' 11 | 12 | module BERTRPC 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 -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | = 1.1.2 / 2009-11-27 2 | * Minor Changes 3 | * Add useful debugging information to ReadTimeoutError 4 | * Switch to using raw socket timeouts over buffered io (Linux only) 5 | * Add BERTRPC::VERSION and BERTRPC.version 6 | 7 | = 1.1.1 / 2009-10-28 8 | Major Changes 9 | * Require bert-1.1.0 or greater 10 | * Remove dependency on Erlectricity 11 | 12 | = 1.1.0 / 2009-10-27 13 | * Add read socket timeout 14 | 15 | = 1.0.0 / 2009-10-19 16 | * No changes. Production ready! 17 | 18 | = 0.4.0 / 2009-10-08 19 | * Major changes 20 | * Convert to use BERT gem. -------------------------------------------------------------------------------- /test/request_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RequestTest < Test::Unit::TestCase 4 | context "A Request" do 5 | setup do 6 | @svc = BERTRPC::Service.new('localhost', 9941) 7 | end 8 | 9 | should "be created with a Service and type" do 10 | assert BERTRPC::Request.new(@svc, :call, nil).is_a?(BERTRPC::Request) 11 | end 12 | end 13 | 14 | context "A Request instance" do 15 | setup do 16 | svc = BERTRPC::Service.new('localhost', 9941) 17 | @req = BERTRPC::Request.new(@svc, :call, nil) 18 | end 19 | 20 | should "return a Mod instance" do 21 | assert @req.myfun.is_a?(BERTRPC::Mod) 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /lib/bertrpc/service.rb: -------------------------------------------------------------------------------- 1 | module BERTRPC 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 < Test::Unit::TestCase 4 | context "A Mod" do 5 | setup do 6 | @svc = BERTRPC::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 BERTRPC::Mod.new(@svc, @req, :mymod).is_a?(BERTRPC::Mod) 12 | end 13 | end 14 | 15 | context "A Mod instance" do 16 | setup do 17 | @svc = BERTRPC::Service.new('localhost', 9941) 18 | @req = @svc.call 19 | @mod = BERTRPC::Mod.new(@svc, @req, :mymod) 20 | end 21 | 22 | should "call execute on the type" do 23 | m = mock(:execute => nil) 24 | BERTRPC::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 | BERTRPC::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 < Test::Unit::TestCase 4 | context "Errors in general" do 5 | should "be creatable with just a message string" do 6 | begin 7 | raise BERTRPC::BERTRPCError.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 BERTRPC::BERTRPCError.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 BERTRPC::BERTRPCError.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/bertrpc/encodes.rb: -------------------------------------------------------------------------------- 1 | module BERTRPC 2 | module Encodes 3 | def encode_ruby_request(ruby_request) 4 | BERT.encode(ruby_request) 5 | end 6 | 7 | def decode_bert_response(bert_response) 8 | ruby_response = BERT.decode(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) 2009 Tom Preston-Werner 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 < Test::Unit::TestCase 4 | context "A Service" do 5 | should "be creatable with host and port" do 6 | svc = BERTRPC::Service.new('localhost', 9941) 7 | assert svc.is_a?(BERTRPC::Service) 8 | end 9 | 10 | should "be creatable with host, port, and timeout" do 11 | svc = BERTRPC::Service.new('localhost', 9941, 5) 12 | assert svc.is_a?(BERTRPC::Service) 13 | end 14 | end 15 | 16 | context "A Service Instance's" do 17 | setup do 18 | @svc = BERTRPC::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?(BERTRPC::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?(BERTRPC::Request) 43 | assert_equal :cast, req.kind 44 | end 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /lib/bertrpc/errors.rb: -------------------------------------------------------------------------------- 1 | module BERTRPC 2 | class BERTRPCError < 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 | end 17 | 18 | self.code = code 19 | super(message) 20 | end 21 | end 22 | 23 | class RemoteError < StandardError 24 | 25 | end 26 | 27 | class ConnectionError < BERTRPCError 28 | 29 | end 30 | 31 | # Raised when we don't get a response from a server in a timely 32 | # manner. This typically occurs in spite of a successful connection. 33 | class ReadTimeoutError < BERTRPCError 34 | attr_reader :host, :port, :timeout 35 | def initialize(host, port, timeout) 36 | @host, @port, @timeout = host, port, timeout 37 | super("No response from #{host}:#{port} in #{timeout}s") 38 | end 39 | end 40 | 41 | class ProtocolError < BERTRPCError 42 | NO_HEADER = [0, "Unable to read length header from server."] 43 | NO_DATA = [1, "Unable to read data from server."] 44 | end 45 | 46 | class ServerError < BERTRPCError 47 | 48 | end 49 | 50 | class UserError < BERTRPCError 51 | 52 | end 53 | 54 | class ProxyError < BERTRPCError 55 | 56 | end 57 | 58 | class InvalidOption < BERTRPCError 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BERTRPC 2 | ======= 3 | 4 | By Tom Preston-Werner (tom@mojombo.com) 5 | 6 | BERT-RPC client library for Ruby. Makes it ridiculously simple to interface 7 | with BERT-RPC servers. 8 | 9 | See the full BERT-RPC specification at [bert-rpc.org](http://bert-rpc.org). 10 | 11 | This library currently only supports the following BERT-RPC features: 12 | 13 | * `call` requests 14 | * `cast` requests 15 | 16 | BERTRPC was developed for GitHub and is currently in production use performing 17 | millions of RPC requests every day. The stability and performance have been 18 | exemplary. 19 | 20 | 21 | Installation 22 | ------------ 23 | 24 | $ gem install bertrpc -s http://gemcutter.org 25 | 26 | 27 | Examples 28 | -------- 29 | 30 | Require the library and create a service: 31 | 32 | require 'bertrpc' 33 | svc = BERTRPC::Service.new('localhost', 9999) 34 | 35 | Make a call: 36 | 37 | svc.call.calc.add(1, 2) 38 | # => 3 39 | 40 | The underlying BERT-RPC transaction of the above call is: 41 | 42 | -> {call, calc, add, [1, 2]} 43 | <- {reply, 3} 44 | 45 | Make a cast: 46 | 47 | svc.cast.stats.incr 48 | # => nil 49 | 50 | The underlying BERT-RPC transaction of the above cast is: 51 | 52 | -> {cast, stats, incr, []} 53 | <- {noreply} 54 | 55 | 56 | Documentation 57 | ------------- 58 | 59 | Creating a service: 60 | 61 | # No timeout 62 | svc = BERTRPC::Service.new('localhost', 9999) 63 | 64 | # 10s socket read timeout, raises BERTRPC::ReadTimeoutError 65 | svc = BERTRPC::Service.new('localhost', 9999, 10) 66 | 67 | 68 | Copyright 69 | --------- 70 | 71 | Copyright (c) 2009 Tom Preston-Werner. See LICENSE for details. 72 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "bertrpc" 8 | gem.summary = %Q{BERTRPC is a Ruby BERT-RPC client library.} 9 | gem.email = "tom@mojombo.com" 10 | gem.homepage = "http://github.com/mojombo/bertrpc" 11 | gem.authors = ["Tom Preston-Werner"] 12 | gem.add_dependency('bert', '>= 1.1.0', '< 2.0.0') 13 | # gem is a Gem::Specification... 14 | # see http://www.rubygems.org/read/chapter/20 for additional settings 15 | end 16 | 17 | rescue LoadError 18 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 19 | end 20 | 21 | require 'rake/testtask' 22 | Rake::TestTask.new(:test) do |test| 23 | test.libs << 'lib' << 'test' 24 | test.pattern = 'test/**/*_test.rb' 25 | test.verbose = true 26 | end 27 | 28 | begin 29 | require 'rcov/rcovtask' 30 | Rcov::RcovTask.new do |test| 31 | test.libs << 'test' 32 | test.pattern = 'test/**/*_test.rb' 33 | test.verbose = true 34 | end 35 | rescue LoadError 36 | task :rcov do 37 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 38 | end 39 | end 40 | 41 | task :default => :test 42 | 43 | require 'rake/rdoctask' 44 | Rake::RDocTask.new do |rdoc| 45 | if File.exist?('VERSION.yml') 46 | config = YAML.load(File.read('VERSION.yml')) 47 | version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}" 48 | else 49 | version = "" 50 | end 51 | 52 | rdoc.rdoc_dir = 'rdoc' 53 | rdoc.title = "bertrpc #{version}" 54 | rdoc.rdoc_files.include('README*') 55 | rdoc.rdoc_files.include('lib/**/*.rb') 56 | end 57 | 58 | task :console do 59 | exec('irb -Ilib -rbertrpc') 60 | end -------------------------------------------------------------------------------- /test/encodes_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EncodesTest < Test::Unit::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 BERT-RPC encoded request" do 11 | bert = "\203h\004d\000\004calld\000\005mymodd\000\005myfunl\000\000\000\003a\001a\002a\003j" 12 | assert_equal bert, @enc.encode_ruby_request(t[: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(t[: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(t[: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(t[:error, [:protocol, 1, "class", "invalid", []]]) 31 | assert_raises(BERTRPC::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(t[:error, [:server, 1, "class", "invalid", []]]) 38 | assert_raises(BERTRPC::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(BERTRPC::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(BERTRPC::ProxyError) do 53 | @enc.decode_bert_response(req) 54 | end 55 | end 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /bertrpc.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{bertrpc} 8 | s.version = "1.1.2" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Tom Preston-Werner"] 12 | s.date = %q{2009-11-28} 13 | s.email = %q{tom@mojombo.com} 14 | s.extra_rdoc_files = [ 15 | "LICENSE", 16 | "README.md" 17 | ] 18 | s.files = [ 19 | ".document", 20 | ".gitignore", 21 | "History.txt", 22 | "LICENSE", 23 | "README.md", 24 | "Rakefile", 25 | "VERSION", 26 | "bertrpc.gemspec", 27 | "lib/bertrpc.rb", 28 | "lib/bertrpc/action.rb", 29 | "lib/bertrpc/encodes.rb", 30 | "lib/bertrpc/errors.rb", 31 | "lib/bertrpc/mod.rb", 32 | "lib/bertrpc/request.rb", 33 | "lib/bertrpc/service.rb", 34 | "test/action_test.rb", 35 | "test/encodes_test.rb", 36 | "test/error_test.rb", 37 | "test/mod_test.rb", 38 | "test/request_test.rb", 39 | "test/service_test.rb", 40 | "test/test_helper.rb" 41 | ] 42 | s.homepage = %q{http://github.com/mojombo/bertrpc} 43 | s.rdoc_options = ["--charset=UTF-8"] 44 | s.require_paths = ["lib"] 45 | s.rubygems_version = %q{1.3.5} 46 | s.summary = %q{BERTRPC is a Ruby BERT-RPC client library.} 47 | s.test_files = [ 48 | "test/action_test.rb", 49 | "test/encodes_test.rb", 50 | "test/error_test.rb", 51 | "test/mod_test.rb", 52 | "test/request_test.rb", 53 | "test/service_test.rb", 54 | "test/test_helper.rb" 55 | ] 56 | 57 | if s.respond_to? :specification_version then 58 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 59 | s.specification_version = 3 60 | 61 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 62 | s.add_runtime_dependency(%q, [">= 1.1.0", "< 2.0.0"]) 63 | else 64 | s.add_dependency(%q, [">= 1.1.0", "< 2.0.0"]) 65 | end 66 | else 67 | s.add_dependency(%q, [">= 1.1.0", "< 2.0.0"]) 68 | end 69 | end 70 | 71 | -------------------------------------------------------------------------------- /lib/bertrpc/action.rb: -------------------------------------------------------------------------------- 1 | module BERTRPC 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(t[@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.length].pack("N")) 23 | sock.write(bert) 24 | end 25 | 26 | def transaction(bert_request) 27 | sock = connect_to(@svc.host, @svc.port, @svc.timeout) 28 | 29 | if @req.options 30 | if @req.options[:cache] && @req.options[:cache][0] == :validation 31 | token = @req.options[:cache][1] 32 | info_bert = encode_ruby_request([:info, :cache, [:validation, token]]) 33 | write(sock, info_bert) 34 | end 35 | end 36 | 37 | write(sock, bert_request) 38 | lenheader = sock.read(4) 39 | raise ProtocolError.new(ProtocolError::NO_HEADER) unless lenheader 40 | len = lenheader.unpack('N').first 41 | bert_response = sock.read(len) 42 | raise ProtocolError.new(ProtocolError::NO_DATA) unless bert_response 43 | sock.close 44 | bert_response 45 | rescue Errno::ECONNREFUSED 46 | raise ConnectionError.new("Unable to connect to #{@svc.host}:#{@svc.port}") 47 | rescue Errno::EAGAIN 48 | raise ReadTimeoutError.new(@svc.host, @svc.port, @svc.timeout) 49 | end 50 | 51 | # Creates a socket object which does speedy, non-blocking reads 52 | # and can perform reliable read timeouts. 53 | # 54 | # Raises Timeout::Error on timeout. 55 | # 56 | # +host+ String address of the target TCP server 57 | # +port+ Integer port of the target TCP server 58 | # +timeout+ Optional Integer (in seconds) of the read timeout 59 | def connect_to(host, port, timeout = nil) 60 | sock = TCPSocket.new(host, port) 61 | sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1 62 | 63 | if timeout 64 | secs = Integer(timeout) 65 | usecs = Integer((timeout - secs) * 1_000_000) 66 | optval = [secs, usecs].pack("l_2") 67 | sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval 68 | sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval 69 | end 70 | 71 | sock 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/action_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ActionTest < Test::Unit::TestCase 4 | context "An Action" do 5 | setup do 6 | @svc = BERTRPC::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 BERTRPC::Action.new(@svc, @req, :mymod, :myfun, [1, 2]).is_a?(BERTRPC::Action) 12 | end 13 | end 14 | 15 | context "An Action instance" do 16 | setup do 17 | @svc = BERTRPC::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(t[:call, :mymod, :myfun, [1]]) 24 | res = @enc.encode_ruby_request(t[:reply, 2]) 25 | call = BERTRPC::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(t[:call, :mymod, :myfun, [[1, 2, 3]]]) 32 | res = @enc.encode_ruby_request(t[:reply, [4, 5, 6]]) 33 | call = BERTRPC::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(t[:call, :mymod, :myfun, [1, 2, 3]]) 40 | res = @enc.encode_ruby_request(t[:reply, [4, 5, 6]]) 41 | call = BERTRPC::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 = BERTRPC::Service.new('localhost', 9941) 49 | @req = @svc.call 50 | @call = BERTRPC::Action.new(@svc, @req, :mymod, :myfun, []) 51 | end 52 | 53 | should "read and write BERT-Ps from the socket" do 54 | io = stub() 55 | io.expects(:write).with("\000\000\000\003") 56 | io.expects(:write).with("foo") 57 | io.expects(:read).with(4).returns("\000\000\000\003") 58 | io.expects(:read).with(3).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 | io.expects(:read).with(4).returns(nil) 69 | @call.expects(:connect_to).returns(io) 70 | begin 71 | @call.transaction("foo") 72 | fail "Should have thrown an error" 73 | rescue BERTRPC::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 | io.expects(:read).with(4).returns("\000\000\000\003") 83 | io.expects(:read).with(3).returns(nil) 84 | @call.expects(:connect_to).returns(io) 85 | begin 86 | @call.transaction("foo") 87 | fail "Should have thrown an error" 88 | rescue BERTRPC::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 | io.expects(:read).with(4).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 BERTRPC::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 | end 109 | end 110 | end 111 | --------------------------------------------------------------------------------