diff --git a/src/ruby/.rubocop.yml b/src/ruby/.rubocop.yml
index 1b255f3963a40a1dd6b7590c1e611938546bd32a..312bdca384e538030d18a3f70e3d9645920031ec 100644
--- a/src/ruby/.rubocop.yml
+++ b/src/ruby/.rubocop.yml
@@ -5,7 +5,7 @@ inherit_from: .rubocop_todo.yml
 AllCops:
   Exclude:
     - 'bin/apis/**/*'
-    - 'bin/interop/test/**/*'
     - 'bin/math.rb'
     - 'bin/math_services.rb'
     - 'pb/grpc/health/v1alpha/*'
+    - 'pb/test/**/*'
diff --git a/src/ruby/bin/grpc_ruby_interop_client b/src/ruby/bin/grpc_ruby_interop_client
new file mode 100755
index 0000000000000000000000000000000000000000..e79fd33aa5011146969618b648f7c4a455f0fe5b
--- /dev/null
+++ b/src/ruby/bin/grpc_ruby_interop_client
@@ -0,0 +1,33 @@
+#!/usr/bin/env ruby
+
+# Copyright 2015, 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.
+
+# Provides a gem binary entry point for the interop client.
+require 'test/client'
diff --git a/src/ruby/bin/grpc_ruby_interop_server b/src/ruby/bin/grpc_ruby_interop_server
new file mode 100755
index 0000000000000000000000000000000000000000..656a5f7c998cc8c7be38407d007a9a02ad65a649
--- /dev/null
+++ b/src/ruby/bin/grpc_ruby_interop_server
@@ -0,0 +1,33 @@
+#!/usr/bin/env ruby
+
+# Copyright 2015, 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.
+
+# Provides a gem binary entry point for the interop server
+require 'test/server'
diff --git a/src/ruby/bin/interop/interop_client.rb b/src/ruby/bin/interop/interop_client.rb
index 48b0eaa0f835e9f6005059d70d74cf59b59c016b..239083f37f6e239cdc702ded10bd03a5c54428cb 100755
--- a/src/ruby/bin/interop/interop_client.rb
+++ b/src/ruby/bin/interop/interop_client.rb
@@ -29,6 +29,12 @@
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+# #######################################################################
+# DEPRECATED: The behaviour in this file has been moved to pb/test/client.rb
+#
+# This file remains to support existing tools and scripts that use it.
+# ######################################################################
+#
 # interop_client is a testing tool that accesses a gRPC interop testing
 # server and runs a test on it.
 #
@@ -39,399 +45,7 @@
 #                                    --test_case=<testcase_name>
 
 this_dir = File.expand_path(File.dirname(__FILE__))
-lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
 pb_dir = File.join(File.dirname(File.dirname(this_dir)), 'pb')
-$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
 $LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
