Adds ruby versions of the interop server and client

- Adds the service and message classes generated from beefcake and a patched proto compiler
- Adds an interop client that uses these service and message classes
- Adds an interop server that implement the service description

TESTED interop client works with the interop server
	Change on 2014/12/08 by temiola <temiola@google.com>
-------------
Created by MOE: http://code.google.com/p/moe-java
MOE_MIGRATED_REVID=81615139
pull/1/merge
temiola 10 years ago committed by Nicolas Noble
parent 5ef51949bc
commit ba22e87c68
  1. 11
      src/ruby/bin/interop/README.md
  2. 229
      src/ruby/bin/interop/interop_client.rb
  3. 185
      src/ruby/bin/interop/interop_server.rb
  4. 14
      src/ruby/bin/interop/net/proto2/bridge/proto/message_set.pb.rb
  5. 12
      src/ruby/bin/interop/net/proto2/proto/empty.pb.rb
  6. 94
      src/ruby/bin/interop/third_party/stubby/testing/proto/messages.pb.rb
  7. 30
      src/ruby/bin/interop/third_party/stubby/testing/proto/test.pb.rb
  8. 1
      src/ruby/grpc.gemspec
  9. 4
      src/ruby/lib/grpc/generic/rpc_desc.rb

@ -0,0 +1,11 @@
Interop test protos
===================
These were generated by a patched version of beefcake and a patched version of
protoc.
- set up and access of the patched versions is described in ../../README.md
The actual test proto is found in Google3 at
- third_party/stubby/testing/proto/test.proto

