blob: 198b0395f8097ed4cf2206ce648582af7abd4b96 [file] [log] [blame]
Tim Emiolaf4ee9612015-08-14 18:47:16 -07001#!/usr/bin/env ruby
2
3# Copyright 2015, Google Inc.
4# 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
96# loads the certificates used to access the test server securely.
97def load_prod_cert
98 fail 'could not find a production cert' if ENV['SSL_CERT_FILE'].nil?
99 GRPC.logger.info("loading prod certs from #{ENV['SSL_CERT_FILE']}")
100 File.open(ENV['SSL_CERT_FILE']).read
101end
102
103# creates SSL Credentials from the test certificates.
104def test_creds
105 certs = load_test_certs
Tim Emiola43a7e4e2015-10-28 00:17:14 -0700106 GRPC::Core::ChannelCredentials.new(certs[0])
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700107end
108
109# creates SSL Credentials from the production certificates.
110def prod_creds
111 cert_text = load_prod_cert
Tim Emiola43a7e4e2015-10-28 00:17:14 -0700112 GRPC::Core::ChannelCredentials.new(cert_text)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700113end
114
115# creates the SSL Credentials.
116def ssl_creds(use_test_ca)
117 return test_creds if use_test_ca
118 prod_creds
119end
120
121# creates a test stub that accesses host:port securely.
122def create_stub(opts)
123 address = "#{opts.host}:#{opts.port}"
124 if opts.secure
125 stub_opts = {
126 :creds => ssl_creds(opts.use_test_ca),
127 GRPC::Core::Channel::SSL_TARGET => opts.host_override
128 }
129
130 # Add service account creds if specified
131 wants_creds = %w(all compute_engine_creds service_account_creds)
132 if wants_creds.include?(opts.test_case)
133 unless opts.oauth_scope.nil?
134 auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
murgatroid99cfa26e12015-12-04 14:36:52 -0800135 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
136 stub_opts[:creds] = stub_opts[:creds].compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700137 end
138 end
139
140 if opts.test_case == 'oauth2_auth_token'
141 auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
142 kw = auth_creds.updater_proc.call({}) # gives as an auth token
143
144 # use a metadata update proc that just adds the auth token.
murgatroid99cfa26e12015-12-04 14:36:52 -0800145 call_creds = GRPC::Core::CallCredentials.new(proc { |md| md.merge(kw) })
146 stub_opts[:creds] = stub_opts[:creds].compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700147 end
148
149 if opts.test_case == 'jwt_token_creds' # don't use a scope
150 auth_creds = Google::Auth.get_application_default
murgatroid99cfa26e12015-12-04 14:36:52 -0800151 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
152 stub_opts[:creds] = stub_opts[:creds].compose call_creds
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700153 end
154
155 GRPC.logger.info("... connecting securely to #{address}")
156 Grpc::Testing::TestService::Stub.new(address, **stub_opts)
157 else
158 GRPC.logger.info("... connecting insecurely to #{address}")
159 Grpc::Testing::TestService::Stub.new(address)
160 end
161end
162
163# produces a string of null chars (\0) of length l.
164def nulls(l)
165 fail 'requires #{l} to be +ve' if l < 0
166 [].pack('x' * l).force_encoding('utf-8')
167end
168
169# a PingPongPlayer implements the ping pong bidi test.
170class PingPongPlayer
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700171 include Grpc::Testing
172 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700173 attr_accessor :queue
174 attr_accessor :canceller_op
175
176 # reqs is the enumerator over the requests
177 def initialize(msg_sizes)
178 @queue = Queue.new
179 @msg_sizes = msg_sizes
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700180 @canceller_op = nil # used to cancel after the first response
181 end
182
183 def each_item
184 return enum_for(:each_item) unless block_given?
185 req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters # short
186 count = 0
187 @msg_sizes.each do |m|
188 req_size, resp_size = m
189 req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
190 response_type: :COMPRESSABLE,
191 response_parameters: [p_cls.new(size: resp_size)])
192 yield req
193 resp = @queue.pop
Tim Emiola19e436d2015-08-17 09:34:40 -0700194 assert('payload type is wrong') { :COMPRESSABLE == resp.payload.type }
195 assert("payload body #{count} has the wrong length") do
196 resp_size == resp.payload.body.length
197 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700198 p "OK: ping_pong #{count}"
199 count += 1
200 unless @canceller_op.nil?
201 canceller_op.cancel
202 break
203 end
204 end
205 end
206end
207
208# defines methods corresponding to each interop test case.
209class NamedTests
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700210 include Grpc::Testing
211 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700212
213 def initialize(stub, args)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700214 @stub = stub
215 @args = args
216 end
217
218 def empty_unary
219 resp = @stub.empty_call(Empty.new)
Tim Emiola19e436d2015-08-17 09:34:40 -0700220 assert('empty_unary: invalid response') { resp.is_a?(Empty) }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700221 p 'OK: empty_unary'
222 end
223
224 def large_unary
225 perform_large_unary
226 p 'OK: large_unary'
227 end
228
229 def service_account_creds
230 # ignore this test if the oauth options are not set
231 if @args.oauth_scope.nil?
232 p 'NOT RUN: service_account_creds; no service_account settings'
233 return
234 end
235 json_key = File.read(ENV[AUTH_ENV])
236 wanted_email = MultiJson.load(json_key)['client_email']
237 resp = perform_large_unary(fill_username: true,
238 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700239 assert("#{__callee__}: bad username") { wanted_email == resp.username }
240 assert("#{__callee__}: bad oauth scope") do
241 @args.oauth_scope.include?(resp.oauth_scope)
242 end
243 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700244 end
245
246 def jwt_token_creds
247 json_key = File.read(ENV[AUTH_ENV])
248 wanted_email = MultiJson.load(json_key)['client_email']
249 resp = perform_large_unary(fill_username: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700250 assert("#{__callee__}: bad username") { wanted_email == resp.username }
251 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700252 end
253
254 def compute_engine_creds
255 resp = perform_large_unary(fill_username: true,
256 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700257 assert("#{__callee__}: bad username") do
258 @args.default_service_account == resp.username
259 end
260 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700261 end
262
263 def oauth2_auth_token
264 resp = perform_large_unary(fill_username: true,
265 fill_oauth_scope: true)
266 json_key = File.read(ENV[AUTH_ENV])
267 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700268 assert("#{__callee__}: bad username") { wanted_email == resp.username }
269 assert("#{__callee__}: bad oauth scope") do
270 @args.oauth_scope.include?(resp.oauth_scope)
271 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700272 p "OK: #{__callee__}"
273 end
274
275 def per_rpc_creds
276 auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
277 kw = auth_creds.updater_proc.call({})
Jan Tattermuschfe3d9ea2015-10-19 18:10:45 -0700278
279 # TODO(jtattermusch): downcase the metadata keys here to make sure
280 # they are not rejected by C core. This is a hotfix that should
281 # be addressed by introducing auto-downcasing logic.
282 kw = Hash[ kw.each_pair.map { |k, v| [k.downcase, v] }]
283
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700284 resp = perform_large_unary(fill_username: true,
285 fill_oauth_scope: true,
286 **kw)
287 json_key = File.read(ENV[AUTH_ENV])
288 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700289 assert("#{__callee__}: bad username") { wanted_email == resp.username }
290 assert("#{__callee__}: bad oauth scope") do
291 @args.oauth_scope.include?(resp.oauth_scope)
292 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700293 p "OK: #{__callee__}"
294 end
295
296 def client_streaming
297 msg_sizes = [27_182, 8, 1828, 45_904]
298 wanted_aggregate_size = 74_922
299 reqs = msg_sizes.map do |x|
300 req = Payload.new(body: nulls(x))
301 StreamingInputCallRequest.new(payload: req)
302 end
303 resp = @stub.streaming_input_call(reqs)
Tim Emiola19e436d2015-08-17 09:34:40 -0700304 assert("#{__callee__}: aggregate payload size is incorrect") do
305 wanted_aggregate_size == resp.aggregated_payload_size
306 end
307 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700308 end
309
310 def server_streaming
311 msg_sizes = [31_415, 9, 2653, 58_979]
312 response_spec = msg_sizes.map { |s| ResponseParameters.new(size: s) }
313 req = StreamingOutputCallRequest.new(response_type: :COMPRESSABLE,
314 response_parameters: response_spec)
315 resps = @stub.streaming_output_call(req)
316 resps.each_with_index do |r, i|
Tim Emiola19e436d2015-08-17 09:34:40 -0700317 assert("#{__callee__}: too many responses") { i < msg_sizes.length }
318 assert("#{__callee__}: payload body #{i} has the wrong length") do
319 msg_sizes[i] == r.payload.body.length
320 end
321 assert("#{__callee__}: payload type is wrong") do
322 :COMPRESSABLE == r.payload.type
323 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700324 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700325 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700326 end
327
328 def ping_pong
329 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
330 ppp = PingPongPlayer.new(msg_sizes)
331 resps = @stub.full_duplex_call(ppp.each_item)
332 resps.each { |r| ppp.queue.push(r) }
Tim Emiola19e436d2015-08-17 09:34:40 -0700333 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700334 end
335
336 def timeout_on_sleeping_server
337 msg_sizes = [[27_182, 31_415]]
338 ppp = PingPongPlayer.new(msg_sizes)
339 resps = @stub.full_duplex_call(ppp.each_item, timeout: 0.001)
340 resps.each { |r| ppp.queue.push(r) }
341 fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)'
342 rescue GRPC::BadStatus => e
Tim Emiola19e436d2015-08-17 09:34:40 -0700343 assert("#{__callee__}: status was wrong") do
344 e.code == GRPC::Core::StatusCodes::DEADLINE_EXCEEDED
345 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700346 p "OK: #{__callee__}"
347 end
348
349 def empty_stream
350 ppp = PingPongPlayer.new([])
351 resps = @stub.full_duplex_call(ppp.each_item)
352 count = 0
353 resps.each do |r|
354 ppp.queue.push(r)
355 count += 1
356 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700357 assert("#{__callee__}: too many responses expected 0") do
358 count == 0
359 end
360 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700361 end
362
363 def cancel_after_begin
364 msg_sizes = [27_182, 8, 1828, 45_904]
365 reqs = msg_sizes.map do |x|
366 req = Payload.new(body: nulls(x))
367 StreamingInputCallRequest.new(payload: req)
368 end
369 op = @stub.streaming_input_call(reqs, return_op: true)
370 op.cancel
Tim Emiola19e436d2015-08-17 09:34:40 -0700371 op.execute
372 fail 'Should have raised GRPC:Cancelled'
373 rescue GRPC::Cancelled
374 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
375 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700376 end
377
378 def cancel_after_first_response
379 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
380 ppp = PingPongPlayer.new(msg_sizes)
381 op = @stub.full_duplex_call(ppp.each_item, return_op: true)
382 ppp.canceller_op = op # causes ppp to cancel after the 1st message
Tim Emiola19e436d2015-08-17 09:34:40 -0700383 op.execute.each { |r| ppp.queue.push(r) }
384 fail 'Should have raised GRPC:Cancelled'
385 rescue GRPC::Cancelled
386 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700387 op.wait
Tim Emiola19e436d2015-08-17 09:34:40 -0700388 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700389 end
390
391 def all
392 all_methods = NamedTests.instance_methods(false).map(&:to_s)
393 all_methods.each do |m|
394 next if m == 'all' || m.start_with?('assert')
395 p "TESTCASE: #{m}"
396 method(m).call
397 end
398 end
399
400 private
401
402 def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
403 req_size, wanted_response_size = 271_828, 314_159
404 payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
405 req = SimpleRequest.new(response_type: :COMPRESSABLE,
406 response_size: wanted_response_size,
407 payload: payload)
408 req.fill_username = fill_username
409 req.fill_oauth_scope = fill_oauth_scope
410 resp = @stub.unary_call(req, **kw)
Tim Emiola19e436d2015-08-17 09:34:40 -0700411 assert('payload type is wrong') do
412 :COMPRESSABLE == resp.payload.type
413 end
414 assert('payload body has the wrong length') do
415 wanted_response_size == resp.payload.body.length
416 end
417 assert('payload body is invalid') do
418 nulls(wanted_response_size) == resp.payload.body
419 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700420 resp
421 end
422end
423
424# Args is used to hold the command line info.
425Args = Struct.new(:default_service_account, :host, :host_override,
426 :oauth_scope, :port, :secure, :test_case,
427 :use_test_ca)
428
429# validates the the command line options, returning them as a Hash.
430def parse_args
431 args = Args.new
432 args.host_override = 'foo.test.google.fr'
433 OptionParser.new do |opts|
434 opts.on('--oauth_scope scope',
435 'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
436 opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
437 args['host'] = v
438 end
439 opts.on('--default_service_account email_address',
440 'email address of the default service account') do |v|
441 args['default_service_account'] = v
442 end
443 opts.on('--server_host_override HOST_OVERRIDE',
444 'override host via a HTTP header') do |v|
445 args['host_override'] = v
446 end
447 opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
448 # instance_methods(false) gives only the methods defined in that class
449 test_cases = NamedTests.instance_methods(false).map(&:to_s)
450 test_case_list = test_cases.join(',')
451 opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
452 " (#{test_case_list})") { |v| args['test_case'] = v }
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700453 opts.on('--use_tls USE_TLS', ['false', 'true'],
454 'require a secure connection?') do |v|
455 args['secure'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700456 end
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700457 opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'],
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700458 'if secure, use the test certificate?') do |v|
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700459 args['use_test_ca'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700460 end
461 end.parse!
462 _check_args(args)
463end
464
465def _check_args(args)
466 %w(host port test_case).each do |a|
467 if args[a].nil?
468 fail(OptionParser::MissingArgument, "please specify --#{a}")
469 end
470 end
471 args
472end
473
474def main
475 opts = parse_args
476 stub = create_stub(opts)
477 NamedTests.new(stub, opts).method(opts['test_case']).call
478end
479
480main