run_tests_matrix.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. #!/usr/bin/env python
  2. # Copyright 2015 gRPC authors.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Run test matrix."""
  16. from __future__ import print_function
  17. import argparse
  18. import multiprocessing
  19. import os
  20. import sys
  21. from python_utils.filter_pull_request_tests import filter_tests
  22. import python_utils.jobset as jobset
  23. import python_utils.report_utils as report_utils
  24. _ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
  25. os.chdir(_ROOT)
  26. _DEFAULT_RUNTESTS_TIMEOUT = 1 * 60 * 60
  27. # C/C++ tests can take long time
  28. _CPP_RUNTESTS_TIMEOUT = 4 * 60 * 60
  29. # Set timeout high for ObjC for Cocoapods to install pods
  30. _OBJC_RUNTESTS_TIMEOUT = 2 * 60 * 60
  31. # Number of jobs assigned to each run_tests.py instance
  32. _DEFAULT_INNER_JOBS = 2
  33. # Name of the top-level umbrella report that includes all the run_tests.py invocations
  34. # Note that the starting letter 't' matters so that the targets are listed AFTER
  35. # the per-test breakdown items that start with 'run_tests/' (it is more readable that way)
  36. _MATRIX_REPORT_NAME = 'toplevel_run_tests_invocations'
  37. def _safe_report_name(name):
  38. """Reports with '+' in target name won't show correctly in ResultStore"""
  39. return name.replace('+', 'p')
  40. def _report_filename(name):
  41. """Generates report file name with directory structure that leads to better presentation by internal CI"""
  42. # 'sponge_log.xml' suffix must be there for results to get recognized by kokoro.
  43. return '%s/%s' % (_safe_report_name(name), 'sponge_log.xml')
  44. def _matrix_job_logfilename(shortname_for_multi_target):
  45. """Generate location for log file that will match the sponge_log.xml from the top-level matrix report."""
  46. # 'sponge_log.log' suffix must be there for log to get recognized as "target log"
  47. # for the corresponding 'sponge_log.xml' report.
  48. # the shortname_for_multi_target component must be set to match the sponge_log.xml location
  49. # because the top-level render_junit_xml_report is called with multi_target=True
  50. sponge_log_name = '%s/%s/%s' % (
  51. _MATRIX_REPORT_NAME, shortname_for_multi_target, 'sponge_log.log')
  52. # env variable can be used to override the base location for the reports
  53. # so we need to match that behavior here too
  54. base_dir = os.getenv('GRPC_TEST_REPORT_BASE_DIR', None)
  55. if base_dir:
  56. sponge_log_name = os.path.join(base_dir, sponge_log_name)
  57. return sponge_log_name
  58. def _docker_jobspec(name,
  59. runtests_args=[],
  60. runtests_envs={},
  61. inner_jobs=_DEFAULT_INNER_JOBS,
  62. timeout_seconds=None):
  63. """Run a single instance of run_tests.py in a docker container"""
  64. if not timeout_seconds:
  65. timeout_seconds = _DEFAULT_RUNTESTS_TIMEOUT
  66. shortname = 'run_tests_%s' % name
  67. test_job = jobset.JobSpec(cmdline=[
  68. 'python3', 'tools/run_tests/run_tests.py', '--use_docker', '-t', '-j',
  69. str(inner_jobs), '-x',
  70. 'run_tests/%s' % _report_filename(name), '--report_suite_name',
  71. '%s' % _safe_report_name(name)
  72. ] + runtests_args,
  73. environ=runtests_envs,
  74. shortname=shortname,
  75. timeout_seconds=timeout_seconds,
  76. logfilename=_matrix_job_logfilename(shortname))
  77. return test_job
  78. def _workspace_jobspec(name,
  79. runtests_args=[],
  80. workspace_name=None,
  81. runtests_envs={},
  82. inner_jobs=_DEFAULT_INNER_JOBS,
  83. timeout_seconds=None):
  84. """Run a single instance of run_tests.py in a separate workspace"""
  85. if not workspace_name:
  86. workspace_name = 'workspace_%s' % name
  87. if not timeout_seconds:
  88. timeout_seconds = _DEFAULT_RUNTESTS_TIMEOUT
  89. shortname = 'run_tests_%s' % name
  90. env = {'WORKSPACE_NAME': workspace_name}
  91. env.update(runtests_envs)
  92. # if report base dir is set, we don't need to ".." to come out of the workspace dir
  93. report_dir_prefix = '' if os.getenv('GRPC_TEST_REPORT_BASE_DIR',
  94. None) else '../'
  95. test_job = jobset.JobSpec(cmdline=[
  96. 'bash', 'tools/run_tests/helper_scripts/run_tests_in_workspace.sh',
  97. '-t', '-j',
  98. str(inner_jobs), '-x',
  99. '%srun_tests/%s' %
  100. (report_dir_prefix, _report_filename(name)), '--report_suite_name',
  101. '%s' % _safe_report_name(name)
  102. ] + runtests_args,
  103. environ=env,
  104. shortname=shortname,
  105. timeout_seconds=timeout_seconds,
  106. logfilename=_matrix_job_logfilename(shortname))
  107. return test_job
  108. def _generate_jobs(languages,
  109. configs,
  110. platforms,
  111. iomgr_platforms=['native'],
  112. arch=None,
  113. compiler=None,
  114. labels=[],
  115. extra_args=[],
  116. extra_envs={},
  117. inner_jobs=_DEFAULT_INNER_JOBS,
  118. timeout_seconds=None):
  119. result = []
  120. for language in languages:
  121. for platform in platforms:
  122. for iomgr_platform in iomgr_platforms:
  123. for config in configs:
  124. name = '%s_%s_%s_%s' % (language, platform, config,
  125. iomgr_platform)
  126. runtests_args = [
  127. '-l', language, '-c', config, '--iomgr_platform',
  128. iomgr_platform
  129. ]
  130. if arch or compiler:
  131. name += '_%s_%s' % (arch, compiler)
  132. runtests_args += [
  133. '--arch', arch, '--compiler', compiler
  134. ]
  135. if '--build_only' in extra_args:
  136. name += '_buildonly'
  137. for extra_env in extra_envs:
  138. name += '_%s_%s' % (extra_env, extra_envs[extra_env])
  139. runtests_args += extra_args
  140. if platform == 'linux':
  141. job = _docker_jobspec(name=name,
  142. runtests_args=runtests_args,
  143. runtests_envs=extra_envs,
  144. inner_jobs=inner_jobs,
  145. timeout_seconds=timeout_seconds)
  146. else:
  147. job = _workspace_jobspec(
  148. name=name,
  149. runtests_args=runtests_args,
  150. runtests_envs=extra_envs,
  151. inner_jobs=inner_jobs,
  152. timeout_seconds=timeout_seconds)
  153. job.labels = [platform, config, language, iomgr_platform
  154. ] + labels
  155. result.append(job)
  156. return result
  157. def _create_test_jobs(extra_args=[], inner_jobs=_DEFAULT_INNER_JOBS):
  158. test_jobs = []
  159. # sanity tests
  160. test_jobs += _generate_jobs(languages=['sanity'],
  161. configs=['dbg'],
  162. platforms=['linux'],
  163. labels=['basictests'],
  164. extra_args=extra_args +
  165. ['--report_multi_target'],
  166. inner_jobs=inner_jobs)
  167. # supported on all platforms.
  168. test_jobs += _generate_jobs(
  169. languages=['c'],
  170. configs=['dbg', 'opt'],
  171. platforms=['linux', 'macos', 'windows'],
  172. labels=['basictests', 'corelang'],
  173. extra_args=
  174. extra_args, # don't use multi_target report because C has too many test cases
  175. inner_jobs=inner_jobs,
  176. timeout_seconds=_CPP_RUNTESTS_TIMEOUT)
  177. # C# tests (both on .NET desktop/mono and .NET core)
  178. test_jobs += _generate_jobs(languages=['csharp'],
  179. configs=['dbg', 'opt'],
  180. platforms=['linux', 'macos', 'windows'],
  181. labels=['basictests', 'multilang'],
  182. extra_args=extra_args +
  183. ['--report_multi_target'],
  184. inner_jobs=inner_jobs)
  185. test_jobs += _generate_jobs(languages=['python'],
  186. configs=['opt'],
  187. platforms=['linux', 'macos', 'windows'],
  188. iomgr_platforms=['native'],
  189. labels=['basictests', 'multilang'],
  190. extra_args=extra_args +
  191. ['--report_multi_target'],
  192. inner_jobs=inner_jobs)
  193. # supported on linux and mac.
  194. test_jobs += _generate_jobs(
  195. languages=['c++'],
  196. configs=['dbg', 'opt'],
  197. platforms=['linux', 'macos'],
  198. labels=['basictests', 'corelang'],
  199. extra_args=
  200. extra_args, # don't use multi_target report because C++ has too many test cases
  201. inner_jobs=inner_jobs,
  202. timeout_seconds=_CPP_RUNTESTS_TIMEOUT)
  203. test_jobs += _generate_jobs(languages=['ruby', 'php7'],
  204. configs=['dbg', 'opt'],
  205. platforms=['linux', 'macos'],
  206. labels=['basictests', 'multilang'],
  207. extra_args=extra_args +
  208. ['--report_multi_target'],
  209. inner_jobs=inner_jobs)
  210. # supported on mac only.
  211. test_jobs += _generate_jobs(languages=['objc'],
  212. configs=['opt'],
  213. platforms=['macos'],
  214. labels=['basictests', 'multilang'],
  215. extra_args=extra_args +
  216. ['--report_multi_target'],
  217. inner_jobs=inner_jobs,
  218. timeout_seconds=_OBJC_RUNTESTS_TIMEOUT)
  219. return test_jobs
  220. def _create_portability_test_jobs(extra_args=[],
  221. inner_jobs=_DEFAULT_INNER_JOBS):
  222. test_jobs = []
  223. # portability C x86
  224. test_jobs += _generate_jobs(languages=['c'],
  225. configs=['dbg'],
  226. platforms=['linux'],
  227. arch='x86',
  228. compiler='default',
  229. labels=['portability', 'corelang'],
  230. extra_args=extra_args,
  231. inner_jobs=inner_jobs)
  232. # portability C and C++ on x64
  233. for compiler in [
  234. 'gcc5', 'gcc10.2_openssl102', 'gcc11', 'gcc_musl', 'clang4',
  235. 'clang13'
  236. ]:
  237. test_jobs += _generate_jobs(languages=['c', 'c++'],
  238. configs=['dbg'],
  239. platforms=['linux'],
  240. arch='x64',
  241. compiler=compiler,
  242. labels=['portability', 'corelang'],
  243. extra_args=extra_args,
  244. inner_jobs=inner_jobs,
  245. timeout_seconds=_CPP_RUNTESTS_TIMEOUT)
  246. # portability C on Windows 64-bit (x86 is the default)
  247. test_jobs += _generate_jobs(languages=['c'],
  248. configs=['dbg'],
  249. platforms=['windows'],
  250. arch='x64',
  251. compiler='default',
  252. labels=['portability', 'corelang'],
  253. extra_args=extra_args,
  254. inner_jobs=inner_jobs)
  255. # portability C on Windows with the "Visual Studio" cmake
  256. # generator, i.e. not using Ninja (to verify that we can still build with msbuild)
  257. test_jobs += _generate_jobs(languages=['c'],
  258. configs=['dbg'],
  259. platforms=['windows'],
  260. arch='default',
  261. compiler='cmake_vs2015',
  262. labels=['portability', 'corelang'],
  263. extra_args=extra_args,
  264. inner_jobs=inner_jobs)
  265. # portability C++ on Windows
  266. # TODO(jtattermusch): some of the tests are failing, so we force --build_only
  267. test_jobs += _generate_jobs(languages=['c++'],
  268. configs=['dbg'],
  269. platforms=['windows'],
  270. arch='default',
  271. compiler='default',
  272. labels=['portability', 'corelang'],
  273. extra_args=extra_args + ['--build_only'],
  274. inner_jobs=inner_jobs,
  275. timeout_seconds=_CPP_RUNTESTS_TIMEOUT)
  276. # portability C and C++ on Windows using VS2017 (build only)
  277. # TODO(jtattermusch): some of the tests are failing, so we force --build_only
  278. test_jobs += _generate_jobs(languages=['c', 'c++'],
  279. configs=['dbg'],
  280. platforms=['windows'],
  281. arch='x64',
  282. compiler='cmake_ninja_vs2017',
  283. labels=['portability', 'corelang'],
  284. extra_args=extra_args + ['--build_only'],
  285. inner_jobs=inner_jobs,
  286. timeout_seconds=_CPP_RUNTESTS_TIMEOUT)
  287. # C and C++ with no-exceptions on Linux
  288. test_jobs += _generate_jobs(languages=['c', 'c++'],
  289. configs=['noexcept'],
  290. platforms=['linux'],
  291. labels=['portability', 'corelang'],
  292. extra_args=extra_args,
  293. inner_jobs=inner_jobs,
  294. timeout_seconds=_CPP_RUNTESTS_TIMEOUT)
  295. test_jobs += _generate_jobs(languages=['python'],
  296. configs=['dbg'],
  297. platforms=['linux'],
  298. arch='default',
  299. compiler='python_alpine',
  300. labels=['portability', 'multilang'],
  301. extra_args=extra_args +
  302. ['--report_multi_target'],
  303. inner_jobs=inner_jobs)
  304. return test_jobs
  305. def _allowed_labels():
  306. """Returns a list of existing job labels."""
  307. all_labels = set()
  308. for job in _create_test_jobs() + _create_portability_test_jobs():
  309. for label in job.labels:
  310. all_labels.add(label)
  311. return sorted(all_labels)
  312. def _runs_per_test_type(arg_str):
  313. """Auxiliary function to parse the "runs_per_test" flag."""
  314. try:
  315. n = int(arg_str)
  316. if n <= 0:
  317. raise ValueError
  318. return n
  319. except:
  320. msg = '\'{}\' is not a positive integer'.format(arg_str)
  321. raise argparse.ArgumentTypeError(msg)
  322. if __name__ == "__main__":
  323. argp = argparse.ArgumentParser(
  324. description='Run a matrix of run_tests.py tests.')
  325. argp.add_argument('-j',
  326. '--jobs',
  327. default=multiprocessing.cpu_count() / _DEFAULT_INNER_JOBS,
  328. type=int,
  329. help='Number of concurrent run_tests.py instances.')
  330. argp.add_argument('-f',
  331. '--filter',
  332. choices=_allowed_labels(),
  333. nargs='+',
  334. default=[],
  335. help='Filter targets to run by label with AND semantics.')
  336. argp.add_argument('--exclude',
  337. choices=_allowed_labels(),
  338. nargs='+',
  339. default=[],
  340. help='Exclude targets with any of given labels.')
  341. argp.add_argument('--build_only',
  342. default=False,
  343. action='store_const',
  344. const=True,
  345. help='Pass --build_only flag to run_tests.py instances.')
  346. argp.add_argument(
  347. '--force_default_poller',
  348. default=False,
  349. action='store_const',
  350. const=True,
  351. help='Pass --force_default_poller to run_tests.py instances.')
  352. argp.add_argument('--dry_run',
  353. default=False,
  354. action='store_const',
  355. const=True,
  356. help='Only print what would be run.')
  357. argp.add_argument(
  358. '--filter_pr_tests',
  359. default=False,
  360. action='store_const',
  361. const=True,
  362. help='Filters out tests irrelevant to pull request changes.')
  363. argp.add_argument(
  364. '--base_branch',
  365. default='origin/master',
  366. type=str,
  367. help='Branch that pull request is requesting to merge into')
  368. argp.add_argument('--inner_jobs',
  369. default=_DEFAULT_INNER_JOBS,
  370. type=int,
  371. help='Number of jobs in each run_tests.py instance')
  372. argp.add_argument(
  373. '-n',
  374. '--runs_per_test',
  375. default=1,
  376. type=_runs_per_test_type,
  377. help='How many times to run each tests. >1 runs implies ' +
  378. 'omitting passing test from the output & reports.')
  379. argp.add_argument('--max_time',
  380. default=-1,
  381. type=int,
  382. help='Maximum amount of time to run tests for' +
  383. '(other tests will be skipped)')
  384. argp.add_argument(
  385. '--internal_ci',
  386. default=False,
  387. action='store_const',
  388. const=True,
  389. help=
  390. '(Deprecated, has no effect) Put reports into subdirectories to improve presentation of '
  391. 'results by Kokoro.')
  392. argp.add_argument('--bq_result_table',
  393. default='',
  394. type=str,
  395. nargs='?',
  396. help='Upload test results to a specified BQ table.')
  397. argp.add_argument('--extra_args',
  398. default='',
  399. type=str,
  400. nargs=argparse.REMAINDER,
  401. help='Extra test args passed to each sub-script.')
  402. args = argp.parse_args()
  403. extra_args = []
  404. if args.build_only:
  405. extra_args.append('--build_only')
  406. if args.force_default_poller:
  407. extra_args.append('--force_default_poller')
  408. if args.runs_per_test > 1:
  409. extra_args.append('-n')
  410. extra_args.append('%s' % args.runs_per_test)
  411. extra_args.append('--quiet_success')
  412. if args.max_time > 0:
  413. extra_args.extend(('--max_time', '%d' % args.max_time))
  414. if args.bq_result_table:
  415. extra_args.append('--bq_result_table')
  416. extra_args.append('%s' % args.bq_result_table)
  417. extra_args.append('--measure_cpu_costs')
  418. if args.extra_args:
  419. extra_args.extend(args.extra_args)
  420. all_jobs = _create_test_jobs(extra_args=extra_args, inner_jobs=args.inner_jobs) + \
  421. _create_portability_test_jobs(extra_args=extra_args, inner_jobs=args.inner_jobs)
  422. jobs = []
  423. for job in all_jobs:
  424. if not args.filter or all(
  425. filter in job.labels for filter in args.filter):
  426. if not any(exclude_label in job.labels
  427. for exclude_label in args.exclude):
  428. jobs.append(job)
  429. if not jobs:
  430. jobset.message('FAILED',
  431. 'No test suites match given criteria.',
  432. do_newline=True)
  433. sys.exit(1)
  434. print('IMPORTANT: The changes you are testing need to be locally committed')
  435. print('because only the committed changes in the current branch will be')
  436. print('copied to the docker environment or into subworkspaces.')
  437. skipped_jobs = []
  438. if args.filter_pr_tests:
  439. print('Looking for irrelevant tests to skip...')
  440. relevant_jobs = filter_tests(jobs, args.base_branch)
  441. if len(relevant_jobs) == len(jobs):
  442. print('No tests will be skipped.')
  443. else:
  444. print('These tests will be skipped:')
  445. skipped_jobs = list(set(jobs) - set(relevant_jobs))
  446. # Sort by shortnames to make printing of skipped tests consistent
  447. skipped_jobs.sort(key=lambda job: job.shortname)
  448. for job in list(skipped_jobs):
  449. print(' %s' % job.shortname)
  450. jobs = relevant_jobs
  451. print('Will run these tests:')
  452. for job in jobs:
  453. print(' %s: "%s"' % (job.shortname, ' '.join(job.cmdline)))
  454. print('')
  455. if args.dry_run:
  456. print('--dry_run was used, exiting')
  457. sys.exit(1)
  458. jobset.message('START', 'Running test matrix.', do_newline=True)
  459. num_failures, resultset = jobset.run(jobs,
  460. newline_on_success=True,
  461. travis=True,
  462. maxjobs=args.jobs)
  463. # Merge skipped tests into results to show skipped tests on report.xml
  464. if skipped_jobs:
  465. ignored_num_skipped_failures, skipped_results = jobset.run(
  466. skipped_jobs, skip_jobs=True)
  467. resultset.update(skipped_results)
  468. report_utils.render_junit_xml_report(resultset,
  469. _report_filename(_MATRIX_REPORT_NAME),
  470. suite_name=_MATRIX_REPORT_NAME,
  471. multi_target=True)
  472. if num_failures == 0:
  473. jobset.message('SUCCESS',
  474. 'All run_tests.py instances finished successfully.',
  475. do_newline=True)
  476. else:
  477. jobset.message('FAILED',
  478. 'Some run_tests.py instances have failed.',
  479. do_newline=True)
  480. sys.exit(1)