@ -0,0 +1,229 @@
# Copyright 2014, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#!/usr/bin/env ruby
# interop_client is a testing tool that accesses a gRPC interop testing
# server and runs a test on it.
#
# Helps validate interoperation b/w different gRPC implementations.
#
# Usage: $ path/to/interop_client.rb --server_host=<hostname> \
# --server_port=<port> \
# --test_case=<testcase_name>
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
require 'optparse'
require 'minitest'
require 'minitest/assertions'
require 'grpc'
require 'grpc/generic/client_stub'
require 'grpc/generic/service'
require 'third_party/stubby/testing/proto/test.pb'
require 'third_party/stubby/testing/proto/messages.pb'
# loads the certificates used to access the test server securely.
def load_test_certs
this_dir = File.expand_path(File.dirname(__FILE__))
data_dir = File.join(File.dirname(File.dirname(this_dir)), 'spec/testdata')
files = ['ca.pem', 'server1.key', 'server1.pem']
files.map { |f| File.open(File.join(data_dir, f)).read }
end
# creates a Credentials from the test certificates.
def test_creds
certs = load_test_certs
creds = GRPC::Core::Credentials.new(certs[0])
end
# creates a test stub that accesses host:port securely.
def create_stub(host, port)
address = "#{host}:#{port}"
stub_opts = {
:creds => test_creds,
GRPC::Core::Channel::SSL_TARGET => 'foo.test.google.com',
}
logger.info("... connecting securely to #{address}")
stub = Grpc::Testing::TestService::Stub.new(address, **stub_opts)
end
# produces a string of null chars (\0) of length l.
def nulls(l)
raise 'requires #{l} to be +ve' if l < 0
[].pack('x' * l)
end
# defines methods corresponding to each interop test case.
class NamedTests
include Minitest::Assertions
include Grpc::Testing
include Grpc::Testing::PayloadType
attr_accessor :assertions # required by Minitest::Assertions
def initialize(stub)
@assertions = 0 # required by Minitest::Assertions
@stub = stub
end
# TESTING
# PASSED
# FAIL
# ruby server: fails beefcake throws on deserializing the 0-length message
def empty_unary
resp = @stub.empty_call(Proto2::Empty.new)
assert resp.is_a?(Proto::Empty), 'empty_unary: invalid response'
p 'OK: empty_unary'
end
# TESTING
# PASSED
# ruby server
# FAILED
def large_unary
req_size, wanted_response_size = 271828, 314159
payload = Payload.new(:type => COMPRESSABLE, :body => nulls(req_size))
req = SimpleRequest.new(:response_type => COMPRESSABLE,
:response_size => wanted_response_size,
:payload => payload)
resp = @stub.unary_call(req)
assert_equal(wanted_response_size, resp.payload.body.length,
'large_unary: payload had the wrong length')
assert_equal(nulls(wanted_response_size), resp.payload.body,
'large_unary: payload content is invalid')
p 'OK: large_unary'
end
# TESTING:
# PASSED
# ruby server
# FAILED
def client_streaming
msg_sizes = [27182, 8, 1828, 45904]
wanted_aggregate_size = 74922
reqs = msg_sizes.map do |x|
req = Payload.new(:body => nulls(x))
StreamingInputCallRequest.new(:payload => req)
end
resp = @stub.streaming_input_call(reqs)
assert_equal(wanted_aggregate_size, resp.aggregated_payload_size,
'client_streaming: aggregate payload size is incorrect')
p 'OK: client_streaming'
end
# TESTING:
# PASSED
# ruby server
# FAILED
def server_streaming
msg_sizes = [31415, 9, 2653, 58979]
response_spec = msg_sizes.map { |s| ResponseParameters.new(:size => s) }
req = StreamingOutputCallRequest.new(:response_type => COMPRESSABLE,
:response_parameters => response_spec)
resps = @stub.streaming_output_call(req)
resps.each_with_index do |r, i|
assert i < msg_sizes.length, 'too many responses'
assert_equal(COMPRESSABLE, r.payload.type, 'payload type is wrong')
assert_equal(msg_sizes[i], r.payload.body.length,
'payload body #{i} has the wrong length')
end
p 'OK: server_streaming'
end
# TESTING:
# PASSED
# ruby server
# FAILED
#
# TODO(temiola): update this test to stay consistent with the java test's
# interpretation of the test spec.
def ping_pong
req_cls, param_cls= StreamingOutputCallRequest, ResponseParameters # short
msg_sizes = [[27182, 31415], [8, 9], [1828, 2653], [45904, 58979]]
reqs = msg_sizes.map do |x|
req_size, resp_size = x
req_cls.new(:payload => Payload.new(:body => nulls(req_size)),
:response_type => COMPRESSABLE,
:response_parameters => param_cls.new(:size => resp_size))
end
resps = @stub.full_duplex_call(reqs)
resps.each_with_index do |r, i|
assert i < msg_sizes.length, 'too many responses'
assert_equal(COMPRESSABLE, r.payload.type, 'payload type is wrong')
assert_equal(msg_sizes[i][1], r.payload.body.length,
'payload body #{i} has the wrong length')
end
p 'OK ping_pong'
end
end
# validates the the command line options, returning them as a Hash.
def parse_options
options = {
'server_host' => nil,
'server_port' => nil,
'test_case' => nil,
}
OptionParser.new do |opts|
opts.banner = 'Usage: --server_host <server_host> --server_port server_port'
opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
options['server_host'] = v
end
opts.on('--server_port SERVER_PORT', 'server port') do |v|
options['server_port'] = v
end
# instance_methods(false) gives only the methods defined in that class
test_cases = NamedTests.instance_methods(false).map { |t| t.to_s }
test_case_list = test_cases.join(',')
opts.on("--test_case CODE", test_cases, {}, "select a test_case",
" (#{test_case_list})") do |v|
options['test_case'] = v
end
end.parse!
['server_host', 'server_port', 'test_case'].each do |arg|
if options[arg].nil?
raise OptionParser::MissingArgument.new("please specify --#{arg}")
end
end
options
end
def main
opts = parse_options
stub = create_stub(opts['server_host'], opts['server_port'])
NamedTests.new(stub).method(opts['test_case']).call
end
main