-$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
-
-require 'optparse'
-require 'minitest'
-require 'minitest/assertions'
-
-require 'grpc'
-require 'googleauth'
-require 'google/protobuf'
-
-require 'test/proto/empty'
-require 'test/proto/messages'
-require 'test/proto/test_services'
-
-require 'signet/ssl_config'
-
-AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR
-
-# 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
-
-# loads the certificates used to access the test server securely.
-def load_prod_cert
-  fail 'could not find a production cert' if ENV['SSL_CERT_FILE'].nil?
-  GRPC.logger.info("loading prod certs from #{ENV['SSL_CERT_FILE']}")
-  File.open(ENV['SSL_CERT_FILE']).read
-end
-
-# creates SSL Credentials from the test certificates.
-def test_creds
-  certs = load_test_certs
-  GRPC::Core::Credentials.new(certs[0])
-end
-
-# creates SSL Credentials from the production certificates.
-def prod_creds
-  cert_text = load_prod_cert
-  GRPC::Core::Credentials.new(cert_text)
-end
-
-# creates the SSL Credentials.
-def ssl_creds(use_test_ca)
-  return test_creds if use_test_ca
-  prod_creds
-end
-
-# creates a test stub that accesses host:port securely.
-def create_stub(opts)
-  address = "#{opts.host}:#{opts.port}"
-  if opts.secure
-    stub_opts = {
-      :creds => ssl_creds(opts.use_test_ca),
-      GRPC::Core::Channel::SSL_TARGET => opts.host_override
-    }
-
-    # Add service account creds if specified
-    wants_creds = %w(all compute_engine_creds service_account_creds)
-    if wants_creds.include?(opts.test_case)
-      unless opts.oauth_scope.nil?
-        auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
-        stub_opts[:update_metadata] = auth_creds.updater_proc
-      end
-    end
-
-    if opts.test_case == 'oauth2_auth_token'
-      auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
-      kw = auth_creds.updater_proc.call({})  # gives as an auth token
-
-      # use a metadata update proc that just adds the auth token.
-      stub_opts[:update_metadata] = proc { |md| md.merge(kw) }
-    end
-
-    if opts.test_case == 'jwt_token_creds'  # don't use a scope
-      auth_creds = Google::Auth.get_application_default
-      stub_opts[:update_metadata] = auth_creds.updater_proc
-    end
-
-    GRPC.logger.info("... connecting securely to #{address}")
-    Grpc::Testing::TestService::Stub.new(address, **stub_opts)
-  else
-    GRPC.logger.info("... connecting insecurely to #{address}")
-    Grpc::Testing::TestService::Stub.new(address)
-  end
-end
-
-# produces a string of null chars (\0) of length l.
-def nulls(l)
-  fail 'requires #{l} to be +ve' if l < 0
-  [].pack('x' * l).force_encoding('utf-8')
-end
-
-# a PingPongPlayer implements the ping pong bidi test.
-class PingPongPlayer
-  include Minitest::Assertions
-  include Grpc::Testing
-  include Grpc::Testing::PayloadType
-  attr_accessor :assertions # required by Minitest::Assertions
-  attr_accessor :queue
-  attr_accessor :canceller_op
-
-  # reqs is the enumerator over the requests
-  def initialize(msg_sizes)
-    @queue = Queue.new
-    @msg_sizes = msg_sizes
-    @assertions = 0  # required by Minitest::Assertions
-    @canceller_op = nil  # used to cancel after the first response
-  end
-
-  def each_item
-    return enum_for(:each_item) unless block_given?
-    req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters  # short
-    count = 0
-    @msg_sizes.each do |m|
-      req_size, resp_size = m
-      req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
-                        response_type: :COMPRESSABLE,
-                        response_parameters: [p_cls.new(size: resp_size)])
-      yield req
-      resp = @queue.pop
-      assert_equal(:COMPRESSABLE, resp.payload.type, 'payload type is wrong')
-      assert_equal(resp_size, resp.payload.body.length,
-                   "payload body #{count} has the wrong length")
-      p "OK: ping_pong #{count}"
-      count += 1
-      unless @canceller_op.nil?
-        canceller_op.cancel
-        break
-      end
-    end
-  end
-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, args)
-    @assertions = 0  # required by Minitest::Assertions
-    @stub = stub
-    @args = args
-  end
-
-  def empty_unary
-    resp = @stub.empty_call(Empty.new)
-    assert resp.is_a?(Empty), 'empty_unary: invalid response'
-    p 'OK: empty_unary'
-  end
-
-  def large_unary
-    perform_large_unary
-    p 'OK: large_unary'
-  end
-
-  def service_account_creds
-    # ignore this test if the oauth options are not set
-    if @args.oauth_scope.nil?
-      p 'NOT RUN: service_account_creds; no service_account settings'
-      return
-    end
-    json_key = File.read(ENV[AUTH_ENV])
-    wanted_email = MultiJson.load(json_key)['client_email']
-    resp = perform_large_unary(fill_username: true,
-                               fill_oauth_scope: true)
-    assert_equal(wanted_email, resp.username,
-                 'service_account_creds: incorrect username')
-    assert(@args.oauth_scope.include?(resp.oauth_scope),
-           'service_account_creds: incorrect oauth_scope')
-    p 'OK: service_account_creds'
-  end
-
-  def jwt_token_creds
-    json_key = File.read(ENV[AUTH_ENV])
-    wanted_email = MultiJson.load(json_key)['client_email']
-    resp = perform_large_unary(fill_username: true)
-    assert_equal(wanted_email, resp.username,
-                 'service_account_creds: incorrect username')
-    p 'OK: jwt_token_creds'
-  end
-
-  def compute_engine_creds
-    resp = perform_large_unary(fill_username: true,
-                               fill_oauth_scope: true)
-    assert_equal(@args.default_service_account, resp.username,
-                 'compute_engine_creds: incorrect username')
-    p 'OK: compute_engine_creds'
-  end
-
-  def oauth2_auth_token
-    resp = perform_large_unary(fill_username: true,
-                               fill_oauth_scope: true)
-    json_key = File.read(ENV[AUTH_ENV])
-    wanted_email = MultiJson.load(json_key)['client_email']
-    assert_equal(wanted_email, resp.username,
-                 "#{__callee__}: incorrect username")
-    assert(@args.oauth_scope.include?(resp.oauth_scope),
-           "#{__callee__}: incorrect oauth_scope")
-    p "OK: #{__callee__}"
-  end
-
-  def per_rpc_creds
-    auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
-    kw = auth_creds.updater_proc.call({})
-    resp = perform_large_unary(fill_username: true,
-                               fill_oauth_scope: true,
-                               **kw)
-    json_key = File.read(ENV[AUTH_ENV])
-    wanted_email = MultiJson.load(json_key)['client_email']
-    assert_equal(wanted_email, resp.username,
-                 "#{__callee__}: incorrect username")
-    assert(@args.oauth_scope.include?(resp.oauth_scope),
-           "#{__callee__}: incorrect oauth_scope")
-    p "OK: #{__callee__}"
-  end
-
-  def client_streaming
-    msg_sizes = [27_182, 8, 1828, 45_904]
-    wanted_aggregate_size = 74_922
-    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
-
-  def server_streaming
-    msg_sizes = [31_415, 9, 2653, 58_979]
-    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
-
-  def ping_pong
-    msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
-    ppp = PingPongPlayer.new(msg_sizes)
-    resps = @stub.full_duplex_call(ppp.each_item)
-    resps.each { |r| ppp.queue.push(r) }
-    p 'OK: ping_pong'
-  end
-
-  def timeout_on_sleeping_server
-    msg_sizes = [[27_182, 31_415]]
-    ppp = PingPongPlayer.new(msg_sizes)
-    resps = @stub.full_duplex_call(ppp.each_item, timeout: 0.001)
-    resps.each { |r| ppp.queue.push(r) }
-    fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)'
-  rescue GRPC::BadStatus => e
-    assert_equal(e.code, GRPC::Core::StatusCodes::DEADLINE_EXCEEDED)
-    p "OK: #{__callee__}"
-  end
-
-  def empty_stream
-    ppp = PingPongPlayer.new([])
-    resps = @stub.full_duplex_call(ppp.each_item)
-    count = 0
-    resps.each do
-      |r| ppp.queue.push(r)
-      count += 1
-    end
-    assert_equal(0, count, 'too many responses, expect 0')
-    p 'OK: empty_stream'
-  end
-
-  def cancel_after_begin
-    msg_sizes = [27_182, 8, 1828, 45_904]
-    reqs = msg_sizes.map do |x|
-      req = Payload.new(body: nulls(x))
-      StreamingInputCallRequest.new(payload: req)
-    end
-    op = @stub.streaming_input_call(reqs, return_op: true)
-    op.cancel
-    assert_raises(GRPC::Cancelled) { op.execute }
-    assert(op.cancelled, 'call operation should be CANCELLED')
-    p 'OK: cancel_after_begin'
-  end
-
-  def cancel_after_first_response
-    msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
-    ppp = PingPongPlayer.new(msg_sizes)
-    op = @stub.full_duplex_call(ppp.each_item, return_op: true)
-    ppp.canceller_op = op  # causes ppp to cancel after the 1st message
-    assert_raises(GRPC::Cancelled) { op.execute.each { |r| ppp.queue.push(r) } }
-    op.wait
-    assert(op.cancelled, 'call operation was not CANCELLED')
-    p 'OK: cancel_after_first_response'
-  end
-
-  def all
-    all_methods = NamedTests.instance_methods(false).map(&:to_s)
-    all_methods.each do |m|
-      next if m == 'all' || m.start_with?('assert')
-      p "TESTCASE: #{m}"
-      method(m).call
-    end
-  end
-
-  private
-
-  def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
-    req_size, wanted_response_size = 271_828, 314_159
-    payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
-    req = SimpleRequest.new(response_type: :COMPRESSABLE,
-                            response_size: wanted_response_size,
-                            payload: payload)
-    req.fill_username = fill_username
-    req.fill_oauth_scope = fill_oauth_scope
-    resp = @stub.unary_call(req, **kw)
-    assert_equal(:COMPRESSABLE, resp.payload.type,
-                 'large_unary: payload had the wrong type')
-    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')
-    resp
-  end
-end
-
-# Args is used to hold the command line info.
-Args = Struct.new(:default_service_account, :host, :host_override,
-                  :oauth_scope, :port, :secure, :test_case,
-                  :use_test_ca)
-
-# validates the the command line options, returning them as a Hash.
-def parse_args
-  args = Args.new
-  args.host_override = 'foo.test.google.fr'
-  OptionParser.new do |opts|
-    opts.on('--oauth_scope scope',
-            'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
-    opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
-      args['host'] = v
-    end
-    opts.on('--default_service_account email_address',
-            'email address of the default service account') do |v|
-      args['default_service_account'] = v
-    end
-    opts.on('--server_host_override HOST_OVERRIDE',
-            'override host via a HTTP header') do |v|
-      args['host_override'] = v
-    end
-    opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
-    # instance_methods(false) gives only the methods defined in that class
-    test_cases = NamedTests.instance_methods(false).map(&:to_s)
-    test_case_list = test_cases.join(',')
-    opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
-            "  (#{test_case_list})") { |v| args['test_case'] = v }
-    opts.on('-s', '--use_tls', 'require a secure connection?') do |v|
-      args['secure'] = v
-    end
-    opts.on('-t', '--use_test_ca',
-            'if secure, use the test certificate?') do |v|
-      args['use_test_ca'] = v
-    end
-  end.parse!
-  _check_args(args)
-end
-
-def _check_args(args)
-  %w(host port test_case).each do |a|
-    if args[a].nil?
-      fail(OptionParser::MissingArgument, "please specify --#{arg}")
-    end
-  end
-  args
-end
-
-def main
-  opts = parse_args
-  stub = create_stub(opts)
-  NamedTests.new(stub, opts).method(opts['test_case']).call
-end
 
