run_interop_tests.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973
  1. #!/usr/bin/env python2.7
  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. """Run interop (cross-language) tests in parallel."""
  31. from __future__ import print_function
  32. import argparse
  33. import atexit
  34. import itertools
  35. import json
  36. import multiprocessing
  37. import os
  38. import re
  39. import subprocess
  40. import sys
  41. import tempfile
  42. import time
  43. import uuid
  44. import python_utils.dockerjob as dockerjob
  45. import python_utils.jobset as jobset
  46. import python_utils.report_utils as report_utils
  47. # Docker doesn't clean up after itself, so we do it on exit.
  48. atexit.register(lambda: subprocess.call(['stty', 'echo']))
  49. ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
  50. os.chdir(ROOT)
  51. _DEFAULT_SERVER_PORT=8080
  52. _SKIP_CLIENT_COMPRESSION = ['client_compressed_unary',
  53. 'client_compressed_streaming']
  54. _SKIP_SERVER_COMPRESSION = ['server_compressed_unary',
  55. 'server_compressed_streaming']
  56. _SKIP_COMPRESSION = _SKIP_CLIENT_COMPRESSION + _SKIP_SERVER_COMPRESSION
  57. _SKIP_ADVANCED = ['status_code_and_message',
  58. 'custom_metadata',
  59. 'unimplemented_method',
  60. 'unimplemented_service']
  61. _TEST_TIMEOUT = 3*60
  62. class CXXLanguage:
  63. def __init__(self):
  64. self.client_cwd = None
  65. self.server_cwd = None
  66. self.safename = 'cxx'
  67. def client_cmd(self, args):
  68. return ['bins/opt/interop_client'] + args
  69. def cloud_to_prod_env(self):
  70. return {}
  71. def server_cmd(self, args):
  72. return ['bins/opt/interop_server', '--use_tls=true'] + args
  73. def global_env(self):
  74. return {}
  75. def unimplemented_test_cases(self):
  76. return []
  77. def unimplemented_test_cases_server(self):
  78. return []
  79. def __str__(self):
  80. return 'c++'
  81. class CSharpLanguage:
  82. def __init__(self):
  83. self.client_cwd = 'src/csharp/Grpc.IntegrationTesting.Client/bin/Debug'
  84. self.server_cwd = 'src/csharp/Grpc.IntegrationTesting.Server/bin/Debug'
  85. self.safename = str(self)
  86. def client_cmd(self, args):
  87. return ['mono', 'Grpc.IntegrationTesting.Client.exe'] + args
  88. def cloud_to_prod_env(self):
  89. return {}
  90. def server_cmd(self, args):
  91. return ['mono', 'Grpc.IntegrationTesting.Server.exe', '--use_tls=true'] + args
  92. def global_env(self):
  93. return {}
  94. def unimplemented_test_cases(self):
  95. return _SKIP_SERVER_COMPRESSION
  96. def unimplemented_test_cases_server(self):
  97. return _SKIP_COMPRESSION
  98. def __str__(self):
  99. return 'csharp'
  100. class CSharpCoreCLRLanguage:
  101. def __init__(self):
  102. self.client_cwd = 'src/csharp/Grpc.IntegrationTesting.Client/bin/Debug/netcoreapp1.0'
  103. self.server_cwd = 'src/csharp/Grpc.IntegrationTesting.Server/bin/Debug/netcoreapp1.0'
  104. self.safename = str(self)
  105. def client_cmd(self, args):
  106. return ['dotnet', 'exec', 'Grpc.IntegrationTesting.Client.dll'] + args
  107. def cloud_to_prod_env(self):
  108. return {}
  109. def server_cmd(self, args):
  110. return ['dotnet', 'exec', 'Grpc.IntegrationTesting.Server.dll', '--use_tls=true'] + args
  111. def global_env(self):
  112. return {}
  113. def unimplemented_test_cases(self):
  114. return _SKIP_SERVER_COMPRESSION
  115. def unimplemented_test_cases_server(self):
  116. return _SKIP_COMPRESSION
  117. def __str__(self):
  118. return 'csharpcoreclr'
  119. class JavaLanguage:
  120. def __init__(self):
  121. self.client_cwd = '../grpc-java'
  122. self.server_cwd = '../grpc-java'
  123. self.safename = str(self)
  124. def client_cmd(self, args):
  125. return ['./run-test-client.sh'] + args
  126. def client_cmd_http2interop(self, args):
  127. return ['./run-http2-client.sh'] + args
  128. def cloud_to_prod_env(self):
  129. return {}
  130. def server_cmd(self, args):
  131. return ['./run-test-server.sh', '--use_tls=true'] + args
  132. def global_env(self):
  133. return {}
  134. def unimplemented_test_cases(self):
  135. return _SKIP_ADVANCED + _SKIP_COMPRESSION
  136. def unimplemented_test_cases_server(self):
  137. return _SKIP_ADVANCED + _SKIP_COMPRESSION
  138. def __str__(self):
  139. return 'java'
  140. class GoLanguage:
  141. def __init__(self):
  142. # TODO: this relies on running inside docker
  143. self.client_cwd = '/go/src/google.golang.org/grpc/interop/client'
  144. self.server_cwd = '/go/src/google.golang.org/grpc/interop/server'
  145. self.safename = str(self)
  146. def client_cmd(self, args):
  147. return ['go', 'run', 'client.go'] + args
  148. def cloud_to_prod_env(self):
  149. return {}
  150. def server_cmd(self, args):
  151. return ['go', 'run', 'server.go', '--use_tls=true'] + args
  152. def global_env(self):
  153. return {}
  154. def unimplemented_test_cases(self):
  155. return _SKIP_COMPRESSION
  156. def unimplemented_test_cases_server(self):
  157. return _SKIP_COMPRESSION
  158. def __str__(self):
  159. return 'go'
  160. class Http2Client:
  161. """Represents the HTTP/2 Interop Test
  162. This pretends to be a language in order to be built and run, but really it
  163. isn't.
  164. """
  165. def __init__(self):
  166. self.client_cwd = None
  167. self.server_cwd = None
  168. self.safename = str(self)
  169. def client_cmd(self, args):
  170. return ['tools/http2_interop/http2_interop.test', '-test.v'] + args
  171. def server_cmd(self, args):
  172. return ['python test/http2_test/http2_test_server.py']
  173. def cloud_to_prod_env(self):
  174. return {}
  175. def global_env(self):
  176. return {}
  177. def unimplemented_test_cases(self):
  178. return _TEST_CASES
  179. def unimplemented_test_cases_server(self):
  180. return []
  181. def __str__(self):
  182. return 'http2'
  183. class NodeLanguage:
  184. def __init__(self):
  185. self.client_cwd = None
  186. self.server_cwd = None
  187. self.safename = str(self)
  188. def client_cmd(self, args):
  189. return ['tools/run_tests/interop/with_nvm.sh',
  190. 'node', 'src/node/interop/interop_client.js'] + args
  191. def cloud_to_prod_env(self):
  192. return {}
  193. def server_cmd(self, args):
  194. return ['tools/run_tests/interop/with_nvm.sh',
  195. 'node', 'src/node/interop/interop_server.js',
  196. '--use_tls=true'] + args
  197. def global_env(self):
  198. return {}
  199. def unimplemented_test_cases(self):
  200. return _SKIP_COMPRESSION
  201. def unimplemented_test_cases_server(self):
  202. return _SKIP_COMPRESSION
  203. def __str__(self):
  204. return 'node'
  205. class PHPLanguage:
  206. def __init__(self):
  207. self.client_cwd = None
  208. self.safename = str(self)
  209. def client_cmd(self, args):
  210. return ['src/php/bin/interop_client.sh'] + args
  211. def cloud_to_prod_env(self):
  212. return {}
  213. def global_env(self):
  214. return {}
  215. def unimplemented_test_cases(self):
  216. return _SKIP_COMPRESSION
  217. def unimplemented_test_cases_server(self):
  218. return []
  219. def __str__(self):
  220. return 'php'
  221. class PHP7Language:
  222. def __init__(self):
  223. self.client_cwd = None
  224. self.safename = str(self)
  225. def client_cmd(self, args):
  226. return ['src/php/bin/interop_client.sh'] + args
  227. def cloud_to_prod_env(self):
  228. return {}
  229. def global_env(self):
  230. return {}
  231. def unimplemented_test_cases(self):
  232. return _SKIP_COMPRESSION
  233. def unimplemented_test_cases_server(self):
  234. return []
  235. def __str__(self):
  236. return 'php7'
  237. class RubyLanguage:
  238. def __init__(self):
  239. self.client_cwd = None
  240. self.server_cwd = None
  241. self.safename = str(self)
  242. def client_cmd(self, args):
  243. return ['tools/run_tests/interop/with_rvm.sh',
  244. 'ruby', 'src/ruby/pb/test/client.rb'] + args
  245. def cloud_to_prod_env(self):
  246. return {}
  247. def server_cmd(self, args):
  248. return ['tools/run_tests/interop/with_rvm.sh',
  249. 'ruby', 'src/ruby/pb/test/server.rb', '--use_tls=true'] + args
  250. def global_env(self):
  251. return {}
  252. def unimplemented_test_cases(self):
  253. return _SKIP_ADVANCED + _SKIP_SERVER_COMPRESSION
  254. def unimplemented_test_cases_server(self):
  255. return _SKIP_ADVANCED + _SKIP_COMPRESSION
  256. def __str__(self):
  257. return 'ruby'
  258. class PythonLanguage:
  259. def __init__(self):
  260. self.client_cwd = None
  261. self.server_cwd = None
  262. self.safename = str(self)
  263. def client_cmd(self, args):
  264. return [
  265. 'py27/bin/python',
  266. 'src/python/grpcio_tests/setup.py',
  267. 'run_interop',
  268. '--client',
  269. '--args="{}"'.format(' '.join(args))
  270. ]
  271. def client_cmd_http2interop(self, args):
  272. return [ 'py27/bin/python',
  273. 'src/python/grpcio_tests/tests/http2/_negative_http2_client.py',
  274. ] + args
  275. def cloud_to_prod_env(self):
  276. return {}
  277. def server_cmd(self, args):
  278. return [
  279. 'py27/bin/python',
  280. 'src/python/grpcio_tests/setup.py',
  281. 'run_interop',
  282. '--server',
  283. '--args="{}"'.format(' '.join(args) + ' --use_tls=true')
  284. ]
  285. def global_env(self):
  286. return {'LD_LIBRARY_PATH': '{}/libs/opt'.format(DOCKER_WORKDIR_ROOT),
  287. 'PYTHONPATH': '{}/src/python/gens'.format(DOCKER_WORKDIR_ROOT)}
  288. def unimplemented_test_cases(self):
  289. return _SKIP_COMPRESSION
  290. def unimplemented_test_cases_server(self):
  291. return _SKIP_COMPRESSION
  292. def __str__(self):
  293. return 'python'
  294. _LANGUAGES = {
  295. 'c++' : CXXLanguage(),
  296. 'csharp' : CSharpLanguage(),
  297. 'csharpcoreclr' : CSharpCoreCLRLanguage(),
  298. 'go' : GoLanguage(),
  299. 'java' : JavaLanguage(),
  300. 'node' : NodeLanguage(),
  301. 'php' : PHPLanguage(),
  302. 'php7' : PHP7Language(),
  303. 'ruby' : RubyLanguage(),
  304. 'python' : PythonLanguage(),
  305. }
  306. # languages supported as cloud_to_cloud servers
  307. _SERVERS = ['c++', 'node', 'csharp', 'csharpcoreclr', 'java', 'go', 'ruby', 'python']
  308. _TEST_CASES = ['large_unary', 'empty_unary', 'ping_pong',
  309. 'empty_stream', 'client_streaming', 'server_streaming',
  310. 'cancel_after_begin', 'cancel_after_first_response',
  311. 'timeout_on_sleeping_server', 'custom_metadata',
  312. 'status_code_and_message', 'unimplemented_method',
  313. 'client_compressed_unary', 'server_compressed_unary',
  314. 'client_compressed_streaming', 'server_compressed_streaming',
  315. 'unimplemented_service']
  316. _AUTH_TEST_CASES = ['compute_engine_creds', 'jwt_token_creds',
  317. 'oauth2_auth_token', 'per_rpc_creds']
  318. _HTTP2_TEST_CASES = ['tls', 'framing']
  319. _HTTP2_BADSERVER_TEST_CASES = ['rst_after_header', 'rst_after_data', 'rst_during_data',
  320. 'goaway', 'ping', 'max_streams']
  321. DOCKER_WORKDIR_ROOT = '/var/local/git/grpc'
  322. def docker_run_cmdline(cmdline, image, docker_args=[], cwd=None, environ=None):
  323. """Wraps given cmdline array to create 'docker run' cmdline from it."""
  324. docker_cmdline = ['docker', 'run', '-i', '--rm=true']
  325. # turn environ into -e docker args
  326. if environ:
  327. for k,v in environ.items():
  328. docker_cmdline += ['-e', '%s=%s' % (k,v)]
  329. # set working directory
  330. workdir = DOCKER_WORKDIR_ROOT
  331. if cwd:
  332. workdir = os.path.join(workdir, cwd)
  333. docker_cmdline += ['-w', workdir]
  334. docker_cmdline += docker_args + [image] + cmdline
  335. return docker_cmdline
  336. def bash_cmdline(cmdline):
  337. """Creates bash -c cmdline from args list."""
  338. # Use login shell:
  339. # * makes error messages clearer if executables are missing
  340. return ['bash', '-c', ' '.join(cmdline)]
  341. def auth_options(language, test_case):
  342. """Returns (cmdline, env) tuple with cloud_to_prod_auth test options."""
  343. language = str(language)
  344. cmdargs = []
  345. env = {}
  346. # TODO(jtattermusch): this file path only works inside docker
  347. key_filepath = '/root/service_account/stubbyCloudTestingTest-ee3fce360ac5.json'
  348. oauth_scope_arg = '--oauth_scope=https://www.googleapis.com/auth/xapi.zoo'
  349. key_file_arg = '--service_account_key_file=%s' % key_filepath
  350. default_account_arg = '--default_service_account=830293263384-compute@developer.gserviceaccount.com'
  351. if test_case in ['jwt_token_creds', 'per_rpc_creds', 'oauth2_auth_token']:
  352. if language in ['csharp', 'csharpcoreclr', 'node', 'php', 'php7', 'python', 'ruby']:
  353. env['GOOGLE_APPLICATION_CREDENTIALS'] = key_filepath
  354. else:
  355. cmdargs += [key_file_arg]
  356. if test_case in ['per_rpc_creds', 'oauth2_auth_token']:
  357. cmdargs += [oauth_scope_arg]
  358. if test_case == 'oauth2_auth_token' and language == 'c++':
  359. # C++ oauth2 test uses GCE creds and thus needs to know the default account
  360. cmdargs += [default_account_arg]
  361. if test_case == 'compute_engine_creds':
  362. cmdargs += [oauth_scope_arg, default_account_arg]
  363. return (cmdargs, env)
  364. def _job_kill_handler(job):
  365. if job._spec.container_name:
  366. dockerjob.docker_kill(job._spec.container_name)
  367. # When the job times out and we decide to kill it,
  368. # we need to wait a before restarting the job
  369. # to prevent "container name already in use" error.
  370. # TODO(jtattermusch): figure out a cleaner way to to this.
  371. time.sleep(2)
  372. def cloud_to_prod_jobspec(language, test_case, server_host_name,
  373. server_host_detail, docker_image=None, auth=False):
  374. """Creates jobspec for cloud-to-prod interop test"""
  375. container_name = None
  376. cmdargs = [
  377. '--server_host=%s' % server_host_detail[0],
  378. '--server_host_override=%s' % server_host_detail[1],
  379. '--server_port=443',
  380. '--use_tls=true',
  381. '--test_case=%s' % test_case]
  382. environ = dict(language.cloud_to_prod_env(), **language.global_env())
  383. if auth:
  384. auth_cmdargs, auth_env = auth_options(language, test_case)
  385. cmdargs += auth_cmdargs
  386. environ.update(auth_env)
  387. cmdline = bash_cmdline(language.client_cmd(cmdargs))
  388. cwd = language.client_cwd
  389. if docker_image:
  390. container_name = dockerjob.random_name('interop_client_%s' %
  391. language.safename)
  392. cmdline = docker_run_cmdline(cmdline,
  393. image=docker_image,
  394. cwd=cwd,
  395. environ=environ,
  396. docker_args=['--net=host',
  397. '--name', container_name])
  398. cwd = None
  399. environ = None
  400. suite_name='cloud_to_prod_auth' if auth else 'cloud_to_prod'
  401. test_job = jobset.JobSpec(
  402. cmdline=cmdline,
  403. cwd=cwd,
  404. environ=environ,
  405. shortname='%s:%s:%s:%s' % (suite_name, server_host_name, language,
  406. test_case),
  407. timeout_seconds=_TEST_TIMEOUT,
  408. flake_retries=5 if args.allow_flakes else 0,
  409. timeout_retries=2 if args.allow_flakes else 0,
  410. kill_handler=_job_kill_handler)
  411. if docker_image:
  412. test_job.container_name = container_name
  413. return test_job
  414. def cloud_to_cloud_jobspec(language, test_case, server_name, server_host,
  415. server_port, docker_image=None):
  416. """Creates jobspec for cloud-to-cloud interop test"""
  417. interop_only_options = [
  418. '--server_host_override=foo.test.google.fr',
  419. '--use_tls=true',
  420. '--use_test_ca=true',
  421. ]
  422. common_options = [
  423. '--test_case=%s' % test_case,
  424. '--server_host=%s' % server_host,
  425. ]
  426. if test_case in sorted(_HTTP2_BADSERVER_TEST_CASES):
  427. # We are running the http2_badserver_interop test. Adjust command line accordingly.
  428. offset = sorted(_HTTP2_BADSERVER_TEST_CASES).index(test_case)
  429. client_options = common_options + ['--server_port=%s' %
  430. (int(server_port)+offset)]
  431. cmdline = bash_cmdline(language.client_cmd_http2interop(client_options))
  432. else:
  433. client_options = interop_only_options + common_options + ['--server_port=%s' % server_port]
  434. cmdline = bash_cmdline(language.client_cmd(client_options))
  435. print('Client_CMD = %s'%cmdline)
  436. cwd = language.client_cwd
  437. environ = language.global_env()
  438. if docker_image:
  439. container_name = dockerjob.random_name('interop_client_%s' % language.safename)
  440. cmdline = docker_run_cmdline(cmdline,
  441. image=docker_image,
  442. environ=environ,
  443. cwd=cwd,
  444. docker_args=['--net=host',
  445. '--name', container_name])
  446. cwd = None
  447. test_job = jobset.JobSpec(
  448. cmdline=cmdline,
  449. cwd=cwd,
  450. environ=environ,
  451. shortname='cloud_to_cloud:%s:%s_server:%s' % (language, server_name,
  452. test_case),
  453. timeout_seconds=_TEST_TIMEOUT,
  454. flake_retries=5 if args.allow_flakes else 0,
  455. timeout_retries=2 if args.allow_flakes else 0,
  456. kill_handler=_job_kill_handler)
  457. if docker_image:
  458. test_job.container_name = container_name
  459. return test_job
  460. def server_jobspec(language, docker_image):
  461. """Create jobspec for running a server"""
  462. container_name = dockerjob.random_name('interop_server_%s' % language.safename)
  463. cmdline = bash_cmdline(
  464. language.server_cmd(['--port=%s' % _DEFAULT_SERVER_PORT]))
  465. environ = language.global_env()
  466. if language.safename == 'http2':
  467. # we are running the http2 interop server. Open next N ports beginning
  468. # with the server port. These ports are used for http2 interop test
  469. # (one test case per port). We also attach the docker container running
  470. # the server to local network, so we don't have to mess with port mapping
  471. port_args = [
  472. '-p', str(_DEFAULT_SERVER_PORT+0),
  473. '-p', str(_DEFAULT_SERVER_PORT+1),
  474. '-p', str(_DEFAULT_SERVER_PORT+2),
  475. '-p', str(_DEFAULT_SERVER_PORT+3),
  476. '-p', str(_DEFAULT_SERVER_PORT+4),
  477. '-p', str(_DEFAULT_SERVER_PORT+5),
  478. '-p', str(_DEFAULT_SERVER_PORT+6),
  479. '--net=host',
  480. ]
  481. else:
  482. port_args = ['-p', str(_DEFAULT_SERVER_PORT)]
  483. docker_cmdline = docker_run_cmdline(cmdline,
  484. image=docker_image,
  485. cwd=language.server_cwd,
  486. environ=environ,
  487. docker_args=port_args +
  488. ['--name', container_name])
  489. server_job = jobset.JobSpec(
  490. cmdline=docker_cmdline,
  491. environ=environ,
  492. shortname='interop_server_%s' % language,
  493. timeout_seconds=30*60)
  494. server_job.container_name = container_name
  495. return server_job
  496. def build_interop_image_jobspec(language, tag=None):
  497. """Creates jobspec for building interop docker image for a language"""
  498. if not tag:
  499. tag = 'grpc_interop_%s:%s' % (language.safename, uuid.uuid4())
  500. env = {'INTEROP_IMAGE': tag,
  501. 'BASE_NAME': 'grpc_interop_%s' % language.safename}
  502. if not args.travis:
  503. env['TTY_FLAG'] = '-t'
  504. # This env variable is used to get around the github rate limit
  505. # error when running the PHP `composer install` command
  506. host_file = '%s/.composer/auth.json' % os.environ['HOME']
  507. if language.safename == 'php' and os.path.exists(host_file):
  508. env['BUILD_INTEROP_DOCKER_EXTRA_ARGS'] = \
  509. '-v %s:/root/.composer/auth.json:ro' % host_file
  510. build_job = jobset.JobSpec(
  511. cmdline=['tools/run_tests/dockerize/build_interop_image.sh'],
  512. environ=env,
  513. shortname='build_docker_%s' % (language),
  514. timeout_seconds=30*60)
  515. build_job.tag = tag
  516. return build_job
  517. def aggregate_http2_results(stdout):
  518. match = re.search(r'\{"cases[^\]]*\]\}', stdout)
  519. if not match:
  520. return None
  521. results = json.loads(match.group(0))
  522. skipped = 0
  523. passed = 0
  524. failed = 0
  525. failed_cases = []
  526. for case in results['cases']:
  527. if case.get('skipped', False):
  528. skipped += 1
  529. else:
  530. if case.get('passed', False):
  531. passed += 1
  532. else:
  533. failed += 1
  534. failed_cases.append(case.get('name', "NONAME"))
  535. return {
  536. 'passed': passed,
  537. 'failed': failed,
  538. 'skipped': skipped,
  539. 'failed_cases': ', '.join(failed_cases),
  540. 'percent': 1.0 * passed / (passed + failed)
  541. }
  542. # A dictionary of prod servers to test.
  543. # Format: server_name: (server_host, server_host_override, errors_allowed)
  544. # TODO(adelez): implement logic for errors_allowed where if the indicated tests
  545. # fail, they don't impact the overall test result.
  546. prod_servers = {
  547. 'default': ('216.239.32.254',
  548. 'grpc-test.sandbox.googleapis.com', False),
  549. 'gateway_v2': ('216.239.32.254',
  550. 'grpc-test2.sandbox.googleapis.com', True),
  551. 'cloud_gateway': ('216.239.32.255', 'grpc-test.sandbox.googleapis.com',
  552. False),
  553. 'cloud_gateway_v2': ('216.239.32.255', 'grpc-test2.sandbox.googleapis.com',
  554. True),
  555. 'gateway_v4': ('216.239.32.254',
  556. 'grpc-test4.sandbox.googleapis.com', True),
  557. 'cloud_gateway_v4': ('216.239.32.255', 'grpc-test4.sandbox.googleapis.com',
  558. True),
  559. }
  560. argp = argparse.ArgumentParser(description='Run interop tests.')
  561. argp.add_argument('-l', '--language',
  562. choices=['all'] + sorted(_LANGUAGES),
  563. nargs='+',
  564. default=['all'],
  565. help='Clients to run.')
  566. argp.add_argument('-j', '--jobs', default=multiprocessing.cpu_count(), type=int)
  567. argp.add_argument('--cloud_to_prod',
  568. default=False,
  569. action='store_const',
  570. const=True,
  571. help='Run cloud_to_prod tests.')
  572. argp.add_argument('--cloud_to_prod_auth',
  573. default=False,
  574. action='store_const',
  575. const=True,
  576. help='Run cloud_to_prod_auth tests.')
  577. argp.add_argument('--prod_servers',
  578. choices=prod_servers.keys(),
  579. default=['default'],
  580. nargs='+',
  581. help=('The servers to run cloud_to_prod and '
  582. 'cloud_to_prod_auth tests against.'))
  583. argp.add_argument('-s', '--server',
  584. choices=['all'] + sorted(_SERVERS),
  585. nargs='+',
  586. help='Run cloud_to_cloud servers in a separate docker ' +
  587. 'image. Servers can only be started automatically if ' +
  588. '--use_docker option is enabled.',
  589. default=[])
  590. argp.add_argument('--override_server',
  591. action='append',
  592. type=lambda kv: kv.split('='),
  593. help='Use servername=HOST:PORT to explicitly specify a server. E.g. csharp=localhost:50000',
  594. default=[])
  595. argp.add_argument('-t', '--travis',
  596. default=False,
  597. action='store_const',
  598. const=True)
  599. argp.add_argument('--use_docker',
  600. default=False,
  601. action='store_const',
  602. const=True,
  603. help='Run all the interop tests under docker. That provides ' +
  604. 'additional isolation and prevents the need to install ' +
  605. 'language specific prerequisites. Only available on Linux.')
  606. argp.add_argument('--allow_flakes',
  607. default=False,
  608. action='store_const',
  609. const=True,
  610. help='Allow flaky tests to show as passing (re-runs failed tests up to five times)')
  611. argp.add_argument('--http2_interop',
  612. default=False,
  613. action='store_const',
  614. const=True,
  615. help='Enable HTTP/2 client edge case testing. (Bad client, good server)')
  616. argp.add_argument('--http2_badserver_interop',
  617. default=False,
  618. action='store_const',
  619. const=True,
  620. help='Enable HTTP/2 server edge case testing. (Good client, bad server)')
  621. args = argp.parse_args()
  622. servers = set(s for s in itertools.chain.from_iterable(_SERVERS
  623. if x == 'all' else [x]
  624. for x in args.server))
  625. if args.use_docker:
  626. if not args.travis:
  627. print('Seen --use_docker flag, will run interop tests under docker.')
  628. print('')
  629. print('IMPORTANT: The changes you are testing need to be locally committed')
  630. print('because only the committed changes in the current branch will be')
  631. print('copied to the docker environment.')
  632. time.sleep(5)
  633. if not args.use_docker and servers:
  634. print('Running interop servers is only supported with --use_docker option enabled.')
  635. sys.exit(1)
  636. languages = set(_LANGUAGES[l]
  637. for l in itertools.chain.from_iterable(
  638. _LANGUAGES.iterkeys() if x == 'all' else [x]
  639. for x in args.language))
  640. http2Interop = Http2Client() if (args.http2_badserver_interop
  641. or args.http2_interop) else None
  642. docker_images={}
  643. if args.use_docker:
  644. # languages for which to build docker images
  645. languages_to_build = set(_LANGUAGES[k] for k in set([str(l) for l in languages] +
  646. [s for s in servers]))
  647. if args.http2_interop or args.http2_badserver_interop:
  648. languages_to_build.add(http2Interop)
  649. build_jobs = []
  650. for l in languages_to_build:
  651. job = build_interop_image_jobspec(l)
  652. docker_images[str(l)] = job.tag
  653. build_jobs.append(job)
  654. if build_jobs:
  655. jobset.message('START', 'Building interop docker images.', do_newline=True)
  656. num_failures, _ = jobset.run(
  657. build_jobs, newline_on_success=True, maxjobs=args.jobs)
  658. if num_failures == 0:
  659. jobset.message('SUCCESS', 'All docker images built successfully.',
  660. do_newline=True)
  661. else:
  662. jobset.message('FAILED', 'Failed to build interop docker images.',
  663. do_newline=True)
  664. for image in docker_images.itervalues():
  665. dockerjob.remove_image(image, skip_nonexistent=True)
  666. sys.exit(1)
  667. # Start interop servers.
  668. server_jobs={}
  669. server_addresses={}
  670. try:
  671. for s in servers:
  672. lang = str(s)
  673. spec = server_jobspec(_LANGUAGES[lang], docker_images.get(lang))
  674. job = dockerjob.DockerJob(spec)
  675. server_jobs[lang] = job
  676. server_addresses[lang] = ('localhost', job.mapped_port(_DEFAULT_SERVER_PORT))
  677. if args.http2_badserver_interop:
  678. # launch a HTTP2 server emulator that creates edge cases
  679. lang = str(http2Interop)
  680. spec = server_jobspec(Http2Client(), docker_images.get(lang))
  681. job = dockerjob.DockerJob(spec)
  682. server_jobs[lang] = job
  683. server_addresses[lang] = ('localhost', _DEFAULT_SERVER_PORT)
  684. jobs = []
  685. if args.cloud_to_prod:
  686. for server_host_name in args.prod_servers:
  687. for language in languages:
  688. for test_case in _TEST_CASES:
  689. if not test_case in language.unimplemented_test_cases():
  690. if not test_case in _SKIP_ADVANCED + _SKIP_COMPRESSION:
  691. test_job = cloud_to_prod_jobspec(
  692. language, test_case, server_host_name,
  693. prod_servers[server_host_name],
  694. docker_image=docker_images.get(str(language)))
  695. jobs.append(test_job)
  696. if args.http2_interop:
  697. for test_case in _HTTP2_TEST_CASES:
  698. test_job = cloud_to_prod_jobspec(
  699. http2Interop, test_case, server_host_name,
  700. prod_servers[server_host_name],
  701. docker_image=docker_images.get(str(http2Interop)))
  702. jobs.append(test_job)
  703. if args.cloud_to_prod_auth:
  704. for server_host_name in args.prod_servers:
  705. for language in languages:
  706. for test_case in _AUTH_TEST_CASES:
  707. if not test_case in language.unimplemented_test_cases():
  708. test_job = cloud_to_prod_jobspec(
  709. language, test_case, server_host_name,
  710. prod_servers[server_host_name],
  711. docker_image=docker_images.get(str(language)), auth=True)
  712. jobs.append(test_job)
  713. for server in args.override_server:
  714. server_name = server[0]
  715. (server_host, server_port) = server[1].split(':')
  716. server_addresses[server_name] = (server_host, server_port)
  717. for server_name, server_address in server_addresses.items():
  718. (server_host, server_port) = server_address
  719. server_language = _LANGUAGES.get(server_name, None)
  720. skip_server = [] # test cases unimplemented by server
  721. if server_language:
  722. skip_server = server_language.unimplemented_test_cases_server()
  723. for language in languages:
  724. for test_case in _TEST_CASES:
  725. if not test_case in language.unimplemented_test_cases():
  726. if not test_case in skip_server and not args.http2_badserver_interop:
  727. test_job = cloud_to_cloud_jobspec(language,
  728. test_case,
  729. server_name,
  730. server_host,
  731. server_port,
  732. docker_image=docker_images.get(str(language)))
  733. jobs.append(test_job)
  734. if args.http2_interop:
  735. for test_case in _HTTP2_TEST_CASES:
  736. if server_name == "go":
  737. # TODO(carl-mastrangelo): Reenable after https://github.com/grpc/grpc-go/issues/434
  738. continue
  739. test_job = cloud_to_cloud_jobspec(http2Interop,
  740. test_case,
  741. server_name,
  742. server_host,
  743. server_port,
  744. docker_image=docker_images.get(str(http2Interop)))
  745. jobs.append(test_job)
  746. if args.http2_badserver_interop:
  747. for language in languages:
  748. for test_case in _HTTP2_BADSERVER_TEST_CASES:
  749. test_job = cloud_to_cloud_jobspec(language,
  750. test_case,
  751. server_name,
  752. server_host,
  753. server_port,
  754. docker_image=docker_images.get(str(language)))
  755. jobs.append(test_job)
  756. if not jobs:
  757. print('No jobs to run.')
  758. for image in docker_images.itervalues():
  759. dockerjob.remove_image(image, skip_nonexistent=True)
  760. sys.exit(1)
  761. num_failures, resultset = jobset.run(jobs, newline_on_success=True,
  762. maxjobs=args.jobs)
  763. if num_failures:
  764. jobset.message('FAILED', 'Some tests failed', do_newline=True)
  765. else:
  766. jobset.message('SUCCESS', 'All tests passed', do_newline=True)
  767. report_utils.render_junit_xml_report(resultset, 'report.xml')
  768. for name, job in resultset.items():
  769. if "http2" in name:
  770. job[0].http2results = aggregate_http2_results(job[0].message)
  771. report_utils.render_interop_html_report(
  772. set([str(l) for l in languages]), servers, _TEST_CASES, _AUTH_TEST_CASES,
  773. _HTTP2_TEST_CASES, resultset, num_failures,
  774. args.cloud_to_prod_auth or args.cloud_to_prod, args.prod_servers,
  775. args.http2_interop)
  776. finally:
  777. # Check if servers are still running.
  778. for server, job in server_jobs.items():
  779. if not job.is_running():
  780. print('Server "%s" has exited prematurely.' % server)
  781. dockerjob.finish_jobs([j for j in server_jobs.itervalues()])
  782. for image in docker_images.itervalues():
  783. print('Removing docker image %s' % image)
  784. dockerjob.remove_image(image)