@ -0,0 +1,185 @@
# Copyright 2014, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#!/usr/bin/env ruby
#
# interop_server is a Testing app that runs a gRPC interop testing server.
#
# It helps validate interoperation b/w gRPC in different environments
#
# Helps validate interoperation b/w different gRPC implementations.
#
# Usage: $ path/to/interop_server.rb --port
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
require 'forwardable'
require 'optparse'
require 'grpc'
require 'grpc/generic/service'
require 'grpc/generic/rpc_server'
require 'third_party/stubby/testing/proto/test.pb'
require 'third_party/stubby/testing/proto/messages.pb'
# loads the certificates by the test server.
def load_test_certs
this_dir = File.expand_path(File.dirname(__FILE__))
data_dir = File.join(File.dirname(File.dirname(this_dir)), 'spec/testdata')
files = ['ca.pem', 'server1.key', 'server1.pem']
files.map { |f| File.open(File.join(data_dir, f)).read }
end
# creates a ServerCredentials from the test certificates.
def test_server_creds
certs = load_test_certs
server_creds = GRPC::Core::ServerCredentials.new(nil, certs[1], certs[2])
end
# produces a string of null chars (\0) of length l.
def nulls(l)
raise 'requires #{l} to be +ve' if l < 0
[].pack('x' * l)
end
# A EnumeratorQueue wraps a Queue yielding the items added to it via each_item.
class EnumeratorQueue
extend Forwardable
def_delegators :@q, :push
def initialize(sentinel)
@q = Queue.new
@sentinel = sentinel
end
def each_item
return enum_for(:each_item) unless block_given?
loop do
r = @q.pop
break if r.equal?(@sentinel)
raise r if r.is_a?Exception
yield r
end
end
end
# A runnable implementation of the schema-specified testing service, with each
# service method implemented as required by the interop testing spec.
class TestTarget < Grpc::Testing::TestService::Service
include Grpc::Testing
include Grpc::Testing::PayloadType
def empty_call(empty, call)
Proto::Empty.new
end
def unary_call(simple_req, call)
req_size = simple_req.response_size
SimpleResponse.new(:payload => Payload.new(:type => COMPRESSABLE,
:body => nulls(req_size)))
end
def streaming_input_call(call)
sizes = call.each_remote_read.map { |x| x.payload.body.length }
sum = sizes.inject { |sum,x| sum + x }
StreamingInputCallResponse.new(:aggregated_payload_size => sum)
end
def streaming_output_call(req, call)
cls = StreamingOutputCallResponse
req.response_parameters.map do |p|
cls.new(:payload => Payload.new(:type => req.response_type,
:body => nulls(p.size)))
end
end
def full_duplex_call(reqs)
# reqs is a lazy Enumerator of the requests sent by the client.
q = EnumeratorQueue.new(self)
cls = StreamingOutputCallResponse
t = Thread.new do
begin
reqs.each do |req|
logger.info("read #{req.inspect}")
resp_size = req.response_parameters[0].size
resp = cls.new(:payload => Payload.new(:type => req.response_type,
:body => nulls(resp_size)))
q.push(resp)
end
logger.info('finished reads')
q.push(self)
rescue StandardError => e
q.push(e) # share the exception with the enumerator
end
end
q.each_item
end
def half_duplex_call(reqs)
# TODO(temiola): clarify the behaviour of the half_duplex_call, it's not
# currently used in any tests
full_duplex_call(reqs)
end
end
# validates the the command line options, returning them as a Hash.
def parse_options
options = {
'port' => nil,
}
OptionParser.new do |opts|
opts.banner = 'Usage: --port port'
opts.on('--port PORT', 'server port') do |v|
options['port'] = v
end
end.parse!
if options['port'].nil?
raise OptionParser::MissingArgument.new("please specify --port")
end
options
end
def main
opts = parse_options
host = "0.0.0.0:#{opts['port']}"
s = GRPC::RpcServer.new(creds: test_server_creds)
s.add_http2_port(host, true)
logger.info("... running securely on #{host}")
s.handle(TestTarget)
s.run
end
main

@ -0,0 +1,14 @@
## Generated from net/proto2/bridge/proto/message_set.proto for proto2.bridge
require 'beefcake'
module Proto2
module Bridge
class MessageSet
include Beefcake::Message
end
class MessageSet
end
end
end

@ -0,0 +1,12 @@
## Generated from net/proto2/proto/empty.proto for proto2
require 'beefcake'
module Proto2
class Empty
include Beefcake::Message
end
class Empty
end
end

