blob: 30550d6cc0a7b473c4715bd0a3357b4c6385d5cf [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)
135 stub_opts[:update_metadata] = auth_creds.updater_proc
136 end
137 end
138
139 if opts.test_case == 'oauth2_auth_token'
140 auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
141 kw = auth_creds.updater_proc.call({}) # gives as an auth token
142
143 # use a metadata update proc that just adds the auth token.
144 stub_opts[:update_metadata] = proc { |md| md.merge(kw) }
145 end
146
147 if opts.test_case == 'jwt_token_creds' # don't use a scope
148 auth_creds = Google::Auth.get_application_default
149 stub_opts[:update_metadata] = auth_creds.updater_proc
150 end
151
152 GRPC.logger.info("... connecting securely to #{address}")
153 Grpc::Testing::TestService::Stub.new(address, **stub_opts)
154 else
155 GRPC.logger.info("... connecting insecurely to #{address}")
156 Grpc::Testing::TestService::Stub.new(address)
157 end
158end
159
160# produces a string of null chars (\0) of length l.
161def nulls(l)
162 fail 'requires #{l} to be +ve' if l < 0
163 [].pack('x' * l).force_encoding('utf-8')
164end
165
166# a PingPongPlayer implements the ping pong bidi test.
167class PingPongPlayer
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700168 include Grpc::Testing
169 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700170 attr_accessor :queue
171 attr_accessor :canceller_op
172
173 # reqs is the enumerator over the requests
174 def initialize(msg_sizes)
175 @queue = Queue.new
176 @msg_sizes = msg_sizes
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700177 @canceller_op = nil # used to cancel after the first response
178 end
179
180 def each_item
181 return enum_for(:each_item) unless block_given?
182 req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters # short
183 count = 0
184 @msg_sizes.each do |m|
185 req_size, resp_size = m
186 req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
187 response_type: :COMPRESSABLE,
188 response_parameters: [p_cls.new(size: resp_size)])
189 yield req
190 resp = @queue.pop
Tim Emiola19e436d2015-08-17 09:34:40 -0700191 assert('payload type is wrong') { :COMPRESSABLE == resp.payload.type }
192 assert("payload body #{count} has the wrong length") do
193 resp_size == resp.payload.body.length
194 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700195 p "OK: ping_pong #{count}"
196 count += 1
197 unless @canceller_op.nil?
198 canceller_op.cancel
199 break
200 end
201 end
202 end
203end
204
205# defines methods corresponding to each interop test case.
206class NamedTests
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700207 include Grpc::Testing
208 include Grpc::Testing::PayloadType
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700209
210 def initialize(stub, args)
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700211 @stub = stub
212 @args = args
213 end
214
215 def empty_unary
216 resp = @stub.empty_call(Empty.new)
Tim Emiola19e436d2015-08-17 09:34:40 -0700217 assert('empty_unary: invalid response') { resp.is_a?(Empty) }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700218 p 'OK: empty_unary'
219 end
220
221 def large_unary
222 perform_large_unary
223 p 'OK: large_unary'
224 end
225
226 def service_account_creds
227 # ignore this test if the oauth options are not set
228 if @args.oauth_scope.nil?
229 p 'NOT RUN: service_account_creds; no service_account settings'
230 return
231 end
232 json_key = File.read(ENV[AUTH_ENV])
233 wanted_email = MultiJson.load(json_key)['client_email']
234 resp = perform_large_unary(fill_username: true,
235 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700236 assert("#{__callee__}: bad username") { wanted_email == resp.username }
237 assert("#{__callee__}: bad oauth scope") do
238 @args.oauth_scope.include?(resp.oauth_scope)
239 end
240 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700241 end
242
243 def jwt_token_creds
244 json_key = File.read(ENV[AUTH_ENV])
245 wanted_email = MultiJson.load(json_key)['client_email']
246 resp = perform_large_unary(fill_username: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700247 assert("#{__callee__}: bad username") { wanted_email == resp.username }
248 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700249 end
250
251 def compute_engine_creds
252 resp = perform_large_unary(fill_username: true,
253 fill_oauth_scope: true)
Tim Emiola19e436d2015-08-17 09:34:40 -0700254 assert("#{__callee__}: bad username") do
255 @args.default_service_account == resp.username
256 end
257 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700258 end
259
260 def oauth2_auth_token
261 resp = perform_large_unary(fill_username: true,
262 fill_oauth_scope: true)
263 json_key = File.read(ENV[AUTH_ENV])
264 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700265 assert("#{__callee__}: bad username") { wanted_email == resp.username }
266 assert("#{__callee__}: bad oauth scope") do
267 @args.oauth_scope.include?(resp.oauth_scope)
268 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700269 p "OK: #{__callee__}"
270 end
271
272 def per_rpc_creds
273 auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
274 kw = auth_creds.updater_proc.call({})
Jan Tattermuschfe3d9ea2015-10-19 18:10:45 -0700275
276 # TODO(jtattermusch): downcase the metadata keys here to make sure
277 # they are not rejected by C core. This is a hotfix that should
278 # be addressed by introducing auto-downcasing logic.
279 kw = Hash[ kw.each_pair.map { |k, v| [k.downcase, v] }]
280
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700281 resp = perform_large_unary(fill_username: true,
282 fill_oauth_scope: true,
283 **kw)
284 json_key = File.read(ENV[AUTH_ENV])
285 wanted_email = MultiJson.load(json_key)['client_email']
Tim Emiola19e436d2015-08-17 09:34:40 -0700286 assert("#{__callee__}: bad username") { wanted_email == resp.username }
287 assert("#{__callee__}: bad oauth scope") do
288 @args.oauth_scope.include?(resp.oauth_scope)
289 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700290 p "OK: #{__callee__}"
291 end
292
293 def client_streaming
294 msg_sizes = [27_182, 8, 1828, 45_904]
295 wanted_aggregate_size = 74_922
296 reqs = msg_sizes.map do |x|
297 req = Payload.new(body: nulls(x))
298 StreamingInputCallRequest.new(payload: req)
299 end
300 resp = @stub.streaming_input_call(reqs)
Tim Emiola19e436d2015-08-17 09:34:40 -0700301 assert("#{__callee__}: aggregate payload size is incorrect") do
302 wanted_aggregate_size == resp.aggregated_payload_size
303 end
304 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700305 end
306
307 def server_streaming
308 msg_sizes = [31_415, 9, 2653, 58_979]
309 response_spec = msg_sizes.map { |s| ResponseParameters.new(size: s) }
310 req = StreamingOutputCallRequest.new(response_type: :COMPRESSABLE,
311 response_parameters: response_spec)
312 resps = @stub.streaming_output_call(req)
313 resps.each_with_index do |r, i|
Tim Emiola19e436d2015-08-17 09:34:40 -0700314 assert("#{__callee__}: too many responses") { i < msg_sizes.length }
315 assert("#{__callee__}: payload body #{i} has the wrong length") do
316 msg_sizes[i] == r.payload.body.length
317 end
318 assert("#{__callee__}: payload type is wrong") do
319 :COMPRESSABLE == r.payload.type
320 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700321 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700322 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700323 end
324
325 def ping_pong
326 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
327 ppp = PingPongPlayer.new(msg_sizes)
328 resps = @stub.full_duplex_call(ppp.each_item)
329 resps.each { |r| ppp.queue.push(r) }
Tim Emiola19e436d2015-08-17 09:34:40 -0700330 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700331 end
332
333 def timeout_on_sleeping_server
334 msg_sizes = [[27_182, 31_415]]
335 ppp = PingPongPlayer.new(msg_sizes)
336 resps = @stub.full_duplex_call(ppp.each_item, timeout: 0.001)
337 resps.each { |r| ppp.queue.push(r) }
338 fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)'
339 rescue GRPC::BadStatus => e
Tim Emiola19e436d2015-08-17 09:34:40 -0700340 assert("#{__callee__}: status was wrong") do
341 e.code == GRPC::Core::StatusCodes::DEADLINE_EXCEEDED
342 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700343 p "OK: #{__callee__}"
344 end
345
346 def empty_stream
347 ppp = PingPongPlayer.new([])
348 resps = @stub.full_duplex_call(ppp.each_item)
349 count = 0
350 resps.each do |r|
351 ppp.queue.push(r)
352 count += 1
353 end
Tim Emiola19e436d2015-08-17 09:34:40 -0700354 assert("#{__callee__}: too many responses expected 0") do
355 count == 0
356 end
357 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700358 end
359
360 def cancel_after_begin
361 msg_sizes = [27_182, 8, 1828, 45_904]
362 reqs = msg_sizes.map do |x|
363 req = Payload.new(body: nulls(x))
364 StreamingInputCallRequest.new(payload: req)
365 end
366 op = @stub.streaming_input_call(reqs, return_op: true)
367 op.cancel
Tim Emiola19e436d2015-08-17 09:34:40 -0700368 op.execute
369 fail 'Should have raised GRPC:Cancelled'
370 rescue GRPC::Cancelled
371 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
372 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700373 end
374
375 def cancel_after_first_response
376 msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
377 ppp = PingPongPlayer.new(msg_sizes)
378 op = @stub.full_duplex_call(ppp.each_item, return_op: true)
379 ppp.canceller_op = op # causes ppp to cancel after the 1st message
Tim Emiola19e436d2015-08-17 09:34:40 -0700380 op.execute.each { |r| ppp.queue.push(r) }
381 fail 'Should have raised GRPC:Cancelled'
382 rescue GRPC::Cancelled
383 assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700384 op.wait
Tim Emiola19e436d2015-08-17 09:34:40 -0700385 p "OK: #{__callee__}"
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700386 end
387
388 def all
389 all_methods = NamedTests.instance_methods(false).map(&:to_s)
390 all_methods.each do |m|
391 next if m == 'all' || m.start_with?('assert')
392 p "TESTCASE: #{m}"
393 method(m).call
394 end
395 end
396
397 private
398
399 def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
400 req_size, wanted_response_size = 271_828, 314_159
401 payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
402 req = SimpleRequest.new(response_type: :COMPRESSABLE,
403 response_size: wanted_response_size,
404 payload: payload)
405 req.fill_username = fill_username
406 req.fill_oauth_scope = fill_oauth_scope
407 resp = @stub.unary_call(req, **kw)
Tim Emiola19e436d2015-08-17 09:34:40 -0700408 assert('payload type is wrong') do
409 :COMPRESSABLE == resp.payload.type
410 end
411 assert('payload body has the wrong length') do
412 wanted_response_size == resp.payload.body.length
413 end
414 assert('payload body is invalid') do
415 nulls(wanted_response_size) == resp.payload.body
416 end
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700417 resp
418 end
419end
420
421# Args is used to hold the command line info.
422Args = Struct.new(:default_service_account, :host, :host_override,
423 :oauth_scope, :port, :secure, :test_case,
424 :use_test_ca)
425
426# validates the the command line options, returning them as a Hash.
427def parse_args
428 args = Args.new
429 args.host_override = 'foo.test.google.fr'
430 OptionParser.new do |opts|
431 opts.on('--oauth_scope scope',
432 'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
433 opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
434 args['host'] = v
435 end
436 opts.on('--default_service_account email_address',
437 'email address of the default service account') do |v|
438 args['default_service_account'] = v
439 end
440 opts.on('--server_host_override HOST_OVERRIDE',
441 'override host via a HTTP header') do |v|
442 args['host_override'] = v
443 end
444 opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
445 # instance_methods(false) gives only the methods defined in that class
446 test_cases = NamedTests.instance_methods(false).map(&:to_s)
447 test_case_list = test_cases.join(',')
448 opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
449 " (#{test_case_list})") { |v| args['test_case'] = v }
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700450 opts.on('--use_tls USE_TLS', ['false', 'true'],
451 'require a secure connection?') do |v|
452 args['secure'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700453 end
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700454 opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'],
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700455 'if secure, use the test certificate?') do |v|
Jan Tattermusch73b3eea2015-10-15 17:52:06 -0700456 args['use_test_ca'] = v == 'true'
Tim Emiolaf4ee9612015-08-14 18:47:16 -0700457 end
458 end.parse!
459 _check_args(args)
460end
461
462def _check_args(args)
463 %w(host port test_case).each do |a|
464 if args[a].nil?
465 fail(OptionParser::MissingArgument, "please specify --#{a}")
466 end
467 end
468 args
469end
470
471def main
472 opts = parse_args
473 stub = create_stub(opts)
474 NamedTests.new(stub, opts).method(opts['test_case']).call
475end
476
477main