1#!/usr/bin/env ruby
2
3# Copyright 2015 gRPC authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17# client is a testing tool that accesses a gRPC interop testing server and runs
18# a test on it.
19#
20# Helps validate interoperation b/w different gRPC implementations.
21#
22# Usage: $ path/to/client.rb --server_host=<hostname> \
23#                            --server_port=<port> \
24#                            --test_case=<testcase_name>
25
26# These lines are required for the generated files to load grpc
27this_dir = File.expand_path(File.dirname(__FILE__))
28lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
29pb_dir = File.dirname(this_dir)
30$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
31$LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
32
33require 'optparse'
34require 'logger'
35
36require_relative '../../lib/grpc'
37require 'googleauth'
38require 'google/protobuf'
39
40require_relative '../src/proto/grpc/testing/empty_pb'
41require_relative '../src/proto/grpc/testing/messages_pb'
42require_relative '../src/proto/grpc/testing/test_services_pb'
43
44AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR
45
46# RubyLogger defines a logger for gRPC based on the standard ruby logger.
47module RubyLogger
48  def logger
49    LOGGER
50  end
51
52  LOGGER = Logger.new(STDOUT)
53  LOGGER.level = Logger::INFO
54end
55
56# GRPC is the general RPC module
57module GRPC
58  # Inject the noop #logger if no module-level logger method has been injected.
59  extend RubyLogger
60end
61
62# AssertionError is use to indicate interop test failures.
63class AssertionError < RuntimeError; end
64
65# Fails with AssertionError if the block does evaluate to true
66def assert(msg = 'unknown cause')
67  fail 'No assertion block provided' unless block_given?
68  fail AssertionError, msg unless yield
69end
70
71# loads the certificates used to access the test server securely.
72def load_test_certs
73  this_dir = File.expand_path(File.dirname(__FILE__))
74  data_dir = File.join(File.dirname(File.dirname(this_dir)), 'spec/testdata')
75  files = ['ca.pem', 'server1.key', 'server1.pem']
76  files.map { |f| File.open(File.join(data_dir, f)).read }
77end
78
79# creates SSL Credentials from the test certificates.
80def test_creds
81  certs = load_test_certs
82  GRPC::Core::ChannelCredentials.new(certs[0])
83end
84
85# creates SSL Credentials from the production certificates.
86def prod_creds
87  GRPC::Core::ChannelCredentials.new()
88end
89
90# creates the SSL Credentials.
91def ssl_creds(use_test_ca)
92  return test_creds if use_test_ca
93  prod_creds
94end
95
96# creates a test stub that accesses host:port securely.
97def create_stub(opts)
98  address = "#{opts.server_host}:#{opts.server_port}"
99
100  # Provide channel args that request compression by default
101  # for compression interop tests
102  if ['client_compressed_unary',
103      'client_compressed_streaming'].include?(opts.test_case)
104    compression_options =
105      GRPC::Core::CompressionOptions.new(default_algorithm: :gzip)
106    compression_channel_args = compression_options.to_channel_arg_hash
107  else
108    compression_channel_args = {}
109  end
110
111  if opts.secure
112    creds = ssl_creds(opts.use_test_ca)
113    stub_opts = {
114      channel_args: {}
115    }
116    unless opts.server_host_override.empty?
117      stub_opts[:channel_args].merge!({
118          GRPC::Core::Channel::SSL_TARGET => opts.server_host_override
119      })
120    end
121
122    # Add service account creds if specified
123    wants_creds = %w(all compute_engine_creds service_account_creds)
124    if wants_creds.include?(opts.test_case)
125      unless opts.oauth_scope.nil?
126        auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
127        call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
128        creds = creds.compose call_creds
129      end
130    end
131
132    if opts.test_case == 'oauth2_auth_token'
133      auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
134      kw = auth_creds.updater_proc.call({})  # gives as an auth token
135
136      # use a metadata update proc that just adds the auth token.
137      call_creds = GRPC::Core::CallCredentials.new(proc { |md| md.merge(kw) })
138      creds = creds.compose call_creds
139    end
140
141    if opts.test_case == 'jwt_token_creds'  # don't use a scope
142      auth_creds = Google::Auth.get_application_default
143      call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
144      creds = creds.compose call_creds
145    end
146
147    GRPC.logger.info("... connecting securely to #{address}")
148    stub_opts[:channel_args].merge!(compression_channel_args)
149    if opts.test_case == "unimplemented_service"
150      Grpc::Testing::UnimplementedService::Stub.new(address, creds, **stub_opts)
151    else
152      Grpc::Testing::TestService::Stub.new(address, creds, **stub_opts)
153    end
154  else
155    GRPC.logger.info("... connecting insecurely to #{address}")
156    if opts.test_case == "unimplemented_service"
157      Grpc::Testing::UnimplementedService::Stub.new(
158        address,
159        :this_channel_is_insecure,
160        channel_args: compression_channel_args
161      )
162    else
163      Grpc::Testing::TestService::Stub.new(
164        address,
165        :this_channel_is_insecure,
166        channel_args: compression_channel_args
167      )
168    end
169  end
170end
171
172# produces a string of null chars (\0) of length l.
173def nulls(l)
174  fail 'requires #{l} to be +ve' if l < 0
175  [].pack('x' * l).force_encoding('ascii-8bit')
176end
177
178# a PingPongPlayer implements the ping pong bidi test.
179class PingPongPlayer
180  include Grpc::Testing
181  include Grpc::Testing::PayloadType
182  attr_accessor :queue
183  attr_accessor :canceller_op
184
185  # reqs is the enumerator over the requests
186  def initialize(msg_sizes)
187    @queue = Queue.new
188    @msg_sizes = msg_sizes
189    @canceller_op = nil  # used to cancel after the first response
190  end
191
192  def each_item
193    return enum_for(:each_item) unless block_given?
194    req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters  # short
195    count = 0
196    @msg_sizes.each do |m|
197      req_size, resp_size = m
198      req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
199                        response_type: :COMPRESSABLE,
200                        response_parameters: [p_cls.new(size: resp_size)])
201      yield req
202      resp = @queue.pop
203      assert('payload type is wrong') { :COMPRESSABLE == resp.payload.type }
204      assert("payload body #{count} has the wrong length") do
205        resp_size == resp.payload.body.length
206      end
207      p "OK: ping_pong #{count}"
208      count += 1
209      unless @canceller_op.nil?
210        canceller_op.cancel
211        break
212      end
213    end
214  end
215end
216
217class BlockingEnumerator
218  include Grpc::Testing
219  include Grpc::Testing::PayloadType
220
221  def initialize(req_size, sleep_time)
222    @req_size = req_size
223    @sleep_time = sleep_time
224  end
225
226  def each_item
227    return enum_for(:each_item) unless block_given?
228    req_cls = StreamingOutputCallRequest
229    req = req_cls.new(payload: Payload.new(body: nulls(@req_size)))
230    yield req
231    # Sleep until after the deadline should have passed
232    sleep(@sleep_time)
233  end
234end
235
236# Intended to be used to wrap a call_op, and to adjust
237# the write flag of the call_op in between messages yielded to it.
238class WriteFlagSettingStreamingInputEnumerable
239  attr_accessor :call_op
240
241  def initialize(requests_and_write_flags)
242    @requests_and_write_flags = requests_and_write_flags
243  end
244
245  def each
246    @requests_and_write_flags.each do |request_and_flag|
247      @call_op.write_flag = request_and_flag[:write_flag]
248      yield request_and_flag[:request]
249    end
250  end
251end
252
253# defines methods corresponding to each interop test case.
254class NamedTests
255  include Grpc::Testing
256  include Grpc::Testing::PayloadType
257  include GRPC::Core::MetadataKeys
258
259  def initialize(stub, args)
260    @stub = stub
261    @args = args
262  end
263
264  def empty_unary
265    resp = @stub.empty_call(Empty.new)
266    assert('empty_unary: invalid response') { resp.is_a?(Empty) }
267  end
268
269  def large_unary
270    perform_large_unary
271  end
272
273  def client_compressed_unary
274    # first request used also for the probe
275    req_size, wanted_response_size = 271_828, 314_159
276    expect_compressed = BoolValue.new(value: true)
277    payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
278    req = SimpleRequest.new(response_type: :COMPRESSABLE,
279                            response_size: wanted_response_size,
280                            payload: payload,
281                            expect_compressed: expect_compressed)
282
283    # send a probe to see if CompressedResponse is supported on the server
284    send_probe_for_compressed_request_support do
285      request_uncompressed_args = {
286        COMPRESSION_REQUEST_ALGORITHM => 'identity'
287      }
288      @stub.unary_call(req, metadata: request_uncompressed_args)
289    end
290
291    # make a call with a compressed message
292    resp = @stub.unary_call(req)
293    assert('Expected second unary call with compression to work') do
294      resp.payload.body.length == wanted_response_size
295    end
296
297    # make a call with an uncompressed message
298    stub_options = {
299      COMPRESSION_REQUEST_ALGORITHM => 'identity'
300    }
301
302    req = SimpleRequest.new(
303      response_type: :COMPRESSABLE,
304      response_size: wanted_response_size,
305      payload: payload,
306      expect_compressed: BoolValue.new(value: false)
307    )
308
309    resp = @stub.unary_call(req, metadata: stub_options)
310    assert('Expected second unary call with compression to work') do
311      resp.payload.body.length == wanted_response_size
312    end
313  end
314
315  def service_account_creds
316    # ignore this test if the oauth options are not set
317    if @args.oauth_scope.nil?
318      p 'NOT RUN: service_account_creds; no service_account settings'
319      return
320    end
321    json_key = File.read(ENV[AUTH_ENV])
322    wanted_email = MultiJson.load(json_key)['client_email']
323    resp = perform_large_unary(fill_username: true,
324                               fill_oauth_scope: true)
325    assert("#{__callee__}: bad username") { wanted_email == resp.username }
326    assert("#{__callee__}: bad oauth scope") do
327      @args.oauth_scope.include?(resp.oauth_scope)
328    end
329  end
330
331  def jwt_token_creds
332    json_key = File.read(ENV[AUTH_ENV])
333    wanted_email = MultiJson.load(json_key)['client_email']
334    resp = perform_large_unary(fill_username: true)
335    assert("#{__callee__}: bad username") { wanted_email == resp.username }
336  end
337
338  def compute_engine_creds
339    resp = perform_large_unary(fill_username: true,
340                               fill_oauth_scope: true)
341    assert("#{__callee__}: bad username") do
342      @args.default_service_account == resp.username
343    end
344  end
345
346  def oauth2_auth_token
347    resp = perform_large_unary(fill_username: true,
348                               fill_oauth_scope: true)
349    json_key = File.read(ENV[AUTH_ENV])
350    wanted_email = MultiJson.load(json_key)['client_email']
351    assert("#{__callee__}: bad username") { wanted_email == resp.username }
352    assert("#{__callee__}: bad oauth scope") do
353      @args.oauth_scope.include?(resp.oauth_scope)
354    end
355  end
356
357  def per_rpc_creds
358    auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
359    update_metadata = proc do |md|
360      kw = auth_creds.updater_proc.call({})
361    end
362
363    call_creds = GRPC::Core::CallCredentials.new(update_metadata)
364
365    resp = perform_large_unary(fill_username: true,
366                               fill_oauth_scope: true,
367                               credentials: call_creds)
368    json_key = File.read(ENV[AUTH_ENV])
369    wanted_email = MultiJson.load(json_key)['client_email']
370    assert("#{__callee__}: bad username") { wanted_email == resp.username }
371    assert("#{__callee__}: bad oauth scope") do
372      @args.oauth_scope.include?(resp.oauth_scope)
373    end
374  end
375
376  def client_streaming
377    msg_sizes = [27_182, 8, 1828, 45_904]
378    wanted_aggregate_size = 74_922
379    reqs = msg_sizes.map do |x|
380      req = Payload.new(body: nulls(x))
381      StreamingInputCallRequest.new(payload: req)
382    end
383    resp = @stub.streaming_input_call(reqs)
384    assert("#{__callee__}: aggregate payload size is incorrect") do
385      wanted_aggregate_size == resp.aggregated_payload_size
386    end
387  end
388
389  def client_compressed_streaming
390    # first request used also by the probe
391    first_request = StreamingInputCallRequest.new(
392      payload: Payload.new(type: :COMPRESSABLE, body: nulls(27_182)),
393      expect_compressed: BoolValue.new(value: true)
394    )
395
396    # send a probe to see if CompressedResponse is supported on the server
397    send_probe_for_compressed_request_support do
398      request_uncompressed_args = {
399        COMPRESSION_REQUEST_ALGORITHM => 'identity'
400      }
401      @stub.streaming_input_call([first_request],
402                                 metadata: request_uncompressed_args)
403    end
404
405    second_request = StreamingInputCallRequest.new(
406      payload: Payload.new(type: :COMPRESSABLE, body: nulls(45_904)),
407      expect_compressed: BoolValue.new(value: false)
408    )
409
410    # Create the requests messages and the corresponding write flags
411    # for each message
412    requests = WriteFlagSettingStreamingInputEnumerable.new([
413      { request: first_request,
414        write_flag: 0 },
415      { request: second_request,
416        write_flag: GRPC::Core::WriteFlags::NO_COMPRESS }
417    ])
418
419    # Create the call_op, pass it to the requests enumerable, and
420    # run the call
421    call_op = @stub.streaming_input_call(requests,
422                                         return_op: true)
423    requests.call_op = call_op
424    resp = call_op.execute
425
426    wanted_aggregate_size = 73_086
427
428    assert("#{__callee__}: aggregate payload size is incorrect") do
429      wanted_aggregate_size == resp.aggregated_payload_size
430    end
431  end
432
433  def server_streaming
434    msg_sizes = [31_415, 9, 2653, 58_979]
435    response_spec = msg_sizes.map { |s| ResponseParameters.new(size: s) }
436    req = StreamingOutputCallRequest.new(response_type: :COMPRESSABLE,
437                                         response_parameters: response_spec)
438    resps = @stub.streaming_output_call(req)
439    resps.each_with_index do |r, i|
440      assert("#{__callee__}: too many responses") { i < msg_sizes.length }
441      assert("#{__callee__}: payload body #{i} has the wrong length") do
442        msg_sizes[i] == r.payload.body.length
443      end
444      assert("#{__callee__}: payload type is wrong") do
445        :COMPRESSABLE == r.payload.type
446      end
447    end
448  end
449
450  def ping_pong
451    msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
452    ppp = PingPongPlayer.new(msg_sizes)
453    resps = @stub.full_duplex_call(ppp.each_item)
454    resps.each { |r| ppp.queue.push(r) }
455  end
456
457  def timeout_on_sleeping_server
458    enum = BlockingEnumerator.new(27_182, 2)
459    deadline = GRPC::Core::TimeConsts::from_relative_time(1)
460    resps = @stub.full_duplex_call(enum.each_item, deadline: deadline)
461    resps.each { } # wait to receive each request (or timeout)
462    fail 'Should have raised GRPC::DeadlineExceeded'
463  rescue GRPC::DeadlineExceeded
464  end
465
466  def empty_stream
467    ppp = PingPongPlayer.new([])
468    resps = @stub.full_duplex_call(ppp.each_item)
469    count = 0
470    resps.each do |r|
471      ppp.queue.push(r)
472      count += 1
473    end
474    assert("#{__callee__}: too many responses expected 0") do
475      count == 0
476    end
477  end
478
479  def cancel_after_begin
480    msg_sizes = [27_182, 8, 1828, 45_904]
481    reqs = msg_sizes.map do |x|
482      req = Payload.new(body: nulls(x))
483      StreamingInputCallRequest.new(payload: req)
484    end
485    op = @stub.streaming_input_call(reqs, return_op: true)
486    op.cancel
487    op.execute
488    fail 'Should have raised GRPC:Cancelled'
489  rescue GRPC::Cancelled
490    assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled? }
491  end
492
493  def cancel_after_first_response
494    msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
495    ppp = PingPongPlayer.new(msg_sizes)
496    op = @stub.full_duplex_call(ppp.each_item, return_op: true)
497    ppp.canceller_op = op  # causes ppp to cancel after the 1st message
498    op.execute.each { |r| ppp.queue.push(r) }
499    fail 'Should have raised GRPC:Cancelled'
500  rescue GRPC::Cancelled
501    assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled? }
502    op.wait
503  end
504
505  def unimplemented_method
506    begin
507      resp = @stub.unimplemented_call(Empty.new)
508    rescue GRPC::Unimplemented => e
509      return
510    rescue Exception => e
511      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
512    end
513    fail AssertionError, "GRPC::Unimplemented should have been raised. Was not."
514  end
515
516  def unimplemented_service
517    begin
518      resp = @stub.unimplemented_call(Empty.new)
519    rescue GRPC::Unimplemented => e
520      return
521    rescue Exception => e
522      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
523    end
524    fail AssertionError, "GRPC::Unimplemented should have been raised. Was not."
525  end
526
527  def status_code_and_message
528
529    # Function wide constants.
530    message = "test status method"
531    code = GRPC::Core::StatusCodes::UNKNOWN
532
533    # Testing with UnaryCall.
534    payload = Payload.new(type: :COMPRESSABLE, body: nulls(1))
535    echo_status = EchoStatus.new(code: code, message: message)
536    req = SimpleRequest.new(response_type: :COMPRESSABLE,
537			    response_size: 1,
538			    payload: payload,
539			    response_status: echo_status)
540    seen_correct_exception = false
541    begin
542      resp = @stub.unary_call(req)
543    rescue GRPC::Unknown => e
544      if e.details != message
545	      fail AssertionError,
546	        "Expected message #{message}. Received: #{e.details}"
547      end
548      seen_correct_exception = true
549    rescue Exception => e
550      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
551    end
552
553    if not seen_correct_exception
554      fail AssertionError, "Did not see expected status from UnaryCall"
555    end
556
557    # testing with FullDuplex
558    req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters
559    duplex_req = req_cls.new(payload: Payload.new(body: nulls(1)),
560                  response_type: :COMPRESSABLE,
561                  response_parameters: [p_cls.new(size: 1)],
562                  response_status: echo_status)
563    seen_correct_exception = false
564    begin
565      resp = @stub.full_duplex_call([duplex_req])
566      resp.each { |r| }
567    rescue GRPC::Unknown => e
568      if e.details != message
569        fail AssertionError,
570          "Expected message #{message}. Received: #{e.details}"
571      end
572      seen_correct_exception = true
573    rescue Exception => e
574      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
575    end
576
577    if not seen_correct_exception
578      fail AssertionError, "Did not see expected status from FullDuplexCall"
579    end
580
581  end
582
583
584  def custom_metadata
585
586    # Function wide constants
587    req_size, wanted_response_size = 271_828, 314_159
588    initial_metadata_key = "x-grpc-test-echo-initial"
589    initial_metadata_value = "test_initial_metadata_value"
590    trailing_metadata_key = "x-grpc-test-echo-trailing-bin"
591    trailing_metadata_value = "\x0a\x0b\x0a\x0b\x0a\x0b"
592
593    metadata = {
594      initial_metadata_key => initial_metadata_value,
595      trailing_metadata_key => trailing_metadata_value
596    }
597
598    # Testing with UnaryCall
599    payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
600    req = SimpleRequest.new(response_type: :COMPRESSABLE,
601			    response_size: wanted_response_size,
602			    payload: payload)
603
604    op = @stub.unary_call(req, metadata: metadata, return_op: true)
605    op.execute
606    if not op.metadata.has_key?(initial_metadata_key)
607      fail AssertionError, "Expected initial metadata. None received"
608    elsif op.metadata[initial_metadata_key] != metadata[initial_metadata_key]
609      fail AssertionError,
610             "Expected initial metadata: #{metadata[initial_metadata_key]}. "\
611             "Received: #{op.metadata[initial_metadata_key]}"
612    end
613    if not op.trailing_metadata.has_key?(trailing_metadata_key)
614      fail AssertionError, "Expected trailing metadata. None received"
615    elsif op.trailing_metadata[trailing_metadata_key] !=
616          metadata[trailing_metadata_key]
617      fail AssertionError,
618            "Expected trailing metadata: #{metadata[trailing_metadata_key]}. "\
619            "Received: #{op.trailing_metadata[trailing_metadata_key]}"
620    end
621
622    # Testing with FullDuplex
623    req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters
624    duplex_req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
625                  response_type: :COMPRESSABLE,
626                  response_parameters: [p_cls.new(size: wanted_response_size)])
627
628    duplex_op = @stub.full_duplex_call([duplex_req], metadata: metadata,
629                                        return_op: true)
630    resp = duplex_op.execute
631    resp.each { |r| } # ensures that the server sends trailing data
632    duplex_op.wait
633    if not duplex_op.metadata.has_key?(initial_metadata_key)
634      fail AssertionError, "Expected initial metadata. None received"
635    elsif duplex_op.metadata[initial_metadata_key] !=
636          metadata[initial_metadata_key]
637      fail AssertionError,
638             "Expected initial metadata: #{metadata[initial_metadata_key]}. "\
639             "Received: #{duplex_op.metadata[initial_metadata_key]}"
640    end
641    if not duplex_op.trailing_metadata[trailing_metadata_key]
642      fail AssertionError, "Expected trailing metadata. None received"
643    elsif duplex_op.trailing_metadata[trailing_metadata_key] !=
644          metadata[trailing_metadata_key]
645      fail AssertionError,
646          "Expected trailing metadata: #{metadata[trailing_metadata_key]}. "\
647          "Received: #{duplex_op.trailing_metadata[trailing_metadata_key]}"
648    end
649
650  end
651
652  def all
653    all_methods = NamedTests.instance_methods(false).map(&:to_s)
654    all_methods.each do |m|
655      next if m == 'all' || m.start_with?('assert')
656      p "TESTCASE: #{m}"
657      method(m).call
658    end
659  end
660
661  private
662
663  def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
664    req_size, wanted_response_size = 271_828, 314_159
665    payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
666    req = SimpleRequest.new(response_type: :COMPRESSABLE,
667                            response_size: wanted_response_size,
668                            payload: payload)
669    req.fill_username = fill_username
670    req.fill_oauth_scope = fill_oauth_scope
671    resp = @stub.unary_call(req, **kw)
672    assert('payload type is wrong') do
673      :COMPRESSABLE == resp.payload.type
674    end
675    assert('payload body has the wrong length') do
676      wanted_response_size == resp.payload.body.length
677    end
678    assert('payload body is invalid') do
679      nulls(wanted_response_size) == resp.payload.body
680    end
681    resp
682  end
683
684  # Send probing message for compressed request on the server, to see
685  # if it's implemented.
686  def send_probe_for_compressed_request_support(&send_probe)
687    bad_status_occurred = false
688
689    begin
690      send_probe.call
691    rescue GRPC::BadStatus => e
692      if e.code == GRPC::Core::StatusCodes::INVALID_ARGUMENT
693        bad_status_occurred = true
694      else
695        fail AssertionError, "Bad status received but code is #{e.code}"
696      end
697    rescue Exception => e
698      fail AssertionError, "Expected BadStatus. Received: #{e.inspect}"
699    end
700
701    assert('CompressedRequest probe failed') do
702      bad_status_occurred
703    end
704  end
705
706end
707
708# Args is used to hold the command line info.
709Args = Struct.new(:default_service_account, :server_host, :server_host_override,
710                  :oauth_scope, :server_port, :secure, :test_case,
711                  :use_test_ca)
712
713# validates the command line options, returning them as a Hash.
714def parse_args
715  args = Args.new
716  args.server_host_override = ''
717  OptionParser.new do |opts|
718    opts.on('--oauth_scope scope',
719            'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
720    opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
721      args['server_host'] = v
722    end
723    opts.on('--default_service_account email_address',
724            'email address of the default service account') do |v|
725      args['default_service_account'] = v
726    end
727    opts.on('--server_host_override HOST_OVERRIDE',
728            'override host via a HTTP header') do |v|
729      args['server_host_override'] = v
730    end
731    opts.on('--server_port SERVER_PORT', 'server port') do |v|
732      args['server_port'] = v
733    end
734    # instance_methods(false) gives only the methods defined in that class
735    test_cases = NamedTests.instance_methods(false).map(&:to_s)
736    test_case_list = test_cases.join(',')
737    opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
738            "  (#{test_case_list})") { |v| args['test_case'] = v }
739    opts.on('--use_tls USE_TLS', ['false', 'true'],
740            'require a secure connection?') do |v|
741      args['secure'] = v == 'true'
742    end
743    opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'],
744            'if secure, use the test certificate?') do |v|
745      args['use_test_ca'] = v == 'true'
746    end
747  end.parse!
748  _check_args(args)
749end
750
751def _check_args(args)
752  %w(server_host server_port test_case).each do |a|
753    if args[a].nil?
754      fail(OptionParser::MissingArgument, "please specify --#{a}")
755    end
756  end
757  args
758end
759
760def main
761  opts = parse_args
762  stub = create_stub(opts)
763  NamedTests.new(stub, opts).method(opts['test_case']).call
764  p "OK: #{opts['test_case']}"
765end
766
767if __FILE__ == $0
768  main
769end
770