@ -0,0 +1,94 @@
## Generated from third_party/stubby/testing/proto/messages.proto for grpc.testing
require 'beefcake'
require 'net/proto2/bridge/proto/message_set.pb'
module Grpc
module Testing
module PayloadType
COMPRESSABLE = 0
UNCOMPRESSABLE = 1
RANDOM = 2
end
class Payload
include Beefcake::Message
end
class SimpleRequest
include Beefcake::Message
end
class SimpleResponse
include Beefcake::Message
end
class SimpleContext
include Beefcake::Message
end
class StreamingInputCallRequest
include Beefcake::Message
end
class StreamingInputCallResponse
include Beefcake::Message
end
class ResponseParameters
include Beefcake::Message
end
class StreamingOutputCallRequest
include Beefcake::Message
end
class StreamingOutputCallResponse
include Beefcake::Message
end
class Payload
optional :type, PayloadType, 1
optional :body, :bytes, 2
end
class SimpleRequest
optional :response_type, PayloadType, 1
optional :response_size, :int32, 2
optional :payload, Payload, 3
end
class SimpleResponse
optional :payload, Payload, 1
optional :effective_gaia_user_id, :int64, 2
end
class SimpleContext
optional :value, :string, 1
end
class StreamingInputCallRequest
optional :payload, Payload, 1
end
class StreamingInputCallResponse
optional :aggregated_payload_size, :int32, 1
end
class ResponseParameters
optional :size, :int32, 1
optional :interval_us, :int32, 2
end
class StreamingOutputCallRequest
optional :response_type, PayloadType, 1
repeated :response_parameters, ResponseParameters, 2
optional :payload, Payload, 3
end
class StreamingOutputCallResponse
optional :payload, Payload, 1
end
end
end

@ -0,0 +1,30 @@
## Generated from third_party/stubby/testing/proto/test.proto for grpc.testing
require 'beefcake'
require 'grpc'
require 'third_party/stubby/testing/proto/messages.pb'
require 'net/proto2/proto/empty.pb'
module Grpc
module Testing
module TestService
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
rpc :EmptyCall, Proto2::Empty, Proto2::Empty
rpc :UnaryCall, SimpleRequest, SimpleResponse
rpc :StreamingOutputCall, StreamingOutputCallRequest, stream(StreamingOutputCallResponse)
rpc :StreamingInputCall, stream(StreamingInputCallRequest), StreamingInputCallResponse
rpc :FullDuplexCall, stream(StreamingOutputCallRequest), stream(StreamingOutputCallResponse)
rpc :HalfDuplexCall, stream(StreamingOutputCallRequest), stream(StreamingOutputCallResponse)
end
Stub = Service.rpc_stub_class
end
end
end

@ -20,6 +20,7 @@ Gem::Specification.new do |s|
s.add_dependency 'xray'
s.add_dependency 'logging', '~> 1.8'
s.add_dependency 'beefcake', '~> 1.1'
s.add_dependency 'minitest', '~> 5.4' # not a dev dependency, used by the interop tests
s.add_development_dependency "bundler", "~> 1.7"
s.add_development_dependency "rake", "~> 10.0"

@ -90,7 +90,7 @@ module GRPC
# error code and detail message.
logger.debug("app error: #{active_call}, status:#{e.code}:#{e.details}")
send_status(active_call, e.code, e.details)
rescue CallError => e
rescue Core::CallError => e
# This is raised by GRPC internals but should rarely, if ever happen.
# Log it, but don't notify the other endpoint..
logger.warn("failed call: #{active_call}\n#{e}")
@ -99,7 +99,7 @@ module GRPC
# event. Send a status of deadline exceeded
logger.warn("late call: #{active_call}")
send_status(active_call, DEADLINE_EXCEEDED, 'late')
rescue EventError => e
rescue Core::EventError => e
# This is raised by GRPC internals but should rarely, if ever happen.
# Log it, but don't notify the other endpoint..
logger.warn("failed call: #{active_call}\n#{e}")

Loading…
Cancel
Save