| #!/usr/bin/env python2.7 |
| # Copyright 2015-2016, Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """Run tests in parallel.""" |
| |
| import argparse |
| import ast |
| import glob |
| import hashlib |
| import itertools |
| import json |
| import multiprocessing |
| import os |
| import platform |
| import random |
| import re |
| import socket |
| import subprocess |
| import sys |
| import tempfile |
| import traceback |
| import time |
| import urllib2 |
| import uuid |
| |
| import jobset |
| import report_utils |
| import watch_dirs |
| |
| |
| ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..')) |
| os.chdir(ROOT) |
| |
| |
| _FORCE_ENVIRON_FOR_WRAPPERS = {} |
| |
| |
| def platform_string(): |
| return jobset.platform_string() |
| |
| |
| # SimpleConfig: just compile with CONFIG=config, and run the binary to test |
| class Config(object): |
| |
| def __init__(self, config, environ=None, timeout_multiplier=1, tool_prefix=[]): |
| if environ is None: |
| environ = {} |
| self.build_config = config |
| self.allow_hashing = (config != 'gcov') |
| self.environ = environ |
| self.environ['CONFIG'] = config |
| self.tool_prefix = tool_prefix |
| self.timeout_multiplier = timeout_multiplier |
| |
| def job_spec(self, cmdline, hash_targets, timeout_seconds=5*60, |
| shortname=None, environ={}, cpu_cost=1.0): |
| """Construct a jobset.JobSpec for a test under this config |
| |
| Args: |
| cmdline: a list of strings specifying the command line the test |
| would like to run |
| hash_targets: either None (don't do caching of test results), or |
| a list of strings specifying files to include in a |
| binary hash to check if a test has changed |
| -- if used, all artifacts needed to run the test must |
| be listed |
| """ |
| actual_environ = self.environ.copy() |
| for k, v in environ.iteritems(): |
| actual_environ[k] = v |
| return jobset.JobSpec(cmdline=self.tool_prefix + cmdline, |
| shortname=shortname, |
| environ=actual_environ, |
| cpu_cost=cpu_cost, |
| timeout_seconds=(self.timeout_multiplier * timeout_seconds if timeout_seconds else None), |
| hash_targets=hash_targets |
| if self.allow_hashing else None, |
| flake_retries=5 if args.allow_flakes else 0, |
| timeout_retries=3 if args.allow_flakes else 0) |
| |
| |
| def get_c_tests(travis, test_lang) : |
| out = [] |
| platforms_str = 'ci_platforms' if travis else 'platforms' |
| with open('tools/run_tests/tests.json') as f: |
| js = json.load(f) |
| return [tgt |
| for tgt in js |
| if tgt['language'] == test_lang and |
| platform_string() in tgt[platforms_str] and |
| not (travis and tgt['flaky'])] |
| |
| |
| class CLanguage(object): |
| |
| def __init__(self, make_target, test_lang): |
| self.make_target = make_target |
| self.platform = platform_string() |
| self.test_lang = test_lang |
| |
| def test_specs(self, config, args): |
| out = [] |
| binaries = get_c_tests(args.travis, self.test_lang) |
| for target in binaries: |
| if config.build_config in target['exclude_configs']: |
| continue |
| if self.platform == 'windows': |
| binary = 'vsprojects/%s%s/%s.exe' % ( |
| 'x64/' if args.arch == 'x64' else '', |
| _WINDOWS_CONFIG[config.build_config], |
| target['name']) |
| else: |
| binary = 'bins/%s/%s' % (config.build_config, target['name']) |
| if os.path.isfile(binary): |
| cmdline = [binary] + target['args'] |
| out.append(config.job_spec(cmdline, [binary], |
| shortname=' '.join(cmdline), |
| cpu_cost=target['cpu_cost'], |
| environ={'GRPC_DEFAULT_SSL_ROOTS_FILE_PATH': |
| os.path.abspath(os.path.dirname( |
| sys.argv[0]) + '/../../src/core/tsi/test_creds/ca.pem')})) |
| elif args.regex == '.*' or platform_string() == 'windows': |
| print '\nWARNING: binary not found, skipping', binary |
| return sorted(out) |
| |
| def make_targets(self, test_regex): |
| if platform_string() != 'windows' and test_regex != '.*': |
| # use the regex to minimize the number of things to build |
| return [os.path.basename(target['name']) |
| for target in get_c_tests(False, self.test_lang) |
| if re.search(test_regex, '/' + target['name'])] |
| if platform_string() == 'windows': |
| # don't build tools on windows just yet |
| return ['buildtests_%s' % self.make_target] |
| return ['buildtests_%s' % self.make_target, 'tools_%s' % self.make_target] |
| |
| def make_options(self): |
| return [] |
| |
| def pre_build_steps(self): |
| if self.platform == 'windows': |
| return [['tools\\run_tests\\pre_build_c.bat']] |
| else: |
| return [] |
| |
| def build_steps(self): |
| return [] |
| |
| def post_tests_steps(self): |
| if self.platform == 'windows': |
| return [] |
| else: |
| return [['tools/run_tests/post_tests_c.sh']] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return True |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return self.make_target |
| |
| |
| class NodeLanguage(object): |
| |
| def test_specs(self, config, args): |
| return [config.job_spec(['tools/run_tests/run_node.sh'], None, |
| environ=_FORCE_ENVIRON_FOR_WRAPPERS)] |
| |
| def pre_build_steps(self): |
| # Default to 1 week cache expiration |
| return [['tools/run_tests/pre_build_node.sh']] |
| |
| def make_targets(self, test_regex): |
| return [] |
| |
| def make_options(self): |
| return [] |
| |
| def build_steps(self): |
| return [['tools/run_tests/build_node.sh']] |
| |
| def post_tests_steps(self): |
| return [] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return False |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return 'node' |
| |
| |
| class PhpLanguage(object): |
| |
| def test_specs(self, config, args): |
| return [config.job_spec(['src/php/bin/run_tests.sh'], None, |
| environ=_FORCE_ENVIRON_FOR_WRAPPERS)] |
| |
| def pre_build_steps(self): |
| return [] |
| |
| def make_targets(self, test_regex): |
| return ['static_c', 'shared_c'] |
| |
| def make_options(self): |
| return [] |
| |
| def build_steps(self): |
| return [['tools/run_tests/build_php.sh']] |
| |
| def post_tests_steps(self): |
| return [['tools/run_tests/post_tests_php.sh']] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return False |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return 'php' |
| |
| |
| class PythonLanguage(object): |
| |
| def __init__(self): |
| self._build_python_versions = ['2.7'] |
| self._has_python_versions = [] |
| |
| def test_specs(self, config, args): |
| environment = dict(_FORCE_ENVIRON_FOR_WRAPPERS) |
| environment['PYVER'] = '2.7' |
| return [config.job_spec( |
| ['tools/run_tests/run_python.sh'], |
| None, |
| environ=environment, |
| shortname='py.test', |
| timeout_seconds=15*60 |
| )] |
| |
| def pre_build_steps(self): |
| return [] |
| |
| def make_targets(self, test_regex): |
| return ['static_c', 'grpc_python_plugin', 'shared_c'] |
| |
| def make_options(self): |
| return [] |
| |
| def build_steps(self): |
| commands = [] |
| for python_version in self._build_python_versions: |
| try: |
| with open(os.devnull, 'w') as output: |
| subprocess.check_call(['which', 'python' + python_version], |
| stdout=output, stderr=output) |
| commands.append(['tools/run_tests/build_python.sh', python_version]) |
| self._has_python_versions.append(python_version) |
| except: |
| jobset.message('WARNING', 'Missing Python ' + python_version, |
| do_newline=True) |
| return commands |
| |
| def post_tests_steps(self): |
| return [] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return False |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return 'python' |
| |
| |
| class RubyLanguage(object): |
| |
| def test_specs(self, config, args): |
| return [config.job_spec(['tools/run_tests/run_ruby.sh'], None, |
| environ=_FORCE_ENVIRON_FOR_WRAPPERS)] |
| |
| def pre_build_steps(self): |
| return [['tools/run_tests/pre_build_ruby.sh']] |
| |
| def make_targets(self, test_regex): |
| return ['static_c'] |
| |
| def make_options(self): |
| return [] |
| |
| def build_steps(self): |
| return [['tools/run_tests/build_ruby.sh']] |
| |
| def post_tests_steps(self): |
| return [['tools/run_tests/post_tests_ruby.sh']] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return False |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return 'ruby' |
| |
| |
| class CSharpLanguage(object): |
| def __init__(self): |
| self.platform = platform_string() |
| |
| def test_specs(self, config, args): |
| with open('src/csharp/tests.json') as f: |
| tests_json = json.load(f) |
| assemblies = tests_json['assemblies'] |
| tests = tests_json['tests'] |
| |
| msbuild_config = _WINDOWS_CONFIG[config.build_config] |
| assembly_files = ['%s/bin/%s/%s.dll' % (a, msbuild_config, a) |
| for a in assemblies] |
| |
| extra_args = ['-labels'] + assembly_files |
| |
| if self.platform == 'windows': |
| script_name = 'tools\\run_tests\\run_csharp.bat' |
| extra_args += ['-domain=None'] |
| else: |
| script_name = 'tools/run_tests/run_csharp.sh' |
| |
| if config.build_config == 'gcov': |
| # On Windows, we only collect C# code coverage. |
| # On Linux, we only collect coverage for native extension. |
| # For code coverage all tests need to run as one suite. |
| return [config.job_spec([script_name] + extra_args, None, |
| shortname='csharp.coverage', |
| environ=_FORCE_ENVIRON_FOR_WRAPPERS)] |
| else: |
| specs = [] |
| for test in tests: |
| cmdline = [script_name, '-run=%s' % test] + extra_args |
| if self.platform == 'windows': |
| # use different output directory for each test to prevent |
| # TestResult.xml clash between parallel test runs. |
| cmdline += ['-work=test-result/%s' % uuid.uuid4()] |
| specs.append(config.job_spec(cmdline, None, |
| shortname='csharp.%s' % test, |
| environ=_FORCE_ENVIRON_FOR_WRAPPERS)) |
| return specs |
| |
| def pre_build_steps(self): |
| if self.platform == 'windows': |
| return [['tools\\run_tests\\pre_build_csharp.bat']] |
| else: |
| return [['tools/run_tests/pre_build_csharp.sh']] |
| |
| def make_targets(self, test_regex): |
| # For Windows, this target doesn't really build anything, |
| # everything is build by buildall script later. |
| if self.platform == 'windows': |
| return [] |
| else: |
| return ['grpc_csharp_ext'] |
| |
| def make_options(self): |
| if self.platform == 'mac': |
| # On Mac, official distribution of mono is 32bit. |
| return ['CFLAGS=-arch i386', 'LDFLAGS=-arch i386'] |
| else: |
| return [] |
| |
| def build_steps(self): |
| if self.platform == 'windows': |
| return [['src\\csharp\\buildall.bat']] |
| else: |
| return [['tools/run_tests/build_csharp.sh']] |
| |
| def post_tests_steps(self): |
| return [] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return False |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return 'csharp' |
| |
| |
| class ObjCLanguage(object): |
| |
| def test_specs(self, config, args): |
| return [config.job_spec(['src/objective-c/tests/run_tests.sh'], None, |
| environ=_FORCE_ENVIRON_FOR_WRAPPERS)] |
| |
| def pre_build_steps(self): |
| return [] |
| |
| def make_targets(self, test_regex): |
| return ['grpc_objective_c_plugin', 'interop_server'] |
| |
| def make_options(self): |
| return [] |
| |
| def build_steps(self): |
| return [['src/objective-c/tests/build_tests.sh']] |
| |
| def post_tests_steps(self): |
| return [] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return False |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return 'objc' |
| |
| |
| class Sanity(object): |
| |
| def test_specs(self, config, args): |
| import yaml |
| with open('tools/run_tests/sanity/sanity_tests.yaml', 'r') as f: |
| return [config.job_spec(cmd['script'].split(), None, |
| timeout_seconds=None, environ={'TEST': 'true'}, |
| cpu_cost=cmd.get('cpu_cost', 1)) |
| for cmd in yaml.load(f)] |
| |
| def pre_build_steps(self): |
| return [] |
| |
| def make_targets(self, test_regex): |
| return ['run_dep_checks'] |
| |
| def make_options(self): |
| return [] |
| |
| def build_steps(self): |
| return [] |
| |
| def post_tests_steps(self): |
| return [] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return False |
| |
| def dockerfile_dir(self, config, arch): |
| return 'tools/dockerfile/grpc_sanity' |
| |
| def __str__(self): |
| return 'sanity' |
| |
| |
| class Build(object): |
| |
| def test_specs(self, config, args): |
| return [] |
| |
| def pre_build_steps(self): |
| return [] |
| |
| def make_targets(self, test_regex): |
| return ['static'] |
| |
| def make_options(self): |
| return [] |
| |
| def build_steps(self): |
| return [] |
| |
| def post_tests_steps(self): |
| return [] |
| |
| def makefile_name(self): |
| return 'Makefile' |
| |
| def supports_multi_config(self): |
| return True |
| |
| def dockerfile_dir(self, config, arch): |
| return None |
| |
| def __str__(self): |
| return self.make_target |
| |
| |
| # different configurations we can run under |
| with open('tools/run_tests/configs.json') as f: |
| _CONFIGS = dict((cfg['config'], Config(**cfg)) for cfg in ast.literal_eval(f.read())) |
| |
| |
| _DEFAULT = ['opt'] |
| _LANGUAGES = { |
| 'c++': CLanguage('cxx', 'c++'), |
| 'c': CLanguage('c', 'c'), |
| 'node': NodeLanguage(), |
| 'php': PhpLanguage(), |
| 'python': PythonLanguage(), |
| 'ruby': RubyLanguage(), |
| 'csharp': CSharpLanguage(), |
| 'objc' : ObjCLanguage(), |
| 'sanity': Sanity(), |
| 'build': Build(), |
| } |
| |
| _WINDOWS_CONFIG = { |
| 'dbg': 'Debug', |
| 'opt': 'Release', |
| 'gcov': 'Debug', |
| } |
| |
| |
| def _windows_arch_option(arch): |
| """Returns msbuild cmdline option for selected architecture.""" |
| if arch == 'default' or arch == 'x86': |
| return '/p:Platform=Win32' |
| elif arch == 'x64': |
| return '/p:Platform=x64' |
| else: |
| print 'Architecture %s not supported.' % arch |
| sys.exit(1) |
| |
| |
| def _check_arch_option(arch): |
| """Checks that architecture option is valid.""" |
| if platform_string() == 'windows': |
| _windows_arch_option(arch) |
| elif platform_string() == 'linux': |
| # On linux, we need to be running under docker with the right architecture. |
| runtime_arch = platform.architecture()[0] |
| if arch == 'default': |
| return |
| elif runtime_arch == '64bit' and arch == 'x64': |
| return |
| elif runtime_arch == '32bit' and arch == 'x86': |
| return |
| else: |
| print 'Architecture %s does not match current runtime architecture.' % arch |
| sys.exit(1) |
| else: |
| if args.arch != 'default': |
| print 'Architecture %s not supported on current platform.' % args.arch |
| sys.exit(1) |
| |
| |
| def _windows_build_bat(compiler): |
| """Returns name of build.bat for selected compiler.""" |
| if compiler == 'default' or compiler == 'vs2013': |
| return 'vsprojects\\build_vs2013.bat' |
| elif compiler == 'vs2015': |
| return 'vsprojects\\build_vs2015.bat' |
| elif compiler == 'vs2010': |
| return 'vsprojects\\build_vs2010.bat' |
| else: |
| print 'Compiler %s not supported.' % compiler |
| sys.exit(1) |
| |
| |
| def _windows_toolset_option(compiler): |
| """Returns msbuild PlatformToolset for selected compiler.""" |
| if compiler == 'default' or compiler == 'vs2013': |
| return '/p:PlatformToolset=v120' |
| elif compiler == 'vs2015': |
| return '/p:PlatformToolset=v140' |
| elif compiler == 'vs2010': |
| return '/p:PlatformToolset=v100' |
| else: |
| print 'Compiler %s not supported.' % compiler |
| sys.exit(1) |
| |
| |
| def _get_dockerfile_dir(language, cfg, arch): |
| """Returns dockerfile to use""" |
| custom = language.dockerfile_dir(cfg, arch) |
| if custom: |
| return custom |
| else: |
| if arch == 'default' or arch == 'x64': |
| return 'tools/dockerfile/grpc_tests_multilang_x64' |
| elif arch == 'x86': |
| return 'tools/dockerfile/grpc_tests_multilang_x86' |
| else: |
| print 'Architecture %s not supported with current settings.' % arch |
| sys.exit(1) |
| |
| def runs_per_test_type(arg_str): |
| """Auxilary function to parse the "runs_per_test" flag. |
| |
| Returns: |
| A positive integer or 0, the latter indicating an infinite number of |
| runs. |
| |
| Raises: |
| argparse.ArgumentTypeError: Upon invalid input. |
| """ |
| if arg_str == 'inf': |
| return 0 |
| try: |
| n = int(arg_str) |
| if n <= 0: raise ValueError |
| return n |
| except: |
| msg = '\'{}\' is not a positive integer or \'inf\''.format(arg_str) |
| raise argparse.ArgumentTypeError(msg) |
| |
| # parse command line |
| argp = argparse.ArgumentParser(description='Run grpc tests.') |
| argp.add_argument('-c', '--config', |
| choices=['all'] + sorted(_CONFIGS.keys()), |
| nargs='+', |
| default=_DEFAULT) |
| argp.add_argument('-n', '--runs_per_test', default=1, type=runs_per_test_type, |
| help='A positive integer or "inf". If "inf", all tests will run in an ' |
| 'infinite loop. Especially useful in combination with "-f"') |
| argp.add_argument('-r', '--regex', default='.*', type=str) |
| argp.add_argument('-j', '--jobs', default=multiprocessing.cpu_count(), type=int) |
| argp.add_argument('-s', '--slowdown', default=1.0, type=float) |
| argp.add_argument('-f', '--forever', |
| default=False, |
| action='store_const', |
| const=True) |
| argp.add_argument('-t', '--travis', |
| default=False, |
| action='store_const', |
| const=True) |
| argp.add_argument('--newline_on_success', |
| default=False, |
| action='store_const', |
| const=True) |
| argp.add_argument('-l', '--language', |
| choices=['all'] + sorted(_LANGUAGES.keys()), |
| nargs='+', |
| default=['all']) |
| argp.add_argument('-S', '--stop_on_failure', |
| default=False, |
| action='store_const', |
| const=True) |
| argp.add_argument('--use_docker', |
| default=False, |
| action='store_const', |
| const=True, |
| help='Run all the tests under docker. That provides ' + |
| 'additional isolation and prevents the need to install ' + |
| 'language specific prerequisites. Only available on Linux.') |
| argp.add_argument('--allow_flakes', |
| default=False, |
| action='store_const', |
| const=True, |
| help='Allow flaky tests to show as passing (re-runs failed tests up to five times)') |
| argp.add_argument('--arch', |
| choices=['default', 'x86', 'x64'], |
| default='default', |
| help='Selects architecture to target. For some platforms "default" is the only supported choice.') |
| argp.add_argument('--compiler', |
| choices=['default', 'vs2010', 'vs2013', 'vs2015'], |
| default='default', |
| help='Selects compiler to use. For some platforms "default" is the only supported choice.') |
| argp.add_argument('--build_only', |
| default=False, |
| action='store_const', |
| const=True, |
| help='Perform all the build steps but dont run any tests.') |
| argp.add_argument('--measure_cpu_costs', default=False, action='store_const', const=True, |
| help='Measure the cpu costs of tests') |
| argp.add_argument('--update_submodules', default=[], nargs='*', |
| help='Update some submodules before building. If any are updated, also run generate_projects. ' + |
| 'Submodules are specified as SUBMODULE_NAME:BRANCH; if BRANCH is omitted, master is assumed.') |
| argp.add_argument('-a', '--antagonists', default=0, type=int) |
| argp.add_argument('-x', '--xml_report', default=None, type=str, |
| help='Generates a JUnit-compatible XML report') |
| args = argp.parse_args() |
| |
| jobset.measure_cpu_costs = args.measure_cpu_costs |
| |
| # update submodules if necessary |
| need_to_regenerate_projects = False |
| for spec in args.update_submodules: |
| spec = spec.split(':', 1) |
| if len(spec) == 1: |
| submodule = spec[0] |
| branch = 'master' |
| elif len(spec) == 2: |
| submodule = spec[0] |
| branch = spec[1] |
| cwd = 'third_party/%s' % submodule |
| def git(cmd, cwd=cwd): |
| print 'in %s: git %s' % (cwd, cmd) |
| subprocess.check_call('git %s' % cmd, cwd=cwd, shell=True) |
| git('fetch') |
| git('checkout %s' % branch) |
| git('pull origin %s' % branch) |
| if os.path.exists('src/%s/gen_build_yaml.py' % submodule): |
| need_to_regenerate_projects = True |
| if need_to_regenerate_projects: |
| if jobset.platform_string() == 'linux': |
| subprocess.check_call('tools/buildgen/generate_projects.sh', shell=True) |
| else: |
| print 'WARNING: may need to regenerate projects, but since we are not on' |
| print ' Linux this step is being skipped. Compilation MAY fail.' |
| |
| |
| # grab config |
| run_configs = set(_CONFIGS[cfg] |
| for cfg in itertools.chain.from_iterable( |
| _CONFIGS.iterkeys() if x == 'all' else [x] |
| for x in args.config)) |
| build_configs = set(cfg.build_config for cfg in run_configs) |
| |
| if args.travis: |
| _FORCE_ENVIRON_FOR_WRAPPERS = {'GRPC_TRACE': 'api'} |
| |
| if 'all' in args.language: |
| lang_list = _LANGUAGES.keys() |
| else: |
| lang_list = args.language |
| # We don't support code coverage on some languages |
| if 'gcov' in args.config: |
| for bad in ['objc', 'sanity', 'build']: |
| if bad in lang_list: |
| lang_list.remove(bad) |
| |
| languages = set(_LANGUAGES[l] for l in lang_list) |
| |
| if len(build_configs) > 1: |
| for language in languages: |
| if not language.supports_multi_config(): |
| print language, 'does not support multiple build configurations' |
| sys.exit(1) |
| |
| language_make_options=[] |
| if any(language.make_options() for language in languages): |
| if len(languages) != 1: |
| print 'languages with custom make options cannot be built simultaneously with other languages' |
| sys.exit(1) |
| else: |
| language_make_options = next(iter(languages)).make_options() |
| |
| if len(languages) != 1 or len(build_configs) != 1: |
| print 'Multi-language and multi-config testing is not supported.' |
| sys.exit(1) |
| |
| if args.use_docker: |
| if not args.travis: |
| print 'Seen --use_docker flag, will run tests under docker.' |
| print |
| print 'IMPORTANT: The changes you are testing need to be locally committed' |
| print 'because only the committed changes in the current branch will be' |
| print 'copied to the docker environment.' |
| time.sleep(5) |
| |
| child_argv = [ arg for arg in sys.argv if not arg == '--use_docker' ] |
| run_tests_cmd = 'python tools/run_tests/run_tests.py %s' % ' '.join(child_argv[1:]) |
| |
| env = os.environ.copy() |
| env['RUN_TESTS_COMMAND'] = run_tests_cmd |
| env['DOCKERFILE_DIR'] = _get_dockerfile_dir(next(iter(languages)), |
| next(iter(build_configs)), |
| args.arch) |
| env['DOCKER_RUN_SCRIPT'] = 'tools/jenkins/docker_run_tests.sh' |
| if args.xml_report: |
| env['XML_REPORT'] = args.xml_report |
| if not args.travis: |
| env['TTY_FLAG'] = '-t' # enables Ctrl-C when not on Jenkins. |
| |
| subprocess.check_call(['tools/jenkins/build_docker_and_run_tests.sh'], |
| shell=True, |
| env=env) |
| sys.exit(0) |
| |
| if platform_string() != 'windows' and args.compiler != 'default': |
| print 'Compiler %s not supported on current platform.' % args.compiler |
| sys.exit(1) |
| |
| _check_arch_option(args.arch) |
| |
| def make_jobspec(cfg, targets, makefile='Makefile'): |
| if platform_string() == 'windows': |
| extra_args = [] |
| # better do parallel compilation |
| # empirically /m:2 gives the best performance/price and should prevent |
| # overloading the windows workers. |
| extra_args.extend(['/m:2']) |
| # disable PDB generation: it's broken, and we don't need it during CI |
| extra_args.extend(['/p:Jenkins=true']) |
| return [ |
| jobset.JobSpec([_windows_build_bat(args.compiler), |
| 'vsprojects\\%s.sln' % target, |
| '/p:Configuration=%s' % _WINDOWS_CONFIG[cfg], |
| _windows_toolset_option(args.compiler), |
| _windows_arch_option(args.arch)] + |
| extra_args + |
| language_make_options, |
| shell=True, timeout_seconds=None) |
| for target in targets] |
| else: |
| if targets: |
| return [jobset.JobSpec([os.getenv('MAKE', 'make'), |
| '-f', makefile, |
| '-j', '%d' % args.jobs, |
| 'EXTRA_DEFINES=GRPC_TEST_SLOWDOWN_MACHINE_FACTOR=%f' % args.slowdown, |
| 'CONFIG=%s' % cfg] + |
| language_make_options + |
| ([] if not args.travis else ['JENKINS_BUILD=1']) + |
| targets, |
| timeout_seconds=None)] |
| else: |
| return [] |
| |
| make_targets = {} |
| for l in languages: |
| makefile = l.makefile_name() |
| make_targets[makefile] = make_targets.get(makefile, set()).union( |
| set(l.make_targets(args.regex))) |
| |
| def build_step_environ(cfg): |
| environ = {'CONFIG': cfg} |
| msbuild_cfg = _WINDOWS_CONFIG.get(cfg) |
| if msbuild_cfg: |
| environ['MSBUILD_CONFIG'] = msbuild_cfg |
| return environ |
| |
| build_steps = list(set( |
| jobset.JobSpec(cmdline, environ=build_step_environ(cfg), flake_retries=5) |
| for cfg in build_configs |
| for l in languages |
| for cmdline in l.pre_build_steps())) |
| if make_targets: |
| make_commands = itertools.chain.from_iterable(make_jobspec(cfg, list(targets), makefile) for cfg in build_configs for (makefile, targets) in make_targets.iteritems()) |
| build_steps.extend(set(make_commands)) |
| build_steps.extend(set( |
| jobset.JobSpec(cmdline, environ=build_step_environ(cfg), timeout_seconds=None) |
| for cfg in build_configs |
| for l in languages |
| for cmdline in l.build_steps())) |
| |
| post_tests_steps = list(set( |
| jobset.JobSpec(cmdline, environ=build_step_environ(cfg)) |
| for cfg in build_configs |
| for l in languages |
| for cmdline in l.post_tests_steps())) |
| runs_per_test = args.runs_per_test |
| forever = args.forever |
| |
| |
| class TestCache(object): |
| """Cache for running tests.""" |
| |
| def __init__(self, use_cache_results): |
| self._last_successful_run = {} |
| self._use_cache_results = use_cache_results |
| self._last_save = time.time() |
| |
| def should_run(self, cmdline, bin_hash): |
| if cmdline not in self._last_successful_run: |
| return True |
| if self._last_successful_run[cmdline] != bin_hash: |
| return True |
| if not self._use_cache_results: |
| return True |
| return False |
| |
| def finished(self, cmdline, bin_hash): |
| self._last_successful_run[cmdline] = bin_hash |
| if time.time() - self._last_save > 1: |
| self.save() |
| |
| def dump(self): |
| return [{'cmdline': k, 'hash': v} |
| for k, v in self._last_successful_run.iteritems()] |
| |
| def parse(self, exdump): |
| self._last_successful_run = dict((o['cmdline'], o['hash']) for o in exdump) |
| |
| def save(self): |
| with open('.run_tests_cache', 'w') as f: |
| f.write(json.dumps(self.dump())) |
| self._last_save = time.time() |
| |
| def maybe_load(self): |
| if os.path.exists('.run_tests_cache'): |
| with open('.run_tests_cache') as f: |
| self.parse(json.loads(f.read())) |
| |
| |
| def _start_port_server(port_server_port): |
| # check if a compatible port server is running |
| # if incompatible (version mismatch) ==> start a new one |
| # if not running ==> start a new one |
| # otherwise, leave it up |
| try: |
| version = int(urllib2.urlopen( |
| 'http://localhost:%d/version_number' % port_server_port, |
| timeout=1).read()) |
| print 'detected port server running version %d' % version |
| running = True |
| except Exception as e: |
| print 'failed to detect port server: %s' % sys.exc_info()[0] |
| print e.strerror |
| running = False |
| if running: |
| current_version = int(subprocess.check_output( |
| [sys.executable, os.path.abspath('tools/run_tests/port_server.py'), |
| 'dump_version'])) |
| print 'my port server is version %d' % current_version |
| running = (version >= current_version) |
| if not running: |
| print 'port_server version mismatch: killing the old one' |
| urllib2.urlopen('http://localhost:%d/quitquitquit' % port_server_port).read() |
| time.sleep(1) |
| if not running: |
| fd, logfile = tempfile.mkstemp() |
| os.close(fd) |
| print 'starting port_server, with log file %s' % logfile |
| args = [sys.executable, os.path.abspath('tools/run_tests/port_server.py'), |
| '-p', '%d' % port_server_port, '-l', logfile] |
| env = dict(os.environ) |
| env['BUILD_ID'] = 'pleaseDontKillMeJenkins' |
| if platform_string() == 'windows': |
| # Working directory of port server needs to be outside of Jenkins |
| # workspace to prevent file lock issues. |
| tempdir = tempfile.mkdtemp() |
| port_server = subprocess.Popen( |
| args, |
| env=env, |
| cwd=tempdir, |
| creationflags = 0x00000008, # detached process |
| close_fds=True) |
| else: |
| port_server = subprocess.Popen( |
| args, |
| env=env, |
| preexec_fn=os.setsid, |
| close_fds=True) |
| time.sleep(1) |
| # ensure port server is up |
| waits = 0 |
| while True: |
| if waits > 10: |
| print 'killing port server due to excessive start up waits' |
| port_server.kill() |
| if port_server.poll() is not None: |
| print 'port_server failed to start' |
| # try one final time: maybe another build managed to start one |
| time.sleep(1) |
| try: |
| urllib2.urlopen('http://localhost:%d/get' % port_server_port, |
| timeout=1).read() |
| print 'last ditch attempt to contact port server succeeded' |
| break |
| except: |
| traceback.print_exc(); |
| port_log = open(logfile, 'r').read() |
| print port_log |
| sys.exit(1) |
| try: |
| urllib2.urlopen('http://localhost:%d/get' % port_server_port, |
| timeout=1).read() |
| print 'port server is up and ready' |
| break |
| except socket.timeout: |
| print 'waiting for port_server: timeout' |
| traceback.print_exc(); |
| time.sleep(1) |
| waits += 1 |
| except urllib2.URLError: |
| print 'waiting for port_server: urlerror' |
| traceback.print_exc(); |
| time.sleep(1) |
| waits += 1 |
| except: |
| traceback.print_exc(); |
| port_server.kill() |
| raise |
| |
| |
| def _calculate_num_runs_failures(list_of_results): |
| """Caculate number of runs and failures for a particular test. |
| |
| Args: |
| list_of_results: (List) of JobResult object. |
| Returns: |
| A tuple of total number of runs and failures. |
| """ |
| num_runs = len(list_of_results) # By default, there is 1 run per JobResult. |
| num_failures = 0 |
| for jobresult in list_of_results: |
| if jobresult.retries > 0: |
| num_runs += jobresult.retries |
| if jobresult.num_failures > 0: |
| num_failures += jobresult.num_failures |
| return num_runs, num_failures |
| |
| |
| # _build_and_run results |
| class BuildAndRunError(object): |
| |
| BUILD = object() |
| TEST = object() |
| POST_TEST = object() |
| |
| |
| # returns a list of things that failed (or an empty list on success) |
| def _build_and_run( |
| check_cancelled, newline_on_success, cache, xml_report=None, build_only=False): |
| """Do one pass of building & running tests.""" |
| # build latest sequentially |
| num_failures, resultset = jobset.run( |
| build_steps, maxjobs=1, stop_on_failure=True, |
| newline_on_success=newline_on_success, travis=args.travis) |
| if num_failures: |
| return [BuildAndRunError.BUILD] |
| |
| if build_only: |
| if xml_report: |
| report_utils.render_junit_xml_report(resultset, xml_report) |
| return [] |
| |
| # start antagonists |
| antagonists = [subprocess.Popen(['tools/run_tests/antagonist.py']) |
| for _ in range(0, args.antagonists)] |
| port_server_port = 32767 |
| _start_port_server(port_server_port) |
| resultset = None |
| num_test_failures = 0 |
| try: |
| infinite_runs = runs_per_test == 0 |
| one_run = set( |
| spec |
| for config in run_configs |
| for language in languages |
| for spec in language.test_specs(config, args) |
| if re.search(args.regex, spec.shortname)) |
| # When running on travis, we want out test runs to be as similar as possible |
| # for reproducibility purposes. |
| if args.travis: |
| massaged_one_run = sorted(one_run, key=lambda x: x.shortname) |
| else: |
| # whereas otherwise, we want to shuffle things up to give all tests a |
| # chance to run. |
| massaged_one_run = list(one_run) # random.shuffle needs an indexable seq. |
| random.shuffle(massaged_one_run) # which it modifies in-place. |
| if infinite_runs: |
| assert len(massaged_one_run) > 0, 'Must have at least one test for a -n inf run' |
| runs_sequence = (itertools.repeat(massaged_one_run) if infinite_runs |
| else itertools.repeat(massaged_one_run, runs_per_test)) |
| all_runs = itertools.chain.from_iterable(runs_sequence) |
| |
| num_test_failures, resultset = jobset.run( |
| all_runs, check_cancelled, newline_on_success=newline_on_success, |
| travis=args.travis, infinite_runs=infinite_runs, maxjobs=args.jobs, |
| stop_on_failure=args.stop_on_failure, |
| cache=cache if not xml_report else None, |
| add_env={'GRPC_TEST_PORT_SERVER': 'localhost:%d' % port_server_port}) |
| if resultset: |
| for k, v in resultset.iteritems(): |
| num_runs, num_failures = _calculate_num_runs_failures(v) |
| if num_failures == num_runs: # what about infinite_runs??? |
| jobset.message('FAILED', k, do_newline=True) |
| elif num_failures > 0: |
| jobset.message( |
| 'FLAKE', '%s [%d/%d runs flaked]' % (k, num_failures, num_runs), |
| do_newline=True) |
| else: |
| jobset.message('PASSED', k, do_newline=True) |
| finally: |
| for antagonist in antagonists: |
| antagonist.kill() |
| if xml_report and resultset: |
| report_utils.render_junit_xml_report(resultset, xml_report) |
| |
| number_failures, _ = jobset.run( |
| post_tests_steps, maxjobs=1, stop_on_failure=True, |
| newline_on_success=newline_on_success, travis=args.travis) |
| |
| out = [] |
| if number_failures: |
| out.append(BuildAndRunError.POST_TEST) |
| if num_test_failures: |
| out.append(BuildAndRunError.TEST) |
| |
| if cache: cache.save() |
| |
| return out |
| |
| |
| test_cache = TestCache(runs_per_test == 1) |
| test_cache.maybe_load() |
| |
| if forever: |
| success = True |
| while True: |
| dw = watch_dirs.DirWatcher(['src', 'include', 'test', 'examples']) |
| initial_time = dw.most_recent_change() |
| have_files_changed = lambda: dw.most_recent_change() != initial_time |
| previous_success = success |
| errors = _build_and_run(check_cancelled=have_files_changed, |
| newline_on_success=False, |
| cache=test_cache, |
| build_only=args.build_only) == 0 |
| if not previous_success and not errors: |
| jobset.message('SUCCESS', |
| 'All tests are now passing properly', |
| do_newline=True) |
| jobset.message('IDLE', 'No change detected') |
| while not have_files_changed(): |
| time.sleep(1) |
| else: |
| errors = _build_and_run(check_cancelled=lambda: False, |
| newline_on_success=args.newline_on_success, |
| cache=test_cache, |
| xml_report=args.xml_report, |
| build_only=args.build_only) |
| if not errors: |
| jobset.message('SUCCESS', 'All tests passed', do_newline=True) |
| else: |
| jobset.message('FAILED', 'Some tests failed', do_newline=True) |
| exit_code = 0 |
| if BuildAndRunError.BUILD in errors: |
| exit_code |= 1 |
| if BuildAndRunError.TEST in errors and not args.travis: |
| exit_code |= 2 |
| if BuildAndRunError.POST_TEST in errors: |
| exit_code |= 4 |
| sys.exit(exit_code) |
| |