client.rb 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. #!/usr/bin/env ruby
  2. # Copyright 2015, Google Inc.
  3. # All rights reserved.
  4. #
  5. # Redistribution and use in source and binary forms, with or without
  6. # modification, are permitted provided that the following conditions are
  7. # met:
  8. #
  9. # * Redistributions of source code must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. # * Redistributions in binary form must reproduce the above
  12. # copyright notice, this list of conditions and the following disclaimer
  13. # in the documentation and/or other materials provided with the
  14. # distribution.
  15. # * Neither the name of Google Inc. nor the names of its
  16. # contributors may be used to endorse or promote products derived from
  17. # this software without specific prior written permission.
  18. #
  19. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  20. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  21. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  22. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  23. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  24. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  25. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  26. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  27. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  29. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. # client is a testing tool that accesses a gRPC interop testing server and runs
  31. # a test on it.
  32. #
  33. # Helps validate interoperation b/w different gRPC implementations.
  34. #
  35. # Usage: $ path/to/client.rb --server_host=<hostname> \
  36. # --server_port=<port> \
  37. # --test_case=<testcase_name>
  38. this_dir = File.expand_path(File.dirname(__FILE__))
  39. lib_dir = File.join(File.dirname(File.dirname(this_dir)), 'lib')
  40. pb_dir = File.dirname(File.dirname(this_dir))
  41. $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
  42. $LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
  43. $LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)
  44. require 'optparse'
  45. require 'logger'
  46. require 'grpc'
  47. require 'googleauth'
  48. require 'google/protobuf'
  49. require 'test/proto/empty'
  50. require 'test/proto/messages'
  51. require 'test/proto/test_services'
  52. require 'signet/ssl_config'
  53. AUTH_ENV = Google::Auth::CredentialsLoader::ENV_VAR
  54. # RubyLogger defines a logger for gRPC based on the standard ruby logger.
  55. module RubyLogger
  56. def logger
  57. LOGGER
  58. end
  59. LOGGER = Logger.new(STDOUT)
  60. LOGGER.level = Logger::INFO
  61. end
  62. # GRPC is the general RPC module
  63. module GRPC
  64. # Inject the noop #logger if no module-level logger method has been injected.
  65. extend RubyLogger
  66. end
  67. # AssertionError is use to indicate interop test failures.
  68. class AssertionError < RuntimeError; end
  69. # Fails with AssertionError if the block does evaluate to true
  70. def assert(msg = 'unknown cause')
  71. fail 'No assertion block provided' unless block_given?
  72. fail AssertionError, msg unless yield
  73. end
  74. # loads the certificates used to access the test server securely.
  75. def load_test_certs
  76. this_dir = File.expand_path(File.dirname(__FILE__))
  77. data_dir = File.join(File.dirname(File.dirname(this_dir)), 'spec/testdata')
  78. files = ['ca.pem', 'server1.key', 'server1.pem']
  79. files.map { |f| File.open(File.join(data_dir, f)).read }
  80. end
  81. # creates SSL Credentials from the test certificates.
  82. def test_creds
  83. certs = load_test_certs
  84. GRPC::Core::ChannelCredentials.new(certs[0])
  85. end
  86. # creates SSL Credentials from the production certificates.
  87. def prod_creds
  88. GRPC::Core::ChannelCredentials.new()
  89. end
  90. # creates the SSL Credentials.
  91. def ssl_creds(use_test_ca)
  92. return test_creds if use_test_ca
  93. prod_creds
  94. end
  95. # creates a test stub that accesses host:port securely.
  96. def create_stub(opts)
  97. address = "#{opts.host}:#{opts.port}"
  98. if opts.secure
  99. stub_opts = {
  100. :creds => ssl_creds(opts.use_test_ca),
  101. GRPC::Core::Channel::SSL_TARGET => opts.host_override
  102. }
  103. # Add service account creds if specified
  104. wants_creds = %w(all compute_engine_creds service_account_creds)
  105. if wants_creds.include?(opts.test_case)
  106. unless opts.oauth_scope.nil?
  107. auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
  108. call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
  109. stub_opts[:creds] = stub_opts[:creds].compose call_creds
  110. end
  111. end
  112. if opts.test_case == 'oauth2_auth_token'
  113. auth_creds = Google::Auth.get_application_default(opts.oauth_scope)
  114. kw = auth_creds.updater_proc.call({}) # gives as an auth token
  115. # use a metadata update proc that just adds the auth token.
  116. call_creds = GRPC::Core::CallCredentials.new(proc { |md| md.merge(kw) })
  117. stub_opts[:creds] = stub_opts[:creds].compose call_creds
  118. end
  119. if opts.test_case == 'jwt_token_creds' # don't use a scope
  120. auth_creds = Google::Auth.get_application_default
  121. call_creds = GRPC::Core::CallCredentials.new(auth_creds.updater_proc)
  122. stub_opts[:creds] = stub_opts[:creds].compose call_creds
  123. end
  124. GRPC.logger.info("... connecting securely to #{address}")
  125. Grpc::Testing::TestService::Stub.new(address, **stub_opts)
  126. else
  127. GRPC.logger.info("... connecting insecurely to #{address}")
  128. Grpc::Testing::TestService::Stub.new(address)
  129. end
  130. end
  131. # produces a string of null chars (\0) of length l.
  132. def nulls(l)
  133. fail 'requires #{l} to be +ve' if l < 0
  134. [].pack('x' * l).force_encoding('ascii-8bit')
  135. end
  136. # a PingPongPlayer implements the ping pong bidi test.
  137. class PingPongPlayer
  138. include Grpc::Testing
  139. include Grpc::Testing::PayloadType
  140. attr_accessor :queue
  141. attr_accessor :canceller_op
  142. # reqs is the enumerator over the requests
  143. def initialize(msg_sizes)
  144. @queue = Queue.new
  145. @msg_sizes = msg_sizes
  146. @canceller_op = nil # used to cancel after the first response
  147. end
  148. def each_item
  149. return enum_for(:each_item) unless block_given?
  150. req_cls, p_cls = StreamingOutputCallRequest, ResponseParameters # short
  151. count = 0
  152. @msg_sizes.each do |m|
  153. req_size, resp_size = m
  154. req = req_cls.new(payload: Payload.new(body: nulls(req_size)),
  155. response_type: :COMPRESSABLE,
  156. response_parameters: [p_cls.new(size: resp_size)])
  157. yield req
  158. resp = @queue.pop
  159. assert('payload type is wrong') { :COMPRESSABLE == resp.payload.type }
  160. assert("payload body #{count} has the wrong length") do
  161. resp_size == resp.payload.body.length
  162. end
  163. p "OK: ping_pong #{count}"
  164. count += 1
  165. unless @canceller_op.nil?
  166. canceller_op.cancel
  167. break
  168. end
  169. end
  170. end
  171. end
  172. # defines methods corresponding to each interop test case.
  173. class NamedTests
  174. include Grpc::Testing
  175. include Grpc::Testing::PayloadType
  176. def initialize(stub, args)
  177. @stub = stub
  178. @args = args
  179. end
  180. def empty_unary
  181. resp = @stub.empty_call(Empty.new)
  182. assert('empty_unary: invalid response') { resp.is_a?(Empty) }
  183. p 'OK: empty_unary'
  184. end
  185. def large_unary
  186. perform_large_unary
  187. p 'OK: large_unary'
  188. end
  189. def service_account_creds
  190. # ignore this test if the oauth options are not set
  191. if @args.oauth_scope.nil?
  192. p 'NOT RUN: service_account_creds; no service_account settings'
  193. return
  194. end
  195. json_key = File.read(ENV[AUTH_ENV])
  196. wanted_email = MultiJson.load(json_key)['client_email']
  197. resp = perform_large_unary(fill_username: true,
  198. fill_oauth_scope: true)
  199. assert("#{__callee__}: bad username") { wanted_email == resp.username }
  200. assert("#{__callee__}: bad oauth scope") do
  201. @args.oauth_scope.include?(resp.oauth_scope)
  202. end
  203. p "OK: #{__callee__}"
  204. end
  205. def jwt_token_creds
  206. json_key = File.read(ENV[AUTH_ENV])
  207. wanted_email = MultiJson.load(json_key)['client_email']
  208. resp = perform_large_unary(fill_username: true)
  209. assert("#{__callee__}: bad username") { wanted_email == resp.username }
  210. p "OK: #{__callee__}"
  211. end
  212. def compute_engine_creds
  213. resp = perform_large_unary(fill_username: true,
  214. fill_oauth_scope: true)
  215. assert("#{__callee__}: bad username") do
  216. @args.default_service_account == resp.username
  217. end
  218. p "OK: #{__callee__}"
  219. end
  220. def oauth2_auth_token
  221. resp = perform_large_unary(fill_username: true,
  222. fill_oauth_scope: true)
  223. json_key = File.read(ENV[AUTH_ENV])
  224. wanted_email = MultiJson.load(json_key)['client_email']
  225. assert("#{__callee__}: bad username") { wanted_email == resp.username }
  226. assert("#{__callee__}: bad oauth scope") do
  227. @args.oauth_scope.include?(resp.oauth_scope)
  228. end
  229. p "OK: #{__callee__}"
  230. end
  231. def per_rpc_creds
  232. auth_creds = Google::Auth.get_application_default(@args.oauth_scope)
  233. kw = auth_creds.updater_proc.call({})
  234. # TODO(jtattermusch): downcase the metadata keys here to make sure
  235. # they are not rejected by C core. This is a hotfix that should
  236. # be addressed by introducing auto-downcasing logic.
  237. kw = Hash[ kw.each_pair.map { |k, v| [k.downcase, v] }]
  238. resp = perform_large_unary(fill_username: true,
  239. fill_oauth_scope: true,
  240. **kw)
  241. json_key = File.read(ENV[AUTH_ENV])
  242. wanted_email = MultiJson.load(json_key)['client_email']
  243. assert("#{__callee__}: bad username") { wanted_email == resp.username }
  244. assert("#{__callee__}: bad oauth scope") do
  245. @args.oauth_scope.include?(resp.oauth_scope)
  246. end
  247. p "OK: #{__callee__}"
  248. end
  249. def client_streaming
  250. msg_sizes = [27_182, 8, 1828, 45_904]
  251. wanted_aggregate_size = 74_922
  252. reqs = msg_sizes.map do |x|
  253. req = Payload.new(body: nulls(x))
  254. StreamingInputCallRequest.new(payload: req)
  255. end
  256. resp = @stub.streaming_input_call(reqs)
  257. assert("#{__callee__}: aggregate payload size is incorrect") do
  258. wanted_aggregate_size == resp.aggregated_payload_size
  259. end
  260. p "OK: #{__callee__}"
  261. end
  262. def server_streaming
  263. msg_sizes = [31_415, 9, 2653, 58_979]
  264. response_spec = msg_sizes.map { |s| ResponseParameters.new(size: s) }
  265. req = StreamingOutputCallRequest.new(response_type: :COMPRESSABLE,
  266. response_parameters: response_spec)
  267. resps = @stub.streaming_output_call(req)
  268. resps.each_with_index do |r, i|
  269. assert("#{__callee__}: too many responses") { i < msg_sizes.length }
  270. assert("#{__callee__}: payload body #{i} has the wrong length") do
  271. msg_sizes[i] == r.payload.body.length
  272. end
  273. assert("#{__callee__}: payload type is wrong") do
  274. :COMPRESSABLE == r.payload.type
  275. end
  276. end
  277. p "OK: #{__callee__}"
  278. end
  279. def ping_pong
  280. msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
  281. ppp = PingPongPlayer.new(msg_sizes)
  282. resps = @stub.full_duplex_call(ppp.each_item)
  283. resps.each { |r| ppp.queue.push(r) }
  284. p "OK: #{__callee__}"
  285. end
  286. def timeout_on_sleeping_server
  287. msg_sizes = [[27_182, 31_415]]
  288. ppp = PingPongPlayer.new(msg_sizes)
  289. resps = @stub.full_duplex_call(ppp.each_item, timeout: 0.001)
  290. resps.each { |r| ppp.queue.push(r) }
  291. fail 'Should have raised GRPC::BadStatus(DEADLINE_EXCEEDED)'
  292. rescue GRPC::BadStatus => e
  293. assert("#{__callee__}: status was wrong") do
  294. e.code == GRPC::Core::StatusCodes::DEADLINE_EXCEEDED
  295. end
  296. p "OK: #{__callee__}"
  297. end
  298. def empty_stream
  299. ppp = PingPongPlayer.new([])
  300. resps = @stub.full_duplex_call(ppp.each_item)
  301. count = 0
  302. resps.each do |r|
  303. ppp.queue.push(r)
  304. count += 1
  305. end
  306. assert("#{__callee__}: too many responses expected 0") do
  307. count == 0
  308. end
  309. p "OK: #{__callee__}"
  310. end
  311. def cancel_after_begin
  312. msg_sizes = [27_182, 8, 1828, 45_904]
  313. reqs = msg_sizes.map do |x|
  314. req = Payload.new(body: nulls(x))
  315. StreamingInputCallRequest.new(payload: req)
  316. end
  317. op = @stub.streaming_input_call(reqs, return_op: true)
  318. op.cancel
  319. op.execute
  320. fail 'Should have raised GRPC:Cancelled'
  321. rescue GRPC::Cancelled
  322. assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
  323. p "OK: #{__callee__}"
  324. end
  325. def cancel_after_first_response
  326. msg_sizes = [[27_182, 31_415], [8, 9], [1828, 2653], [45_904, 58_979]]
  327. ppp = PingPongPlayer.new(msg_sizes)
  328. op = @stub.full_duplex_call(ppp.each_item, return_op: true)
  329. ppp.canceller_op = op # causes ppp to cancel after the 1st message
  330. op.execute.each { |r| ppp.queue.push(r) }
  331. fail 'Should have raised GRPC:Cancelled'
  332. rescue GRPC::Cancelled
  333. assert("#{__callee__}: call operation should be CANCELLED") { op.cancelled }
  334. op.wait
  335. p "OK: #{__callee__}"
  336. end
  337. def all
  338. all_methods = NamedTests.instance_methods(false).map(&:to_s)
  339. all_methods.each do |m|
  340. next if m == 'all' || m.start_with?('assert')
  341. p "TESTCASE: #{m}"
  342. method(m).call
  343. end
  344. end
  345. private
  346. def perform_large_unary(fill_username: false, fill_oauth_scope: false, **kw)
  347. req_size, wanted_response_size = 271_828, 314_159
  348. payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
  349. req = SimpleRequest.new(response_type: :COMPRESSABLE,
  350. response_size: wanted_response_size,
  351. payload: payload)
  352. req.fill_username = fill_username
  353. req.fill_oauth_scope = fill_oauth_scope
  354. resp = @stub.unary_call(req, **kw)
  355. assert('payload type is wrong') do
  356. :COMPRESSABLE == resp.payload.type
  357. end
  358. assert('payload body has the wrong length') do
  359. wanted_response_size == resp.payload.body.length
  360. end
  361. assert('payload body is invalid') do
  362. nulls(wanted_response_size) == resp.payload.body
  363. end
  364. resp
  365. end
  366. end
  367. # Args is used to hold the command line info.
  368. Args = Struct.new(:default_service_account, :host, :host_override,
  369. :oauth_scope, :port, :secure, :test_case,
  370. :use_test_ca)
  371. # validates the the command line options, returning them as a Hash.
  372. def parse_args
  373. args = Args.new
  374. args.host_override = 'foo.test.google.fr'
  375. OptionParser.new do |opts|
  376. opts.on('--oauth_scope scope',
  377. 'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
  378. opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
  379. args['host'] = v
  380. end
  381. opts.on('--default_service_account email_address',
  382. 'email address of the default service account') do |v|
  383. args['default_service_account'] = v
  384. end
  385. opts.on('--server_host_override HOST_OVERRIDE',
  386. 'override host via a HTTP header') do |v|
  387. args['host_override'] = v
  388. end
  389. opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
  390. # instance_methods(false) gives only the methods defined in that class
  391. test_cases = NamedTests.instance_methods(false).map(&:to_s)
  392. test_case_list = test_cases.join(',')
  393. opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
  394. " (#{test_case_list})") { |v| args['test_case'] = v }
  395. opts.on('--use_tls USE_TLS', ['false', 'true'],
  396. 'require a secure connection?') do |v|
  397. args['secure'] = v == 'true'
  398. end
  399. opts.on('--use_test_ca USE_TEST_CA', ['false', 'true'],
  400. 'if secure, use the test certificate?') do |v|
  401. args['use_test_ca'] = v == 'true'
  402. end
  403. end.parse!
  404. _check_args(args)
  405. end
  406. def _check_args(args)
  407. %w(host port test_case).each do |a|
  408. if args[a].nil?
  409. fail(OptionParser::MissingArgument, "please specify --#{a}")
  410. end
  411. end
  412. args
  413. end
  414. def main
  415. opts = parse_args
  416. stub = create_stub(opts)
  417. NamedTests.new(stub, opts).method(opts['test_case']).call
  418. end
  419. main