OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env ruby |
| 2 |
| 3 # Copyright 2015-2016, 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 |
| 41 this_dir = File.expand_path(File.dirname(__FILE__)) |
| 42 lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib') |
| 43 pb_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 |
| 48 require 'optparse' |
| 49 require 'logger' |
| 50 |
| 51 require 'grpc' |
| 52 require 'googleauth' |
| 53 require 'google/protobuf' |
| 54 |
| 55 require 'test/proto/empty' |
| 56 require 'test/proto/messages' |
| 57 require 'test/proto/test_services' |
| 58 |
| 59 AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR |
| 60 |
| 61 # RubyLogger defines a logger for gRPC based on the standard ruby logger. |
| 62 module RubyLogger |
| 63 def logger |
| 64 LOGGER |
| 65 end |
| 66 |
| 67 LOGGER = Logger.new(STDOUT) |
| 68 LOGGER.level = Logger::INFO |
| 69 end |
| 70 |
| 71 # GRPC is the general RPC module |
| 72 module GRPC |
| 73 # Inject the noop #logger if no module-level logger method has been injected. |
| 74 extend RubyLogger |
| 75 end |
| 76 |
| 77 # AssertionError is use to indicate interop test failures. |
| 78 class AssertionError < RuntimeError; end |
| 79 |
| 80 # Fails with AssertionError if the block does evaluate to true |
| 81 def assert(msg = 'unknown cause') |
| 82 fail 'No assertion block provided' unless block_given? |
| 83 fail AssertionError, msg unless yield |
| 84 end |
| 85 |
| 86 # loads the certificates used to access the test server securely. |
| 87 def 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 } |
| 92 end |
| 93 |
| 94 # creates SSL Credentials from the test certificates. |
| 95 def test_creds |
| 96 certs = load_test_certs |
| 97 GRPC::Core::ChannelCredentials.new(certs[0]) |
| 98 end |
| 99 |
| 100 # creates SSL Credentials from the production certificates. |
| 101 def prod_creds |
| 102 GRPC::Core::ChannelCredentials.new() |
| 103 end |
| 104 |
| 105 # creates the SSL Credentials. |
| 106 def ssl_creds(use_test_ca) |
| 107 return test_creds if use_test_ca |
| 108 prod_creds |
| 109 end |
| 110 |
| 111 # creates a test stub that accesses host:port securely. |
| 112 def create_stub(opts) |
| 113 address = "#{opts.host}:#{opts.port}" |
| 114 if opts.secure |
| 115 creds = ssl_creds(opts.use_test_ca) |
| 116 stub_opts = { |
| 117 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) |
| 125 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc) |
| 126 creds = creds.compose call_creds |
| 127 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. |
| 135 call_creds = GRPC::Core::CallCredentials.new(proc { |md| md.merge(kw) }) |
| 136 creds = creds.compose call_creds |
| 137 end |
| 138 |
| 139 if opts.test_case == 'jwt_token_creds' # don't use a scope |
| 140 auth_creds = Google::Auth.get_application_default |
| 141 call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc) |
| 142 creds = creds.compose call_creds |
| 143 end |
| 144 |
| 145 GRPC.logger.info("... connecting securely to #{address}") |
| 146 Grpc::Testing::TestService::Stub.new(address, creds, **stub_opts) |
| 147 else |
| 148 GRPC.logger.info("... connecting insecurely to #{address}") |
| 149 Grpc::Testing::TestService::Stub.new(address, :this_channel_is_insecure) |
| 150 end |
| 151 end |
| 152 |
| 153 # produces a string of null chars (\0) of length l. |
| 154 def nulls(l) |
| 155 fail 'requires #{l} to be +ve' if l < 0 |
| 156 [].pack('x' * l).force_encoding('ascii-8bit') |
| 157 end |
| 158 |
| 159 # a PingPongPlayer implements the ping pong bidi test. |
| 160 class PingPongPlayer |
| 161 include Grpc::Testing |
| 162 include Grpc::Testing::PayloadType |
| 163 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 |
| 170 @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 |
| 184 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 |
| 188 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 |
| 196 end |
| 197 |
| 198 # defines methods corresponding to each interop test case. |
| 199 class NamedTests |
| 200 include Grpc::Testing |
| 201 include Grpc::Testing::PayloadType |
| 202 |
| 203 def initialize(stub, args) |
| 204 @stub = stub |
| 205 @args = args |
| 206 end |
| 207 |
| 208 def empty_unary |
| 209 resp = @stub.empty_call(Empty.new) |
| 210 assert('empty_unary: invalid response') { resp.is_a?(Empty) } |
| 211 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) |
| 229 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__}" |
| 234 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) |
| 240 assert("#{__callee__}: bad username") { wanted_email == resp.username } |
| 241 p "OK: #{__callee__}" |
| 242 end |
| 243 |
| 244 def compute_engine_creds |
| 245 resp = perform_large_unary(fill_username: true, |
| 246 fill_oauth_scope: true) |
| 247 assert("#{__callee__}: bad username") do |
| 248 @args.default_service_account == resp.username |
| 249 end |
| 250 p "OK: #{__callee__}" |
| 251 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'] |
| 258 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 |
| 262 p "OK: #{__callee__}" |
| 263 end |
| 264 |
| 265 def per_rpc_creds |
| 266 auth_creds = Google::Auth.get_application_default(@args.oauth_scope) |
| 267 update_metadata = proc do |md| |
| 268 kw = auth_creds.updater_proc.call({}) |
| 269 end |
| 270 |
| 271 call_creds = GRPC::Core::CallCredentials.new(update_metadata) |
| 272 |
| 273 resp = perform_large_unary(fill_username: true, |
| 274 fill_oauth_scope: true, |
| 275 credentials: call_creds) |
| 276 json_key = File.read(ENV[AUTH_ENV]) |
| 277 wanted_email = MultiJson.load(json_key)['client_email'] |
| 278 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 |
| 282 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) |
| 293 assert("#{__callee__}: aggregate payload size is incorrect") do |
| 294 wanted_aggregate_size == resp.aggregated_payload_size |
| 295 end |
| 296 p "OK: #{__callee__}" |
| 297 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| |
| 306 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 |
| 313 end |
| 314 p "OK: #{__callee__}" |
| 315 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) } |
| 322 p "OK: #{__callee__}" |
| 323 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 |
| 332 assert("#{__callee__}: status was wrong") do |
| 333 e.code == GRPC::Core::StatusCodes::DEADLINE_EXCEEDED |
| 334 end |
| 335 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 |
| 346 assert("#{__callee__}: too many responses expected 0") do |
| 347 count == 0 |
| 348 end |
| 349 p "OK: #{__callee__}" |
| 350 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 |
| 360 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__}" |
| 365 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 |
| 372 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 } |
| 376 op.wait |
| 377 p "OK: #{__callee__}" |
| 378 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) |
| 400 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 |
| 409 resp |
| 410 end |
| 411 end |
| 412 |
| 413 # Args is used to hold the command line info. |
| 414 Args = 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. |
| 419 def 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 } |
| 442 opts.on('--use_tls USE_TLS', ['false', 'true'], |
| 443 'require a secure connection?') do |v| |
| 444 args['secure'] = v == 'true' |
| 445 end |
| 446 opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'], |
| 447 'if secure, use the test certificate?') do |v| |
| 448 args['use_test_ca'] = v == 'true' |
| 449 end |
| 450 end.parse! |
| 451 _check_args(args) |
| 452 end |
| 453 |
| 454 def _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 |
| 461 end |
| 462 |
| 463 def main |
| 464 opts = parse_args |
| 465 stub = create_stub(opts) |
| 466 NamedTests.new(stub, opts).method(opts['test_case']).call |
| 467 end |
| 468 |
| 469 main |
OLD | NEW |