blob: 4e77492ba9833d46c2894a59998077f05aa0d978 [file] [log] [blame]
Tim Emiolaf4ee9612015-08-14 18:47:16 -07001#!/usr/bin/env ruby
2
murgatroid9900450af2016-01-05 15:13:08 -08003# Copyright 2015-2016, Google Inc.
Tim Emiolaf4ee9612015-08-14 18:47:16 -07004# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are
8# met:
9#
10# * Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12# * Redistributions in binary form must reproduce the above
13# copyright notice, this list of conditions and the following disclaimer
14# in the documentation and/or other materials provided with the
15# distribution.
16# * Neither the name of Google Inc. nor the names of its
17# contributors may be used to endorse or promote products derived from
18# this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32# client is a testing tool that accesses a gRPC interop testing server and runs
33# a test on it.
34#
35# Helps validate interoperation b/w different gRPC implementations.
36#
37# Usage: $ path/to/client.rb --server_host=<hostname> \
38# --server_port=<port> \
39# --test_case=<testcase_name>
40
41this_dir = File.expand_path(File.dirname(__FILE__))
42lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
43pb_dir = File.dirname(File.dirname(this_dir))
44$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
45$LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
46$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
47
48require 'optparse'
Tim Emiola69a672e2015-11-10 14:59:15 -080049require 'logger'
Tim Emiolaf4ee9612015-08-14 18:47:16 -070050
51require 'grpc'
52require 'googleauth'
53require 'google/protobuf'
54
55require 'test/proto/empty'
56require 'test/proto/messages'
57require 'test/proto/test_services'
58
59require 'signet/ssl_config'
60
61AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR
62
Tim Emiola69a672e2015-11-10 14:59:15 -080063# RubyLogger defines a logger for gRPC based on the standard ruby logger.
64module RubyLogger
65 def logger
66 LOGGER
67 end
68
69 LOGGER = Logger.new(STDOUT)
Tim Emiolaa0824352015-11-11 13:43:16 -080070 LOGGER.level = Logger::INFO
Tim Emiola69a672e2015-11-10 14:59:15 -080071end
72
73# GRPC is the general RPC module
74module GRPC
75 # Inject the noop #logger if no module-level logger method has been injected.
76 extend RubyLogger
77end
78
Tim Emiola19e436d2015-08-17 09:34:40 -070079# AssertionError is use to indicate interop test failures.
80class AssertionError < RuntimeError; end
81
82# Fails with AssertionError if the block does evaluate to true
83def assert(msg = 'unknown cause')
84 fail 'No assertion block provided' unless block_given?
85 fail AssertionError, msg unless yield
86end
87
Tim Emiolaf4ee9612015-08-14 18:47:16 -070088# loads the certificates used to access the test server securely.
89def load_test_certs
90 this_dir = File.expand_path(File.dirname(__FILE__))
91 data_dir = File.join(File.dirname(File.dirname(this_dir)), 'spec/testdata')
92 files = ['ca.pem', 'server1.key', 'server1.pem']
93 files.map { |f| File.open(File.join(data_dir, f)).read }
94end
95
Tim Emiolaf4ee9612015-08-14 18:47:16 -070096# creates SSL Credentials from the test certificates.
97def test_creds
98 certs = load_test_certs
Tim Emiola43a7e4e2015-10-28 00:17:14 -070099 GRPC::Core::ChannelCredentials.new(certs[0])
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700100end
101
102# creates SSL Credentials from the production certificates.
103def prod_creds
Jan Tattermuscha6b2c4c2015-12-10 16:41:11 -0800104 GRPC::Core::ChannelCredentials.new()
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700105end
106
107# creates the SSL Credentials.
108def ssl_creds(use_test_ca)
109 return test_creds if use_test_ca
110 prod_creds
111end
112
113# creates a test stub that accesses host:port securely.
114def create_stub(opts)
115 address = "#{opts.host}:#{opts.port}"
116 if opts.secure
murgatroid99d24424f2015-12-16 14:01:26 -0800117 creds = ssl_creds(opts.use_test_ca)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700118 stub_opts = {
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700119 GRPC::Core::Channel::SSL_TARGET => opts.host_override
120 }
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)
murgatroid99cfa26e12015-12-04 14:36:52 -0800127 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
murgatroid99d24424f2015-12-16 14:01:26 -0800128 creds = creds.compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700129 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.
murgatroid99cfa26e12015-12-04 14:36:52 -0800137 call_creds = GRPC::Core::CallCredentials.new(proc { |md| md.merge(kw) })
murgatroid99d24424f2015-12-16 14:01:26 -0800138 creds = creds.compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700139 end
140
141 if opts.test_case == 'jwt_token_creds' # don't use a scope
142 auth_creds = Google::Auth.get_application_default
murgatroid99cfa26e12015-12-04 14:36:52 -0800143 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
murgatroid99d24424f2015-12-16 14:01:26 -0800144 creds = creds.compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700145 end
146
147 GRPC.logger.info("... connecting securely to #{address}")
murgatroid99d24424f2015-12-16 14:01:26 -0800148 Grpc::Testing::TestService::Stub.new(address, creds, **stub_opts)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700149 else
150 GRPC.logger.info("... connecting insecurely to #{address}")
murgatroid99d24424f2015-12-16 14:01:26 -0800151 Grpc::Testing::TestService::Stub.new(address, :this_channel_is_insecure)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700152 end
153end
154
155# produces a string of null chars (\0) of length l.
156def nulls(l)
157 fail 'requires #{l} to be +ve' if l < 0
murgatroid9959e54722015-12-08 15:48:21 -0800158 [].pack('x' * l).force_encoding('ascii-8bit')
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700159end
160
161# a PingPongPlayer implements the ping pong bidi test.
162class PingPongPlayer
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700163 include Grpc::Testing
164 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700165 attr_accessor :queue
166 attr_accessor :canceller_op
167
168 # reqs is the enumerator over the requests
169 def initialize(msg_sizes)
170 @queue = Queue.new
171 @msg_sizes = msg_sizes
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700172 @canceller_op = nil # used to cancel after the first response
173 end
174
175 def each_item
176 return enum_for(:each_item) unless block_given?
177 req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters # short
178 count = 0
179 @msg_sizes.each do |m|
180 req_size, resp_size = m
181 req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
182 response_type: :COMPRESSABLE,
183 response_parameters: [p_cls.new(size: resp_size)])
184 yield req
185 resp = @queue.pop
Tim Emiola19e436d2015-08-17 09:34:40 -0700186 assert('payload type is wrong') { :COMPRESSABLE == resp.payload.type }
187 assert("payload body #{count} has the wrong length") do
188 resp_size == resp.payload.body.length
189 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700190 p "OK: ping_pong #{count}"
191 count += 1
192 unless @canceller_op.nil?
193 canceller_op.cancel
194 break
195 end
196 end
197 end
198end
199
200# defines methods corresponding to each interop test case.
201class NamedTests
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700202 include Grpc::Testing
203 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700204
205 def initialize(stub, args)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700206 @stub = stub
207 @args = args
208 end
209
210 def empty_unary
211 resp = @stub.empty_call(Empty.new)
Tim Emiola19e436d2015-08-17 09:34:40 -0700212 assert('empty_unary: invalid response') { resp.is_a?(Empty) }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700213 p 'OK: empty_unary'
214 end
215
216 def large_unary
217 perform_large_unary
218 p 'OK: large_unary'
219 end
220
221 def service_account_creds
222 # ignore this test if the oauth options are not set
223 if @args.oauth_scope.nil?
224 p 'NOT RUN: service_account_creds; no service_account settings'
225 return
226 end
227 json_key = File.read(ENV[AUTH_ENV])
228 wanted_email = MultiJson.load(json_key)['client_email']
229 resp = perform_large_unary(fill_username: true,
230 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700231 assert("#{__callee__}: bad username") { wanted_email == resp.username }
232 assert("#{__callee__}: bad oauth scope") do
233 @args.oauth_scope.include?(resp.oauth_scope)
234 end
235 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700236 end
237
238 def jwt_token_creds
239 json_key = File.read(ENV[AUTH_ENV])
240 wanted_email = MultiJson.load(json_key)['client_email']
241 resp = perform_large_unary(fill_username: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700242 assert("#{__callee__}: bad username") { wanted_email == resp.username }
243 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700244 end
245
246 def compute_engine_creds
247 resp = perform_large_unary(fill_username: true,
248 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700249 assert("#{__callee__}: bad username") do
250 @args.default_service_account == resp.username
251 end
252 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700253 end
254
255 def oauth2_auth_token
256 resp = perform_large_unary(fill_username: true,
257 fill_oauth_scope: true)
258 json_key = File.read(ENV[AUTH_ENV])
259 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700260 assert("#{__callee__}: bad username") { wanted_email == resp.username }
261 assert("#{__callee__}: bad oauth scope") do
262 @args.oauth_scope.include?(resp.oauth_scope)
263 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700264 p "OK: #{__callee__}"
265 end
266
267 def per_rpc_creds
268 auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
murgatroid9900450af2016-01-05 15:13:08 -0800269 update_metadata = proc do |md|
270 kw = auth_creds.updater_proc.call({})
Jan Tattermuschfe3d9ea2015-10-19 18:10:45 -0700271
murgatroid9900450af2016-01-05 15:13:08 -0800272 # TODO(mlumish): downcase the metadata keys here to make sure
273 # they are not rejected by C core. This is a hotfix that should
274 # be addressed by introducing auto-downcasing logic.
275 Hash[ kw.each_pair.map { |k, v| [k.downcase, v] }]
276 end
277
278 call_creds = GRPC::Core::CallCredentials.new(update_metadata)
Jan Tattermuschfe3d9ea2015-10-19 18:10:45 -0700279
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700280 resp = perform_large_unary(fill_username: true,
281 fill_oauth_scope: true,
murgatroid9900450af2016-01-05 15:13:08 -0800282 credentials: call_creds)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700283 json_key = File.read(ENV[AUTH_ENV])
284 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700285 assert("#{__callee__}: bad username") { wanted_email == resp.username }
286 assert("#{__callee__}: bad oauth scope") do
287 @args.oauth_scope.include?(resp.oauth_scope)
288 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700289 p "OK: #{__callee__}"
290 end
291
292 def client_streaming
293 msg_sizes = [27_182, 8, 1828, 45_904]
294 wanted_aggregate_size = 74_922
295 reqs = msg_sizes.map do |x|
296 req = Payload.new(body: nulls(x))
297 StreamingInputCallRequest.new(payload: req)
298 end
299 resp = @stub.streaming_input_call(reqs)
Tim Emiola19e436d2015-08-17 09:34:40 -0700300 assert("#{__callee__}: aggregate payload size is incorrect") do
301 wanted_aggregate_size == resp.aggregated_payload_size
302 end
303 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700304 end
305
306 def server_streaming
307 msg_sizes = [31_415, 9, 2653, 58_979]
308 response_spec = msg_sizes.map { |s| ResponseParameters.new(size: s) }
309 req = StreamingOutputCallRequest.new(response_type: :COMPRESSABLE,
310 response_parameters: response_spec)
311 resps = @stub.streaming_output_call(req)
312 resps.each_with_index do |r, i|
Tim Emiola19e436d2015-08-17 09:34:40 -0700313 assert("#{__callee__}: too many responses") { i < msg_sizes.length }
314 assert("#{__callee__}: payload body #{i} has the wrong length") do
315 msg_sizes[i] == r.payload.body.length
316 end
317 assert("#{__callee__}: payload type is wrong") do
318 :COMPRESSABLE == r.payload.type
319 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700320 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700321 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700322 end
323
324 def ping_pong
325 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
326 ppp = PingPongPlayer.new(msg_sizes)
327 resps = @stub.full_duplex_call(ppp.each_item)
328 resps.each { |r| ppp.queue.push(r) }
Tim Emiola19e436d2015-08-17 09:34:40 -0700329 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700330 end
331
332 def timeout_on_sleeping_server
333 msg_sizes = [[27_182, 31_415]]
334 ppp = PingPongPlayer.new(msg_sizes)
335 resps = @stub.full_duplex_call(ppp.each_item, timeout: 0.001)
336 resps.each { |r| ppp.queue.push(r) }
337 fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)'
338 rescue GRPC::BadStatus => e
Tim Emiola19e436d2015-08-17 09:34:40 -0700339 assert("#{__callee__}: status was wrong") do
340 e.code == GRPC::Core::StatusCodes::DEADLINE_EXCEEDED
341 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700342 p "OK: #{__callee__}"
343 end
344
345 def empty_stream
346 ppp = PingPongPlayer.new([])
347 resps = @stub.full_duplex_call(ppp.each_item)
348 count = 0
349 resps.each do |r|
350 ppp.queue.push(r)
351 count += 1
352 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700353 assert("#{__callee__}: too many responses expected 0") do
354 count == 0
355 end
356 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700357 end
358
359 def cancel_after_begin
360 msg_sizes = [27_182, 8, 1828, 45_904]
361 reqs = msg_sizes.map do |x|
362 req = Payload.new(body: nulls(x))
363 StreamingInputCallRequest.new(payload: req)
364 end
365 op = @stub.streaming_input_call(reqs, return_op: true)
366 op.cancel
Tim Emiola19e436d2015-08-17 09:34:40 -0700367 op.execute
368 fail 'Should have raised GRPC:Cancelled'
369 rescue GRPC::Cancelled
370 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
371 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700372 end
373
374 def cancel_after_first_response
375 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
376 ppp = PingPongPlayer.new(msg_sizes)
377 op = @stub.full_duplex_call(ppp.each_item, return_op: true)
378 ppp.canceller_op = op # causes ppp to cancel after the 1st message
Tim Emiola19e436d2015-08-17 09:34:40 -0700379 op.execute.each { |r| ppp.queue.push(r) }
380 fail 'Should have raised GRPC:Cancelled'
381 rescue GRPC::Cancelled
382 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700383 op.wait
Tim Emiola19e436d2015-08-17 09:34:40 -0700384 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700385 end
386
387 def all
388 all_methods = NamedTests.instance_methods(false).map(&:to_s)
389 all_methods.each do |m|
390 next if m == 'all' || m.start_with?('assert')
391 p "TESTCASE: #{m}"
392 method(m).call
393 end
394 end
395
396 private
397
398 def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
399 req_size, wanted_response_size = 271_828, 314_159
400 payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
401 req = SimpleRequest.new(response_type: :COMPRESSABLE,
402 response_size: wanted_response_size,
403 payload: payload)
404 req.fill_username = fill_username
405 req.fill_oauth_scope = fill_oauth_scope
406 resp = @stub.unary_call(req, **kw)
Tim Emiola19e436d2015-08-17 09:34:40 -0700407 assert('payload type is wrong') do
408 :COMPRESSABLE == resp.payload.type
409 end
410 assert('payload body has the wrong length') do
411 wanted_response_size == resp.payload.body.length
412 end
413 assert('payload body is invalid') do
414 nulls(wanted_response_size) == resp.payload.body
415 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700416 resp
417 end
418end
419
420# Args is used to hold the command line info.
421Args = Struct.new(:default_service_account, :host, :host_override,
422 :oauth_scope, :port, :secure, :test_case,
423 :use_test_ca)
424
425# validates the the command line options, returning them as a Hash.
426def parse_args
427 args = Args.new
428 args.host_override = 'foo.test.google.fr'
429 OptionParser.new do |opts|
430 opts.on('--oauth_scope scope',
431 'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
432 opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
433 args['host'] = v
434 end
435 opts.on('--default_service_account email_address',
436 'email address of the default service account') do |v|
437 args['default_service_account'] = v
438 end
439 opts.on('--server_host_override HOST_OVERRIDE',
440 'override host via a HTTP header') do |v|
441 args['host_override'] = v
442 end
443 opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
444 # instance_methods(false) gives only the methods defined in that class
445 test_cases = NamedTests.instance_methods(false).map(&:to_s)
446 test_case_list = test_cases.join(',')
447 opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
448 " (#{test_case_list})") { |v| args['test_case'] = v }
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700449 opts.on('--use_tls USE_TLS', ['false', 'true'],
450 'require a secure connection?') do |v|
451 args['secure'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700452 end
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700453 opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'],
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700454 'if secure, use the test certificate?') do |v|
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700455 args['use_test_ca'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700456 end
457 end.parse!
458 _check_args(args)
459end
460
461def _check_args(args)
462 %w(host port test_case).each do |a|
463 if args[a].nil?
464 fail(OptionParser::MissingArgument, "please specify --#{a}")
465 end
466 end
467 args
468end
469
470def main
471 opts = parse_args
472 stub = create_stub(opts)
473 NamedTests.new(stub, opts).method(opts['test_case']).call
474end
475
476main