-main
+require 'test/client'
diff --git a/src/ruby/bin/interop/interop_server.rb b/src/ruby/bin/interop/interop_server.rb
index dd9a56914160b7686b395f25b9d8a1aa7b3bcfa1..c6b0d00ec6361daa69d0460b267c516b82d7f602 100755
--- a/src/ruby/bin/interop/interop_server.rb
+++ b/src/ruby/bin/interop/interop_server.rb
@@ -29,6 +29,12 @@
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+# #######################################################################
+# DEPRECATED: The behaviour in this file has been moved to pb/test/server.rb
+#
+# This file remains to support existing tools and scripts that use it.
+# ######################################################################
+#
 # interop_server is a Testing app that runs a gRPC interop testing server.
 #
 # It helps validate interoperation b/w gRPC in different environments
@@ -38,159 +44,7 @@
 # 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')
 pb_dir = File.join(File.dirname(File.dirname(this_dir)), 'pb')
-$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
 $LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
-$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
-
-require 'forwardable'
-require 'optparse'
-
-require 'grpc'
-
-require 'test/proto/empty'
-require 'test/proto/messages'
-require 'test/proto/test_services'
-
-# 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
-  GRPC::Core::ServerCredentials.new(nil, certs[1], certs[2])
-end
-
-# produces a string of null chars (\0) of length l.
-def nulls(l)
-  fail 'requires #{l} to be +ve' if l < 0
-  [].pack('x' * l).force_encoding('utf-8')
-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)
-      fail 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)
-    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 { |s, x| s + 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
-    Thread.new do
-      begin
-        GRPC.logger.info('interop-server: started receiving')
-        reqs.each do |req|
-          resp_size = req.response_parameters[0].size
-          GRPC.logger.info("read a req, response size is #{resp_size}")
-          resp = cls.new(payload: Payload.new(type: req.response_type,
-                                              body: nulls(resp_size)))
-          q.push(resp)
-        end
-        GRPC.logger.info('interop-server: finished receiving')
-        q.push(self)
-      rescue StandardError => e
-        GRPC.logger.info('interop-server: failed')
-        GRPC.logger.warn(e)
-        q.push(e)  # share the exception with the enumerator
-      end
-    end
-    q.each_item
-  end
-
-  def half_duplex_call(reqs)
-    # TODO: update with unique behaviour of the half_duplex_call if that's
-    # ever required by any of the tests.
-    full_duplex_call(reqs)
-  end
-end
-
-# validates the the command line options, returning them as a Hash.
-def parse_options
-  options = {
-    'port' => nil,
-    'secure' => false
-  }
-  OptionParser.new do |opts|
-    opts.banner = 'Usage: --port port'
-    opts.on('--port PORT', 'server port') do |v|
-      options['port'] = v
-    end
-    opts.on('-s', '--use_tls', 'require a secure connection?') do |v|
-      options['secure'] = v
-    end
-  end.parse!
-
-  if options['port'].nil?
-    fail(OptionParser::MissingArgument, 'please specify --port')
-  end
-  options
-end
-
-def main
-  opts = parse_options
-  host = "0.0.0.0:#{opts['port']}"
-  s = GRPC::RpcServer.new
-  if opts['secure']
-    s.add_http2_port(host, test_server_creds)
-    GRPC.logger.info("... running securely on #{host}")
-  else
-    s.add_http2_port(host)
-    GRPC.logger.info("... running insecurely on #{host}")
-  end
-  s.handle(TestTarget)
-  s.run_till_terminated
-end
 
-main
+require 'test/server'
diff --git a/src/ruby/grpc.gemspec b/src/ruby/grpc.gemspec
index 22fafe1b50bd4b4ed524019d6371d6a0d51718ef..bda0352c26f466ac8c741022874456e2aaa5a010 100755
--- a/src/ruby/grpc.gemspec
+++ b/src/ruby/grpc.gemspec
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
   %w(math noproto).each do |b|
     s.executables += ["#{b}_client.rb", "#{b}_server.rb"]
   end
+  s.executables += %w(grpc_ruby_interop_client grpc_ruby_interop_server)
   s.require_paths = %w( bin lib pb )
   s.platform      = Gem::Platform::RUBY
 
diff --git a/src/ruby/pb/test/client.rb b/src/ruby/pb/test/client.rb
new file mode 100755
index 0000000000000000000000000000000000000000..66c78f8bf8fe6e7a6aadee4e3843c674ebc62630
--- /dev/null
+++ b/src/ruby/pb/test/client.rb
@@ -0,0 +1,437 @@
+#!/usr/bin/env ruby
+
+# Copyright 2015, 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.
+
+# 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/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')
+pb_dir = File.dirname(File.dirname(this_dir))
+$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
+$LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
+$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
+
+require 'optparse'
+require 'minitest'
+require 'minitest/assertions'
+
+require 'grpc'
+require 'googleauth'
+require 'google/protobuf'
+
+require 'test/proto/empty'
+require 'test/proto/messages'
+require 'test/proto/test_services'
+
+require 'signet/ssl_config'
+
+AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR
+
+# 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
+
+# loads the certificates used to access the test server securely.
+def load_prod_cert
+  fail 'could not find a production cert' if ENV['SSL_CERT_FILE'].nil?
+  GRPC.logger.info("loading prod certs from #{ENV['SSL_CERT_FILE']}")
+  File.open(ENV['SSL_CERT_FILE']).read
+end
+
+# creates SSL Credentials from the test certificates.
+def test_creds
+  certs = load_test_certs
+  GRPC::Core::Credentials.new(certs[0])
+end
+
+# creates SSL Credentials from the production certificates.
+def prod_creds
+  cert_text = load_prod_cert
+  GRPC::Core::Credentials.new(cert_text)
+end
+
+# creates the SSL Credentials.
+def ssl_creds(use_test_ca)
+  return test_creds if use_test_ca
+  prod_creds
+end
+
+# creates a test stub that accesses host:port securely.
+def create_stub(opts)
+  address = "#{opts.host}:#{opts.port}"
+  if opts.secure
+    stub_opts = {
+      :creds => ssl_creds(opts.use_test_ca),
+      GRPC::Core::Channel::SSL_TARGET => opts.host_override
+    }
+
+    # Add service account creds if specified
+    wants_creds = %w(all compute_engine_creds service_account_creds)
+    if wants_creds.include?(opts.test_case)
+      unless opts.oauth_scope.nil?
+        auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
+        stub_opts[:update_metadata] = auth_creds.updater_proc
+      end
+    end
+
+    if opts.test_case == 'oauth2_auth_token'
+      auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
+      kw = auth_creds.updater_proc.call({})  # gives as an auth token
+
+      # use a metadata update proc that just adds the auth token.
+      stub_opts[:update_metadata] = proc { |md| md.merge(kw) }
+    end
+
+    if opts.test_case == 'jwt_token_creds'  # don't use a scope
+      auth_creds = Google::Auth.get_application_default
+      stub_opts[:update_metadata] = auth_creds.updater_proc
+    end
+
+    GRPC.logger.info("... connecting securely to #{address}")
+    Grpc::Testing::TestService::Stub.new(address, **stub_opts)
+  else
+    GRPC.logger.info("... connecting insecurely to #{address}")
+    Grpc::Testing::TestService::Stub.new(address)
+  end
+end
+
+# produces a string of null chars (\0) of length l.
+def nulls(l)
+  fail 'requires #{l} to be +ve' if l < 0
+  [].pack('x' * l).force_encoding('utf-8')
+end
+
+# a PingPongPlayer implements the ping pong bidi test.
+class PingPongPlayer
+  include Minitest::Assertions
+  include Grpc::Testing
+  include Grpc::Testing::PayloadType
+  attr_accessor :assertions # required by Minitest::Assertions
+  attr_accessor :queue
+  attr_accessor :canceller_op
+
+  # reqs is the enumerator over the requests
+  def initialize(msg_sizes)
+    @queue = Queue.new
+    @msg_sizes = msg_sizes
+    @assertions = 0  # required by Minitest::Assertions
+    @canceller_op = nil  # used to cancel after the first response
+  end
+
+  def each_item
+    return enum_for(:each_item) unless block_given?
+    req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters  # short
+    count = 0
+    @msg_sizes.each do |m|
+      req_size, resp_size = m
+      req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
+                        response_type: :COMPRESSABLE,
+                        response_parameters: [p_cls.new(size: resp_size)])
+      yield req
+      resp = @queue.pop
+      assert_equal(:COMPRESSABLE, resp.payload.type, 'payload type is wrong')
+      assert_equal(resp_size, resp.payload.body.length,
+                   "payload body #{count} has the wrong length")
+      p "OK: ping_pong #{count}"
+      count += 1
+      unless @canceller_op.nil?
+        canceller_op.cancel
+        break
+      end
+    end
+  end
+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, args)
+    @assertions = 0  # required by Minitest::Assertions
+    @stub = stub
+    @args = args
+  end
+
+  def empty_unary
+    resp = @stub.empty_call(Empty.new)
+    assert resp.is_a?(Empty), 'empty_unary: invalid response'
+    p 'OK: empty_unary'
+  end
+
+  def large_unary
+    perform_large_unary
+    p 'OK: large_unary'
+  end
+
+  def service_account_creds
+    # ignore this test if the oauth options are not set
+    if @args.oauth_scope.nil?
+      p 'NOT RUN: service_account_creds; no service_account settings'
+      return
+    end
+    json_key = File.read(ENV[AUTH_ENV])
+    wanted_email = MultiJson.load(json_key)['client_email']
+    resp = perform_large_unary(fill_username: true,
+                               fill_oauth_scope: true)
+    assert_equal(wanted_email, resp.username,
+                 'service_account_creds: incorrect username')
+    assert(@args.oauth_scope.include?(resp.oauth_scope),
+           'service_account_creds: incorrect oauth_scope')
+    p 'OK: service_account_creds'
+  end
+
+  def jwt_token_creds
+    json_key = File.read(ENV[AUTH_ENV])
+    wanted_email = MultiJson.load(json_key)['client_email']
+    resp = perform_large_unary(fill_username: true)
+    assert_equal(wanted_email, resp.username,
+                 'service_account_creds: incorrect username')
+    p 'OK: jwt_token_creds'
+  end
+
+  def compute_engine_creds
+    resp = perform_large_unary(fill_username: true,
+                               fill_oauth_scope: true)
+    assert_equal(@args.default_service_account, resp.username,
+                 'compute_engine_creds: incorrect username')
+    p 'OK: compute_engine_creds'
+  end
+
+  def oauth2_auth_token
+    resp = perform_large_unary(fill_username: true,
+                               fill_oauth_scope: true)
+    json_key = File.read(ENV[AUTH_ENV])
+    wanted_email = MultiJson.load(json_key)['client_email']
+    assert_equal(wanted_email, resp.username,
+                 "#{__callee__}: incorrect username")
+    assert(@args.oauth_scope.include?(resp.oauth_scope),
+           "#{__callee__}: incorrect oauth_scope")
+    p "OK: #{__callee__}"
+  end
+
+  def per_rpc_creds
+    auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
+    kw = auth_creds.updater_proc.call({})
+    resp = perform_large_unary(fill_username: true,
+                               fill_oauth_scope: true,
+                               **kw)
+    json_key = File.read(ENV[AUTH_ENV])
+    wanted_email = MultiJson.load(json_key)['client_email']
+    assert_equal(wanted_email, resp.username,
+                 "#{__callee__}: incorrect username")
+    assert(@args.oauth_scope.include?(resp.oauth_scope),
+           "#{__callee__}: incorrect oauth_scope")
+    p "OK: #{__callee__}"
+  end
+
+  def client_streaming
+    msg_sizes = [27_182, 8, 1828, 45_904]
+    wanted_aggregate_size = 74_922
+    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
+
+  def server_streaming
+    msg_sizes = [31_415, 9, 2653, 58_979]
+    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
+
+  def ping_pong
+    msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
+    ppp = PingPongPlayer.new(msg_sizes)
+    resps = @stub.full_duplex_call(ppp.each_item)
+    resps.each { |r| ppp.queue.push(r) }
+    p 'OK: ping_pong'
+  end
+
+  def timeout_on_sleeping_server
+    msg_sizes = [[27_182, 31_415]]
+    ppp = PingPongPlayer.new(msg_sizes)
+    resps = @stub.full_duplex_call(ppp.each_item, timeout: 0.001)
+    resps.each { |r| ppp.queue.push(r) }
+    fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)'
+  rescue GRPC::BadStatus => e
+    assert_equal(e.code, GRPC::Core::StatusCodes::DEADLINE_EXCEEDED)
+    p "OK: #{__callee__}"
+  end
+
+  def empty_stream
+    ppp = PingPongPlayer.new([])
+    resps = @stub.full_duplex_call(ppp.each_item)
+    count = 0
+    resps.each do |r|
+      ppp.queue.push(r)
+      count += 1
+    end
+    assert_equal(0, count, 'too many responses, expect 0')
+    p 'OK: empty_stream'
+  end
+
+  def cancel_after_begin
+    msg_sizes = [27_182, 8, 1828, 45_904]
+    reqs = msg_sizes.map do |x|
+      req = Payload.new(body: nulls(x))
+      StreamingInputCallRequest.new(payload: req)
+    end
+    op = @stub.streaming_input_call(reqs, return_op: true)
+    op.cancel
+    assert_raises(GRPC::Cancelled) { op.execute }
+    assert(op.cancelled, 'call operation should be CANCELLED')
+    p 'OK: cancel_after_begin'
+  end
+
+  def cancel_after_first_response
+    msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
+    ppp = PingPongPlayer.new(msg_sizes)
+    op = @stub.full_duplex_call(ppp.each_item, return_op: true)
+    ppp.canceller_op = op  # causes ppp to cancel after the 1st message
+    assert_raises(GRPC::Cancelled) { op.execute.each { |r| ppp.queue.push(r) } }
+    op.wait
+    assert(op.cancelled, 'call operation was not CANCELLED')
+    p 'OK: cancel_after_first_response'
+  end
+
+  def all
+    all_methods = NamedTests.instance_methods(false).map(&:to_s)
+    all_methods.each do |m|
+      next if m == 'all' || m.start_with?('assert')
+      p "TESTCASE: #{m}"
+      method(m).call
+    end
+  end
+
+  private
+
+  def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
+    req_size, wanted_response_size = 271_828, 314_159
+    payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
+    req = SimpleRequest.new(response_type: :COMPRESSABLE,
+                            response_size: wanted_response_size,
+                            payload: payload)
+    req.fill_username = fill_username
+    req.fill_oauth_scope = fill_oauth_scope
+    resp = @stub.unary_call(req, **kw)
+    assert_equal(:COMPRESSABLE, resp.payload.type,
+                 'large_unary: payload had the wrong type')
+    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')
+    resp
+  end
+end
+
+# Args is used to hold the command line info.
+Args = Struct.new(:default_service_account, :host, :host_override,
+                  :oauth_scope, :port, :secure, :test_case,
+                  :use_test_ca)
+
+# validates the the command line options, returning them as a Hash.
+def parse_args
+  args = Args.new
+  args.host_override = 'foo.test.google.fr'
+  OptionParser.new do |opts|
+    opts.on('--oauth_scope scope',
+            'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
+    opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
+      args['host'] = v
+    end
+    opts.on('--default_service_account email_address',
+            'email address of the default service account') do |v|
+      args['default_service_account'] = v
+    end
+    opts.on('--server_host_override HOST_OVERRIDE',
+            'override host via a HTTP header') do |v|
+      args['host_override'] = v
+    end
+    opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
+    # instance_methods(false) gives only the methods defined in that class
+    test_cases = NamedTests.instance_methods(false).map(&:to_s)
+    test_case_list = test_cases.join(',')
+    opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
+            "  (#{test_case_list})") { |v| args['test_case'] = v }
+    opts.on('-s', '--use_tls', 'require a secure connection?') do |v|
+      args['secure'] = v
+    end
+    opts.on('-t', '--use_test_ca',
+            'if secure, use the test certificate?') do |v|
+      args['use_test_ca'] = v
+    end
+  end.parse!
+  _check_args(args)
+end
+
+def _check_args(args)
+  %w(host port test_case).each do |a|
+    if args[a].nil?
+      fail(OptionParser::MissingArgument, "please specify --#{a}")
+    end
+  end
+  args
+end
+
+def main
+  opts = parse_args
+  stub = create_stub(opts)
+  NamedTests.new(stub, opts).method(opts['test_case']).call
+end
+
+main
diff --git a/src/ruby/pb/test/server.rb b/src/ruby/pb/test/server.rb
new file mode 100755
index 0000000000000000000000000000000000000000..e2e1ecbd6293879ba863b79873300d3406aabb67
--- /dev/null
+++ b/src/ruby/pb/test/server.rb
@@ -0,0 +1,196 @@
+#!/usr/bin/env ruby
+
+# Copyright 2015, 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.
+
+# 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')
+pb_dir = File.dirname(File.dirname(this_dir))
+$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
+$LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
+$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
+
+require 'forwardable'
+require 'optparse'
+
+require 'grpc'
+
+require 'test/proto/empty'
+require 'test/proto/messages'
+require 'test/proto/test_services'
+
+# 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
+  GRPC::Core::ServerCredentials.new(nil, certs[1], certs[2])
+end
+
+# produces a string of null chars (\0) of length l.
+def nulls(l)
+  fail 'requires #{l} to be +ve' if l < 0
+  [].pack('x' * l).force_encoding('utf-8')
+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)
+      fail 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)
+    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 { |s, x| s + 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
+    Thread.new do
+      begin
+        GRPC.logger.info('interop-server: started receiving')
+        reqs.each do |req|
+          resp_size = req.response_parameters[0].size
+          GRPC.logger.info("read a req, response size is #{resp_size}")
+          resp = cls.new(payload: Payload.new(type: req.response_type,
+                                              body: nulls(resp_size)))
+          q.push(resp)
+        end
+        GRPC.logger.info('interop-server: finished receiving')
+        q.push(self)
+      rescue StandardError => e
+        GRPC.logger.info('interop-server: failed')
+        GRPC.logger.warn(e)
+        q.push(e)  # share the exception with the enumerator
+      end
+    end
+    q.each_item
+  end
+
+  def half_duplex_call(reqs)
+    # TODO: update with unique behaviour of the half_duplex_call if that's
+    # ever required by any of the tests.
+    full_duplex_call(reqs)
+  end
+end
+
+# validates the the command line options, returning them as a Hash.
+def parse_options
+  options = {
+    'port' => nil,
+    'secure' => false
+  }
+  OptionParser.new do |opts|
+    opts.banner = 'Usage: --port port'
+    opts.on('--port PORT', 'server port') do |v|
+      options['port'] = v
+    end
+    opts.on('-s', '--use_tls', 'require a secure connection?') do |v|
+      options['secure'] = v
+    end
+  end.parse!
+
+  if options['port'].nil?
+    fail(OptionParser::MissingArgument, 'please specify --port')
+  end
+  options
+end
+
+def main
+  opts = parse_options
+  host = "0.0.0.0:#{opts['port']}"
+  s = GRPC::RpcServer.new
+  if opts['secure']
+    s.add_http2_port(host, test_server_creds)
+    GRPC.logger.info("... running securely on #{host}")
+  else
+    s.add_http2_port(host)
+    GRPC.logger.info("... running insecurely on #{host}")
+  end
+  s.handle(TestTarget)
+  s.run_till_terminated
+end
+
+main