Imported Upstream version 1.27.0
[platform/upstream/grpc.git] / tools / interop_matrix / run_interop_matrix_tests.py
1 #!/usr/bin/env python2.7
2 # Copyright 2017 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 tests using docker images in Google Container Registry per matrix."""
16
17 from __future__ import print_function
18
19 import argparse
20 import atexit
21 import json
22 import multiprocessing
23 import os
24 import re
25 import subprocess
26 import sys
27 import uuid
28
29 # Language Runtime Matrix
30 import client_matrix
31
32 python_util_dir = os.path.abspath(
33     os.path.join(os.path.dirname(__file__), '../run_tests/python_utils'))
34 sys.path.append(python_util_dir)
35 import dockerjob
36 import jobset
37 import report_utils
38 import upload_test_results
39
40 _TEST_TIMEOUT_SECONDS = 60
41 _PULL_IMAGE_TIMEOUT_SECONDS = 15 * 60
42 _MAX_PARALLEL_DOWNLOADS = 6
43 _LANGUAGES = client_matrix.LANG_RUNTIME_MATRIX.keys()
44 # All gRPC release tags, flattened, deduped and sorted.
45 _RELEASES = sorted(
46     list(
47         set(release
48             for release_dict in client_matrix.LANG_RELEASE_MATRIX.values()
49             for release in release_dict.keys())))
50
51 argp = argparse.ArgumentParser(description='Run interop tests.')
52 argp.add_argument('-j', '--jobs', default=multiprocessing.cpu_count(), type=int)
53 argp.add_argument('--gcr_path',
54                   default='gcr.io/grpc-testing',
55                   help='Path of docker images in Google Container Registry')
56 argp.add_argument('--release',
57                   default='all',
58                   choices=['all'] + _RELEASES,
59                   help='Release tags to test.  When testing all '
60                   'releases defined in client_matrix.py, use "all".')
61 argp.add_argument('-l',
62                   '--language',
63                   choices=['all'] + sorted(_LANGUAGES),
64                   nargs='+',
65                   default=['all'],
66                   help='Languages to test')
67 argp.add_argument(
68     '--keep',
69     action='store_true',
70     help='keep the created local images after finishing the tests.')
71 argp.add_argument('--report_file',
72                   default='report.xml',
73                   help='The result file to create.')
74 argp.add_argument('--allow_flakes',
75                   default=False,
76                   action='store_const',
77                   const=True,
78                   help=('Allow flaky tests to show as passing (re-runs failed '
79                         'tests up to five times)'))
80 argp.add_argument('--bq_result_table',
81                   default='',
82                   type=str,
83                   nargs='?',
84                   help='Upload test results to a specified BQ table.')
85 # Requests will be routed through specified VIP by default.
86 # See go/grpc-interop-tests (internal-only) for details.
87 argp.add_argument('--server_host',
88                   default='74.125.206.210',
89                   type=str,
90                   nargs='?',
91                   help='The gateway to backend services.')
92
93
94 def _get_test_images_for_lang(lang, release_arg, image_path_prefix):
95     """Find docker images for a language across releases and runtimes.
96
97   Returns dictionary of list of (<tag>, <image-full-path>) keyed by runtime.
98   """
99     if release_arg == 'all':
100         # Use all defined releases for given language
101         releases = client_matrix.get_release_tags(lang)
102     else:
103         # Look for a particular release.
104         if release_arg not in client_matrix.get_release_tags(lang):
105             jobset.message('SKIPPED',
106                            'release %s for %s is not defined' %
107                            (release_arg, lang),
108                            do_newline=True)
109             return {}
110         releases = [release_arg]
111
112     # Image tuples keyed by runtime.
113     images = {}
114     for tag in releases:
115         for runtime in client_matrix.get_runtimes_for_lang_release(lang, tag):
116             image_name = '%s/grpc_interop_%s:%s' % (image_path_prefix, runtime,
117                                                     tag)
118             image_tuple = (tag, image_name)
119
120             if not images.has_key(runtime):
121                 images[runtime] = []
122             images[runtime].append(image_tuple)
123     return images
124
125
126 def _read_test_cases_file(lang, runtime, release):
127     """Read test cases from a bash-like file and return a list of commands"""
128     # Check to see if we need to use a particular version of test cases.
129     release_info = client_matrix.LANG_RELEASE_MATRIX[lang].get(release)
130     if release_info:
131         testcases_file = release_info.testcases_file
132     if not testcases_file:
133         # TODO(jtattermusch): remove the double-underscore, it is pointless
134         testcases_file = '%s__master' % lang
135
136     # For csharp, the testcases file used depends on the runtime
137     # TODO(jtattermusch): remove this odd specialcase
138     if lang == 'csharp' and runtime == 'csharpcoreclr':
139         testcases_file = testcases_file.replace('csharp_', 'csharpcoreclr_')
140
141     testcases_filepath = os.path.join(os.path.dirname(__file__), 'testcases',
142                                       testcases_file)
143     lines = []
144     with open(testcases_filepath) as f:
145         for line in f.readlines():
146             line = re.sub('\\#.*$', '', line)  # remove hash comments
147             line = line.strip()
148             if line and not line.startswith('echo'):
149                 # Each non-empty line is a treated as a test case command
150                 lines.append(line)
151     return lines
152
153
154 def _cleanup_docker_image(image):
155     jobset.message('START', 'Cleanup docker image %s' % image, do_newline=True)
156     dockerjob.remove_image(image, skip_nonexistent=True)
157
158
159 args = argp.parse_args()
160
161
162 # caches test cases (list of JobSpec) loaded from file.  Keyed by lang and runtime.
163 def _generate_test_case_jobspecs(lang, runtime, release, suite_name):
164     """Returns the list of test cases from testcase files per lang/release."""
165     testcase_lines = _read_test_cases_file(lang, runtime, release)
166
167     job_spec_list = []
168     for line in testcase_lines:
169         # TODO(jtattermusch): revisit the logic for updating test case commands
170         # what it currently being done seems fragile.
171
172         # Extract test case name from the command line
173         m = re.search(r'--test_case=(\w+)', line)
174         testcase_name = m.group(1) if m else 'unknown_test'
175
176         # Extract the server name from the command line
177         if '--server_host_override=' in line:
178             m = re.search(
179                 r'--server_host_override=((.*).sandbox.googleapis.com)', line)
180         else:
181             m = re.search(r'--server_host=((.*).sandbox.googleapis.com)', line)
182         server = m.group(1) if m else 'unknown_server'
183         server_short = m.group(2) if m else 'unknown_server'
184
185         # replace original server_host argument
186         assert '--server_host=' in line
187         line = re.sub(r'--server_host=[^ ]*',
188                       r'--server_host=%s' % args.server_host, line)
189
190         # some interop tests don't set server_host_override (see #17407),
191         # but we need to use it if different host is set via cmdline args.
192         if args.server_host != server and not '--server_host_override=' in line:
193             line = re.sub(r'(--server_host=[^ ]*)',
194                           r'\1 --server_host_override=%s' % server, line)
195
196         spec = jobset.JobSpec(cmdline=line,
197                               shortname='%s:%s:%s:%s' %
198                               (suite_name, lang, server_short, testcase_name),
199                               timeout_seconds=_TEST_TIMEOUT_SECONDS,
200                               shell=True,
201                               flake_retries=5 if args.allow_flakes else 0)
202         job_spec_list.append(spec)
203     return job_spec_list
204
205
206 def _pull_images_for_lang(lang, images):
207     """Pull all images for given lang from container registry."""
208     jobset.message('START',
209                    'Downloading images for language "%s"' % lang,
210                    do_newline=True)
211     download_specs = []
212     for release, image in images:
213         # Pull the image and warm it up.
214         # First time we use an image with "docker run", it takes time to unpack
215         # the image and later this delay would fail our test cases.
216         cmdline = [
217             'time gcloud docker -- pull %s && time docker run --rm=true %s /bin/true'
218             % (image, image)
219         ]
220         spec = jobset.JobSpec(cmdline=cmdline,
221                               shortname='pull_image_%s' % (image),
222                               timeout_seconds=_PULL_IMAGE_TIMEOUT_SECONDS,
223                               shell=True,
224                               flake_retries=2)
225         download_specs.append(spec)
226     # too many image downloads at once tend to get stuck
227     max_pull_jobs = min(args.jobs, _MAX_PARALLEL_DOWNLOADS)
228     num_failures, resultset = jobset.run(download_specs,
229                                          newline_on_success=True,
230                                          maxjobs=max_pull_jobs)
231     if num_failures:
232         jobset.message('FAILED',
233                        'Failed to download some images',
234                        do_newline=True)
235         return False
236     else:
237         jobset.message('SUCCESS',
238                        'All images downloaded successfully.',
239                        do_newline=True)
240         return True
241
242
243 def _run_tests_for_lang(lang, runtime, images, xml_report_tree):
244     """Find and run all test cases for a language.
245
246   images is a list of (<release-tag>, <image-full-path>) tuple.
247   """
248     skip_tests = False
249     if not _pull_images_for_lang(lang, images):
250         jobset.message(
251             'FAILED',
252             'Image download failed. Skipping tests for language "%s"' % lang,
253             do_newline=True)
254         skip_tests = True
255
256     total_num_failures = 0
257     for release, image in images:
258         suite_name = '%s__%s_%s' % (lang, runtime, release)
259         job_spec_list = _generate_test_case_jobspecs(lang, runtime, release,
260                                                      suite_name)
261
262         if not job_spec_list:
263             jobset.message('FAILED',
264                            'No test cases were found.',
265                            do_newline=True)
266             total_num_failures += 1
267             continue
268
269         num_failures, resultset = jobset.run(job_spec_list,
270                                              newline_on_success=True,
271                                              add_env={'docker_image': image},
272                                              maxjobs=args.jobs,
273                                              skip_jobs=skip_tests)
274         if args.bq_result_table and resultset:
275             upload_test_results.upload_interop_results_to_bq(
276                 resultset, args.bq_result_table)
277         if skip_tests:
278             jobset.message('FAILED', 'Tests were skipped', do_newline=True)
279             total_num_failures += 1
280         elif num_failures:
281             jobset.message('FAILED', 'Some tests failed', do_newline=True)
282             total_num_failures += num_failures
283         else:
284             jobset.message('SUCCESS', 'All tests passed', do_newline=True)
285
286         report_utils.append_junit_xml_results(xml_report_tree, resultset,
287                                               'grpc_interop_matrix', suite_name,
288                                               str(uuid.uuid4()))
289
290     # cleanup all downloaded docker images
291     for _, image in images:
292         if not args.keep:
293             _cleanup_docker_image(image)
294
295     return total_num_failures
296
297
298 languages = args.language if args.language != ['all'] else _LANGUAGES
299 total_num_failures = 0
300 _xml_report_tree = report_utils.new_junit_xml_tree()
301 for lang in languages:
302     docker_images = _get_test_images_for_lang(lang, args.release, args.gcr_path)
303     for runtime in sorted(docker_images.keys()):
304         total_num_failures += _run_tests_for_lang(lang, runtime,
305                                                   docker_images[runtime],
306                                                   _xml_report_tree)
307
308 report_utils.create_xml_report_file(_xml_report_tree, args.report_file)
309
310 if total_num_failures:
311     sys.exit(1)
312 sys.exit(0)