blob: 695a5c4ea231022e49642134099b3058c9eb459f [file] [log] [blame]
Tim Emiolaf4ee9612015-08-14 18:47:16 -07001#!/usr/bin/env ruby
2
Craig Tiller6169d5f2016-03-31 07:46:18 -07003# Copyright 2015, 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
Tim Emiolaf4ee9612015-08-14 18:47:16 -070059AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR
60
Tim Emiola69a672e2015-11-10 14:59:15 -080061# RubyLogger defines a logger for gRPC based on the standard ruby logger.
62module RubyLogger
63 def logger
64 LOGGER
65 end
66
67 LOGGER = Logger.new(STDOUT)
Tim Emiolaa0824352015-11-11 13:43:16 -080068 LOGGER.level = Logger::INFO
Tim Emiola69a672e2015-11-10 14:59:15 -080069end
70
71# GRPC is the general RPC module
72module GRPC
73 # Inject the noop #logger if no module-level logger method has been injected.
74 extend RubyLogger
75end
76
Tim Emiola19e436d2015-08-17 09:34:40 -070077# AssertionError is use to indicate interop test failures.
78class AssertionError < RuntimeError; end
79
80# Fails with AssertionError if the block does evaluate to true
81def assert(msg = 'unknown cause')
82 fail 'No assertion block provided' unless block_given?
83 fail AssertionError, msg unless yield
84end
85
Tim Emiolaf4ee9612015-08-14 18:47:16 -070086# loads the certificates used to access the test server securely.
87def load_test_certs
88 this_dir = File.expand_path(File.dirname(__FILE__))
89 data_dir = File.join(File.dirname(File.dirname(this_dir)), 'spec/testdata')
90 files = ['ca.pem', 'server1.key', 'server1.pem']
91 files.map { |f| File.open(File.join(data_dir, f)).read }
92end
93
Tim Emiolaf4ee9612015-08-14 18:47:16 -070094# creates SSL Credentials from the test certificates.
95def test_creds
96 certs = load_test_certs
Tim Emiola43a7e4e2015-10-28 00:17:14 -070097 GRPC::Core::ChannelCredentials.new(certs[0])
Tim Emiolaf4ee9612015-08-14 18:47:16 -070098end
99
100# creates SSL Credentials from the production certificates.
101def prod_creds
Jan Tattermuscha6b2c4c2015-12-10 16:41:11 -0800102 GRPC::Core::ChannelCredentials.new()
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700103end
104
105# creates the SSL Credentials.
106def ssl_creds(use_test_ca)
107 return test_creds if use_test_ca
108 prod_creds
109end
110
111# creates a test stub that accesses host:port securely.
112def create_stub(opts)
113 address = "#{opts.host}:#{opts.port}"
114 if opts.secure
murgatroid99d24424f2015-12-16 14:01:26 -0800115 creds = ssl_creds(opts.use_test_ca)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700116 stub_opts = {
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700117 GRPC::Core::Channel::SSL_TARGET => opts.host_override
118 }
119
120 # Add service account creds if specified
121 wants_creds = %w(all compute_engine_creds service_account_creds)
122 if wants_creds.include?(opts.test_case)
123 unless opts.oauth_scope.nil?
124 auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
murgatroid99cfa26e12015-12-04 14:36:52 -0800125 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
murgatroid99d24424f2015-12-16 14:01:26 -0800126 creds = creds.compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700127 end
128 end
129
130 if opts.test_case == 'oauth2_auth_token'
131 auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
132 kw = auth_creds.updater_proc.call({}) # gives as an auth token
133
134 # use a metadata update proc that just adds the auth token.
murgatroid99cfa26e12015-12-04 14:36:52 -0800135 call_creds = GRPC::Core::CallCredentials.new(proc { |md| md.merge(kw) })
murgatroid99d24424f2015-12-16 14:01:26 -0800136 creds = creds.compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700137 end
138
139 if opts.test_case == 'jwt_token_creds' # don't use a scope
140 auth_creds = Google::Auth.get_application_default
murgatroid99cfa26e12015-12-04 14:36:52 -0800141 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
murgatroid99d24424f2015-12-16 14:01:26 -0800142 creds = creds.compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700143 end
144
145 GRPC.logger.info("... connecting securely to #{address}")
murgatroid99d24424f2015-12-16 14:01:26 -0800146 Grpc::Testing::TestService::Stub.new(address, creds, **stub_opts)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700147 else
148 GRPC.logger.info("... connecting insecurely to #{address}")
murgatroid99d24424f2015-12-16 14:01:26 -0800149 Grpc::Testing::TestService::Stub.new(address, :this_channel_is_insecure)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700150 end
151end
152
153# produces a string of null chars (\0) of length l.
154def nulls(l)
155 fail 'requires #{l} to be +ve' if l < 0
murgatroid9959e54722015-12-08 15:48:21 -0800156 [].pack('x' * l).force_encoding('ascii-8bit')
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700157end
158
159# a PingPongPlayer implements the ping pong bidi test.
160class PingPongPlayer
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700161 include Grpc::Testing
162 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700163 attr_accessor :queue
164 attr_accessor :canceller_op
165
166 # reqs is the enumerator over the requests
167 def initialize(msg_sizes)
168 @queue = Queue.new
169 @msg_sizes = msg_sizes
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700170 @canceller_op = nil # used to cancel after the first response
171 end
172
173 def each_item
174 return enum_for(:each_item) unless block_given?
175 req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters # short
176 count = 0
177 @msg_sizes.each do |m|
178 req_size, resp_size = m
179 req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
180 response_type: :COMPRESSABLE,
181 response_parameters: [p_cls.new(size: resp_size)])
182 yield req
183 resp = @queue.pop
Tim Emiola19e436d2015-08-17 09:34:40 -0700184 assert('payload type is wrong') { :COMPRESSABLE == resp.payload.type }
185 assert("payload body #{count} has the wrong length") do
186 resp_size == resp.payload.body.length
187 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700188 p "OK: ping_pong #{count}"
189 count += 1
190 unless @canceller_op.nil?
191 canceller_op.cancel
192 break
193 end
194 end
195 end
196end
197
198# defines methods corresponding to each interop test case.
199class NamedTests
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700200 include Grpc::Testing
201 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700202
203 def initialize(stub, args)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700204 @stub = stub
205 @args = args
206 end
207
208 def empty_unary
209 resp = @stub.empty_call(Empty.new)
Tim Emiola19e436d2015-08-17 09:34:40 -0700210 assert('empty_unary: invalid response') { resp.is_a?(Empty) }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700211 p 'OK: empty_unary'
212 end
213
214 def large_unary
215 perform_large_unary
216 p 'OK: large_unary'
217 end
218
219 def service_account_creds
220 # ignore this test if the oauth options are not set
221 if @args.oauth_scope.nil?
222 p 'NOT RUN: service_account_creds; no service_account settings'
223 return
224 end
225 json_key = File.read(ENV[AUTH_ENV])
226 wanted_email = MultiJson.load(json_key)['client_email']
227 resp = perform_large_unary(fill_username: true,
228 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700229 assert("#{__callee__}: bad username") { wanted_email == resp.username }
230 assert("#{__callee__}: bad oauth scope") do
231 @args.oauth_scope.include?(resp.oauth_scope)
232 end
233 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700234 end
235
236 def jwt_token_creds
237 json_key = File.read(ENV[AUTH_ENV])
238 wanted_email = MultiJson.load(json_key)['client_email']
239 resp = perform_large_unary(fill_username: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700240 assert("#{__callee__}: bad username") { wanted_email == resp.username }
241 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700242 end
243
244 def compute_engine_creds
245 resp = perform_large_unary(fill_username: true,
246 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700247 assert("#{__callee__}: bad username") do
248 @args.default_service_account == resp.username
249 end
250 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700251 end
252
253 def oauth2_auth_token
254 resp = perform_large_unary(fill_username: true,
255 fill_oauth_scope: true)
256 json_key = File.read(ENV[AUTH_ENV])
257 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700258 assert("#{__callee__}: bad username") { wanted_email == resp.username }
259 assert("#{__callee__}: bad oauth scope") do
260 @args.oauth_scope.include?(resp.oauth_scope)
261 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700262 p "OK: #{__callee__}"
263 end
264
265 def per_rpc_creds
266 auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
murgatroid9900450af2016-01-05 15:13:08 -0800267 update_metadata = proc do |md|
268 kw = auth_creds.updater_proc.call({})
murgatroid9900450af2016-01-05 15:13:08 -0800269 end
270
271 call_creds = GRPC::Core::CallCredentials.new(update_metadata)
Jan Tattermuschfe3d9ea2015-10-19 18:10:45 -0700272
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700273 resp = perform_large_unary(fill_username: true,
274 fill_oauth_scope: true,
murgatroid9900450af2016-01-05 15:13:08 -0800275 credentials: call_creds)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700276 json_key = File.read(ENV[AUTH_ENV])
277 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700278 assert("#{__callee__}: bad username") { wanted_email == resp.username }
279 assert("#{__callee__}: bad oauth scope") do
280 @args.oauth_scope.include?(resp.oauth_scope)
281 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700282 p "OK: #{__callee__}"
283 end
284
285 def client_streaming
286 msg_sizes = [27_182, 8, 1828, 45_904]
287 wanted_aggregate_size = 74_922
288 reqs = msg_sizes.map do |x|
289 req = Payload.new(body: nulls(x))
290 StreamingInputCallRequest.new(payload: req)
291 end
292 resp = @stub.streaming_input_call(reqs)
Tim Emiola19e436d2015-08-17 09:34:40 -0700293 assert("#{__callee__}: aggregate payload size is incorrect") do
294 wanted_aggregate_size == resp.aggregated_payload_size
295 end
296 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700297 end
298
299 def server_streaming
300 msg_sizes = [31_415, 9, 2653, 58_979]
301 response_spec = msg_sizes.map { |s| ResponseParameters.new(size: s) }
302 req = StreamingOutputCallRequest.new(response_type: :COMPRESSABLE,
303 response_parameters: response_spec)
304 resps = @stub.streaming_output_call(req)
305 resps.each_with_index do |r, i|
Tim Emiola19e436d2015-08-17 09:34:40 -0700306 assert("#{__callee__}: too many responses") { i < msg_sizes.length }
307 assert("#{__callee__}: payload body #{i} has the wrong length") do
308 msg_sizes[i] == r.payload.body.length
309 end
310 assert("#{__callee__}: payload type is wrong") do
311 :COMPRESSABLE == r.payload.type
312 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700313 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700314 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700315 end
316
317 def ping_pong
318 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
319 ppp = PingPongPlayer.new(msg_sizes)
320 resps = @stub.full_duplex_call(ppp.each_item)
321 resps.each { |r| ppp.queue.push(r) }
Tim Emiola19e436d2015-08-17 09:34:40 -0700322 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700323 end
324
325 def timeout_on_sleeping_server
326 msg_sizes = [[27_182, 31_415]]
327 ppp = PingPongPlayer.new(msg_sizes)
328 resps = @stub.full_duplex_call(ppp.each_item, timeout: 0.001)
329 resps.each { |r| ppp.queue.push(r) }
330 fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)'
331 rescue GRPC::BadStatus => e
Tim Emiola19e436d2015-08-17 09:34:40 -0700332 assert("#{__callee__}: status was wrong") do
333 e.code == GRPC::Core::StatusCodes::DEADLINE_EXCEEDED
334 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700335 p "OK: #{__callee__}"
336 end
337
338 def empty_stream
339 ppp = PingPongPlayer.new([])
340 resps = @stub.full_duplex_call(ppp.each_item)
341 count = 0
342 resps.each do |r|
343 ppp.queue.push(r)
344 count += 1
345 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700346 assert("#{__callee__}: too many responses expected 0") do
347 count == 0
348 end
349 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700350 end
351
352 def cancel_after_begin
353 msg_sizes = [27_182, 8, 1828, 45_904]
354 reqs = msg_sizes.map do |x|
355 req = Payload.new(body: nulls(x))
356 StreamingInputCallRequest.new(payload: req)
357 end
358 op = @stub.streaming_input_call(reqs, return_op: true)
359 op.cancel
Tim Emiola19e436d2015-08-17 09:34:40 -0700360 op.execute
361 fail 'Should have raised GRPC:Cancelled'
362 rescue GRPC::Cancelled
363 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
364 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700365 end
366
367 def cancel_after_first_response
368 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
369 ppp = PingPongPlayer.new(msg_sizes)
370 op = @stub.full_duplex_call(ppp.each_item, return_op: true)
371 ppp.canceller_op = op # causes ppp to cancel after the 1st message
Tim Emiola19e436d2015-08-17 09:34:40 -0700372 op.execute.each { |r| ppp.queue.push(r) }
373 fail 'Should have raised GRPC:Cancelled'
374 rescue GRPC::Cancelled
375 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700376 op.wait
Tim Emiola19e436d2015-08-17 09:34:40 -0700377 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700378 end
379
380 def all
381 all_methods = NamedTests.instance_methods(false).map(&:to_s)
382 all_methods.each do |m|
383 next if m == 'all' || m.start_with?('assert')
384 p "TESTCASE: #{m}"
385 method(m).call
386 end
387 end
388
389 private
390
391 def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
392 req_size, wanted_response_size = 271_828, 314_159
393 payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
394 req = SimpleRequest.new(response_type: :COMPRESSABLE,
395 response_size: wanted_response_size,
396 payload: payload)
397 req.fill_username = fill_username
398 req.fill_oauth_scope = fill_oauth_scope
399 resp = @stub.unary_call(req, **kw)
Tim Emiola19e436d2015-08-17 09:34:40 -0700400 assert('payload type is wrong') do
401 :COMPRESSABLE == resp.payload.type
402 end
403 assert('payload body has the wrong length') do
404 wanted_response_size == resp.payload.body.length
405 end
406 assert('payload body is invalid') do
407 nulls(wanted_response_size) == resp.payload.body
408 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700409 resp
410 end
411end
412
413# Args is used to hold the command line info.
414Args = Struct.new(:default_service_account, :host, :host_override,
415 :oauth_scope, :port, :secure, :test_case,
416 :use_test_ca)
417
418# validates the the command line options, returning them as a Hash.
419def parse_args
420 args = Args.new
421 args.host_override = 'foo.test.google.fr'
422 OptionParser.new do |opts|
423 opts.on('--oauth_scope scope',
424 'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
425 opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
426 args['host'] = v
427 end
428 opts.on('--default_service_account email_address',
429 'email address of the default service account') do |v|
430 args['default_service_account'] = v
431 end
432 opts.on('--server_host_override HOST_OVERRIDE',
433 'override host via a HTTP header') do |v|
434 args['host_override'] = v
435 end
436 opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
437 # instance_methods(false) gives only the methods defined in that class
438 test_cases = NamedTests.instance_methods(false).map(&:to_s)
439 test_case_list = test_cases.join(',')
440 opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
441 " (#{test_case_list})") { |v| args['test_case'] = v }
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700442 opts.on('--use_tls USE_TLS', ['false', 'true'],
443 'require a secure connection?') do |v|
444 args['secure'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700445 end
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700446 opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'],
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700447 'if secure, use the test certificate?') do |v|
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700448 args['use_test_ca'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700449 end
450 end.parse!
451 _check_args(args)
452end
453
454def _check_args(args)
455 %w(host port test_case).each do |a|
456 if args[a].nil?
457 fail(OptionParser::MissingArgument, "please specify --#{a}")
458 end
459 end
460 args
461end
462
463def main
464 opts = parse_args
465 stub = create_stub(opts)
466 NamedTests.new(stub, opts).method(opts['test_case']).call
murgatroid99e621f132016-04-21 14:28:00 -0700467 p "OK: #{opts['test_case']}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700468end
469
murgatroid99e621f132016-04-21 14:28:00 -0700470if __FILE__ == $0
471 main
472end