#!/usr/bin/env ruby

# Copyright 2015 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# 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>

# These lines are required for the generated files to load grpc
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
pb_dir = 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)

require 'optparse'
require 'logger'

require_relative '../../lib/grpc'
require 'googleauth'
require 'google/protobuf'

require_relative '../src/proto/grpc/testing/empty_pb'
require_relative '../src/proto/grpc/testing/messages_pb'
require_relative '../src/proto/grpc/testing/test_services_pb'

AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR

# RubyLogger defines a logger for gRPC based on the standard ruby logger.
module RubyLogger
  def logger
    LOGGER
  end

  LOGGER = Logger.new(STDOUT)
  LOGGER.level = Logger::INFO
end

# GRPC is the general RPC module
module GRPC
  # Inject the noop #logger if no module-level logger method has been injected.
  extend RubyLogger
end

# AssertionError is use to indicate interop test failures.
class AssertionError < RuntimeError; end

# Fails with AssertionError if the block does evaluate to true
def assert(msg = 'unknown cause')
  fail 'No assertion block provided' unless block_given?
  fail AssertionError, msg unless yield
end

# 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 SSL Credentials from the test certificates.
def test_creds
  certs = load_test_certs
  GRPC::Core::ChannelCredentials.new(certs[0])
end

# creates SSL Credentials from the production certificates.
def prod_creds
  GRPC::Core::ChannelCredentials.new()
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}"

  # Provide channel args that request compression by default
  # for compression interop tests
  if ['client_compressed_unary',
      'client_compressed_streaming'].include?(opts.test_case)
    compression_options =
      GRPC::Core::CompressionOptions.new(default_algorithm: :gzip)
    compression_channel_args = compression_options.to_channel_arg_hash
  else
    compression_channel_args = {}
  end

  if opts.secure
    creds = ssl_creds(opts.use_test_ca)
    stub_opts = {
      channel_args: {
        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)
        call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
        creds = creds.compose call_creds
      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.
      call_creds = GRPC::Core::CallCredentials.new(proc { |md| md.merge(kw) })
      creds = creds.compose call_creds
    end

    if opts.test_case == 'jwt_token_creds'  # don't use a scope
      auth_creds = Google::Auth.get_application_default
      call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
      creds = creds.compose call_creds
    end

    GRPC.logger.info("... connecting securely to #{address}")
    stub_opts[:channel_args].merge!(compression_channel_args)
    if opts.test_case == "unimplemented_service"
      Grpc::Testing::UnimplementedService::Stub.new(address, creds, **stub_opts)
    else
      Grpc::Testing::TestService::Stub.new(address, creds, **stub_opts)
    end
  else
    GRPC.logger.info("... connecting insecurely to #{address}")
    if opts.test_case == "unimplemented_service"
      Grpc::Testing::UnimplementedService::Stub.new(
        address,
        :this_channel_is_insecure,
        channel_args: compression_channel_args
      )
    else
      Grpc::Testing::TestService::Stub.new(
        address,
        :this_channel_is_insecure,
        channel_args: compression_channel_args
      )
    end
  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('ascii-8bit')
end

# a PingPongPlayer implements the ping pong bidi test.
class PingPongPlayer
  include Grpc::Testing
  include Grpc::Testing::PayloadType
  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
    @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('payload type is wrong') { :COMPRESSABLE == resp.payload.type }
      assert("payload body #{count} has the wrong length") do
        resp_size == resp.payload.body.length
      end
      p "OK: ping_pong #{count}"
      count += 1
      unless @canceller_op.nil?
        canceller_op.cancel
        break
      end
    end
  end
end

class BlockingEnumerator
  include Grpc::Testing
  include Grpc::Testing::PayloadType

  def initialize(req_size, sleep_time)
    @req_size = req_size
    @sleep_time = sleep_time
  end

  def each_item
    return enum_for(:each_item) unless block_given?
    req_cls = StreamingOutputCallRequest
    req = req_cls.new(payload: Payload.new(body: nulls(@req_size)))
    yield req
    # Sleep until after the deadline should have passed
    sleep(@sleep_time)
  end
end

# Intended to be used to wrap a call_op, and to adjust
# the write flag of the call_op in between messages yielded to it.
class WriteFlagSettingStreamingInputEnumerable
  attr_accessor :call_op

  def initialize(requests_and_write_flags)
    @requests_and_write_flags = requests_and_write_flags
  end

  def each
    @requests_and_write_flags.each do |request_and_flag|
      @call_op.write_flag = request_and_flag[:write_flag]
      yield request_and_flag[:request]
    end
  end
end

# defines methods corresponding to each interop test case.
class NamedTests
  include Grpc::Testing
  include Grpc::Testing::PayloadType
  include GRPC::Core::MetadataKeys

  def initialize(stub, args)
    @stub = stub
    @args = args
  end

  def empty_unary
    resp = @stub.empty_call(Empty.new)
    assert('empty_unary: invalid response') { resp.is_a?(Empty) }
  end

  def large_unary
    perform_large_unary
  end

  def client_compressed_unary
    # first request used also for the probe
    req_size, wanted_response_size = 271_828, 314_159
    expect_compressed = BoolValue.new(value: true)
    payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
    req = SimpleRequest.new(response_type: :COMPRESSABLE,
                            response_size: wanted_response_size,
                            payload: payload,
                            expect_compressed: expect_compressed)

    # send a probe to see if CompressedResponse is supported on the server
    send_probe_for_compressed_request_support do
      request_uncompressed_args = {
        COMPRESSION_REQUEST_ALGORITHM => 'identity'
      }
      @stub.unary_call(req, metadata: request_uncompressed_args)
    end

    # make a call with a compressed message
    resp = @stub.unary_call(req)
    assert('Expected second unary call with compression to work') do
      resp.payload.body.length == wanted_response_size
    end

    # make a call with an uncompressed message
    stub_options = {
      COMPRESSION_REQUEST_ALGORITHM => 'identity'
    }

    req = SimpleRequest.new(
      response_type: :COMPRESSABLE,
      response_size: wanted_response_size,
      payload: payload,
      expect_compressed: BoolValue.new(value: false)
    )

    resp = @stub.unary_call(req, metadata: stub_options)
    assert('Expected second unary call with compression to work') do
      resp.payload.body.length == wanted_response_size
    end
  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("#{__callee__}: bad username") { wanted_email == resp.username }
    assert("#{__callee__}: bad oauth scope") do
      @args.oauth_scope.include?(resp.oauth_scope)
    end
  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("#{__callee__}: bad username") { wanted_email == resp.username }
  end

  def compute_engine_creds
    resp = perform_large_unary(fill_username: true,
                               fill_oauth_scope: true)
    assert("#{__callee__}: bad username") do
      @args.default_service_account == resp.username
    end
  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("#{__callee__}: bad username") { wanted_email == resp.username }
    assert("#{__callee__}: bad oauth scope") do
      @args.oauth_scope.include?(resp.oauth_scope)
    end
  end

  def per_rpc_creds
    auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
    update_metadata = proc do |md|
      kw = auth_creds.updater_proc.call({})
    end

    call_creds = GRPC::Core::CallCredentials.new(update_metadata)

    resp = perform_large_unary(fill_username: true,
                               fill_oauth_scope: true,
                               credentials: call_creds)
    json_key = File.read(ENV[AUTH_ENV])
    wanted_email = MultiJson.load(json_key)['client_email']
    assert("#{__callee__}: bad username") { wanted_email == resp.username }
    assert("#{__callee__}: bad oauth scope") do
      @args.oauth_scope.include?(resp.oauth_scope)
    end
  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("#{__callee__}: aggregate payload size is incorrect") do
      wanted_aggregate_size == resp.aggregated_payload_size
    end
  end

  def client_compressed_streaming
    # first request used also by the probe
    first_request = StreamingInputCallRequest.new(
      payload: Payload.new(type: :COMPRESSABLE, body: nulls(27_182)),
      expect_compressed: BoolValue.new(value: true)
    )

    # send a probe to see if CompressedResponse is supported on the server
    send_probe_for_compressed_request_support do
      request_uncompressed_args = {
        COMPRESSION_REQUEST_ALGORITHM => 'identity'
      }
      @stub.streaming_input_call([first_request],
                                 metadata: request_uncompressed_args)
    end

    second_request = StreamingInputCallRequest.new(
      payload: Payload.new(type: :COMPRESSABLE, body: nulls(45_904)),
      expect_compressed: BoolValue.new(value: false)
    )

    # Create the requests messages and the corresponding write flags
    # for each message
    requests = WriteFlagSettingStreamingInputEnumerable.new([
      { request: first_request,
        write_flag: 0 },
      { request: second_request,
        write_flag: GRPC::Core::WriteFlags::NO_COMPRESS }
    ])

    # Create the call_op, pass it to the requests enumerable, and
    # run the call
    call_op = @stub.streaming_input_call(requests,
                                         return_op: true)
    requests.call_op = call_op
    resp = call_op.execute

    wanted_aggregate_size = 73_086

    assert("#{__callee__}: aggregate payload size is incorrect") do
      wanted_aggregate_size == resp.aggregated_payload_size
    end
  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("#{__callee__}: too many responses") { i < msg_sizes.length }
      assert("#{__callee__}: payload body #{i} has the wrong length") do
        msg_sizes[i] == r.payload.body.length
      end
      assert("#{__callee__}: payload type is wrong") do
        :COMPRESSABLE == r.payload.type
      end
    end
  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) }
  end

  def timeout_on_sleeping_server
    enum = BlockingEnumerator.new(27_182, 2)
    deadline = GRPC::Core::TimeConsts::from_relative_time(1)
    resps = @stub.full_duplex_call(enum.each_item, deadline: deadline)
    resps.each { } # wait to receive each request (or timeout)
    fail 'Should have raised GRPC::DeadlineExceeded'
  rescue GRPC::DeadlineExceeded
  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("#{__callee__}: too many responses expected 0") do
      count == 0
    end
  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
    op.execute
    fail 'Should have raised GRPC:Cancelled'
  rescue GRPC::Cancelled
    assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled? }
  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
    op.execute.each { |r| ppp.queue.push(r) }
    fail 'Should have raised GRPC:Cancelled'
  rescue GRPC::Cancelled
    assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled? }
    op.wait
  end

  def unimplemented_method
    begin
      resp = @stub.unimplemented_call(Empty.new)
    rescue GRPC::Unimplemented => e
      return
    rescue Exception => e
      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
    end
    fail AssertionError, "GRPC::Unimplemented should have been raised. Was not."
  end

  def unimplemented_service
    begin
      resp = @stub.unimplemented_call(Empty.new)
    rescue GRPC::Unimplemented => e
      return
    rescue Exception => e
      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
    end
    fail AssertionError, "GRPC::Unimplemented should have been raised. Was not."
  end

  def status_code_and_message

    # Function wide constants.
    message = "test status method"
    code = GRPC::Core::StatusCodes::UNKNOWN

    # Testing with UnaryCall.
    payload = Payload.new(type: :COMPRESSABLE, body: nulls(1))
    echo_status = EchoStatus.new(code: code, message: message)
    req = SimpleRequest.new(response_type: :COMPRESSABLE,
			    response_size: 1,
			    payload: payload,
			    response_status: echo_status)
    seen_correct_exception = false
    begin
      resp = @stub.unary_call(req)
    rescue GRPC::Unknown => e
      if e.details != message
	      fail AssertionError,
	        "Expected message #{message}. Received: #{e.details}"
      end
      seen_correct_exception = true
    rescue Exception => e
      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
    end

    if not seen_correct_exception
      fail AssertionError, "Did not see expected status from UnaryCall"
    end

    # testing with FullDuplex
    req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters
    duplex_req = req_cls.new(payload: Payload.new(body: nulls(1)),
                  response_type: :COMPRESSABLE,
                  response_parameters: [p_cls.new(size: 1)],
                  response_status: echo_status)
    seen_correct_exception = false
    begin
      resp = @stub.full_duplex_call([duplex_req])
      resp.each { |r| }
    rescue GRPC::Unknown => e
      if e.details != message
        fail AssertionError,
          "Expected message #{message}. Received: #{e.details}"
      end
      seen_correct_exception = true
    rescue Exception => e
      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
    end

    if not seen_correct_exception
      fail AssertionError, "Did not see expected status from FullDuplexCall"
    end

  end


  def custom_metadata

    # Function wide constants
    req_size, wanted_response_size = 271_828, 314_159
    initial_metadata_key = "x-grpc-test-echo-initial"
    initial_metadata_value = "test_initial_metadata_value"
    trailing_metadata_key = "x-grpc-test-echo-trailing-bin"
    trailing_metadata_value = "\x0a\x0b\x0a\x0b\x0a\x0b"

    metadata = {
      initial_metadata_key => initial_metadata_value,
      trailing_metadata_key => trailing_metadata_value
    }

    # Testing with UnaryCall
    payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
    req = SimpleRequest.new(response_type: :COMPRESSABLE,
			    response_size: wanted_response_size,
			    payload: payload)

    op = @stub.unary_call(req, metadata: metadata, return_op: true)
    op.execute
    if not op.metadata.has_key?(initial_metadata_key)
      fail AssertionError, "Expected initial metadata. None received"
    elsif op.metadata[initial_metadata_key] != metadata[initial_metadata_key]
      fail AssertionError, 
             "Expected initial metadata: #{metadata[initial_metadata_key]}. "\
             "Received: #{op.metadata[initial_metadata_key]}"
    end
    if not op.trailing_metadata.has_key?(trailing_metadata_key)
      fail AssertionError, "Expected trailing metadata. None received"
    elsif op.trailing_metadata[trailing_metadata_key] !=
          metadata[trailing_metadata_key]
      fail AssertionError, 
            "Expected trailing metadata: #{metadata[trailing_metadata_key]}. "\
            "Received: #{op.trailing_metadata[trailing_metadata_key]}"
    end

    # Testing with FullDuplex
    req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters
    duplex_req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
                  response_type: :COMPRESSABLE,
                  response_parameters: [p_cls.new(size: wanted_response_size)])

    duplex_op = @stub.full_duplex_call([duplex_req], metadata: metadata,
                                        return_op: true)
    resp = duplex_op.execute
    resp.each { |r| } # ensures that the server sends trailing data
    duplex_op.wait
    if not duplex_op.metadata.has_key?(initial_metadata_key)
      fail AssertionError, "Expected initial metadata. None received"
    elsif duplex_op.metadata[initial_metadata_key] !=
          metadata[initial_metadata_key]
      fail AssertionError,
             "Expected initial metadata: #{metadata[initial_metadata_key]}. "\
             "Received: #{duplex_op.metadata[initial_metadata_key]}"
    end
    if not duplex_op.trailing_metadata[trailing_metadata_key]
      fail AssertionError, "Expected trailing metadata. None received"
    elsif duplex_op.trailing_metadata[trailing_metadata_key] !=
          metadata[trailing_metadata_key]
      fail AssertionError, 
          "Expected trailing metadata: #{metadata[trailing_metadata_key]}. "\
          "Received: #{duplex_op.trailing_metadata[trailing_metadata_key]}"
    end

  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('payload type is wrong') do
      :COMPRESSABLE == resp.payload.type
    end
    assert('payload body has the wrong length') do
      wanted_response_size == resp.payload.body.length
    end
    assert('payload body is invalid') do
      nulls(wanted_response_size) == resp.payload.body
    end
    resp
  end

  # Send probing message for compressed request on the server, to see
  # if it's implemented.
  def send_probe_for_compressed_request_support(&send_probe)
    bad_status_occurred = false

    begin
      send_probe.call
    rescue GRPC::BadStatus => e
      if e.code == GRPC::Core::StatusCodes::INVALID_ARGUMENT
        bad_status_occurred = true
      else
        fail AssertionError, "Bad status received but code is #{e.code}"
      end
    rescue Exception => e
      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
    end

    assert('CompressedRequest probe failed') do
      bad_status_occurred
    end
  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 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('--use_tls USE_TLS', ['false', 'true'],
            'require a secure connection?') do |v|
      args['secure'] = v == 'true'
    end
    opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'],
            'if secure, use the test certificate?') do |v|
      args['use_test_ca'] = v == 'true'
    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
  p "OK: #{opts['test_case']}"
end

if __FILE__ == $0
  main
end