diff options
Diffstat (limited to 'js/src/devtools/automation/autospider.py')
-rwxr-xr-x | js/src/devtools/automation/autospider.py | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/js/src/devtools/automation/autospider.py b/js/src/devtools/automation/autospider.py new file mode 100755 index 000000000..030d8c98d --- /dev/null +++ b/js/src/devtools/automation/autospider.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python + +import argparse +import json +import logging +import re +import os +import platform +import posixpath +import shutil +import subprocess +import sys + +from collections import Counter, namedtuple +from os import environ as env +from subprocess import Popen +from threading import Timer + +Dirs = namedtuple('Dirs', ['scripts', 'js_src', 'source', 'tooltool']) + + +def directories(pathmodule, cwd, fixup=lambda s: s): + scripts = pathmodule.join(fixup(cwd), fixup(pathmodule.dirname(__file__))) + js_src = pathmodule.abspath(pathmodule.join(scripts, "..", "..")) + source = pathmodule.abspath(pathmodule.join(js_src, "..", "..")) + tooltool = pathmodule.abspath(env.get('TOOLTOOL_CHECKOUT', + pathmodule.join(source, ".."))) + return Dirs(scripts, js_src, source, tooltool) + +# Some scripts will be called with sh, which cannot use backslashed +# paths. So for direct subprocess.* invocation, use normal paths from +# DIR, but when running under the shell, use POSIX style paths. +DIR = directories(os.path, os.getcwd()) +PDIR = directories(posixpath, os.environ["PWD"], + fixup=lambda s: re.sub(r'^(\w):', r'/\1', s)) + +parser = argparse.ArgumentParser( + description='Run a spidermonkey shell build job') +parser.add_argument('--dep', action='store_true', + help='do not clobber the objdir before building') +parser.add_argument('--platform', '-p', type=str, metavar='PLATFORM', + default='', help='build platform, including a suffix ("-debug" or "") used by buildbot to override the variant\'s "debug" setting. The platform can be used to specify 32 vs 64 bits.') +parser.add_argument('--timeout', '-t', type=int, metavar='TIMEOUT', + default=10800, + help='kill job after TIMEOUT seconds') +parser.add_argument('--objdir', type=str, metavar='DIR', + default=env.get('OBJDIR', 'obj-spider'), + help='object directory') +group = parser.add_mutually_exclusive_group() +group.add_argument('--optimize', action='store_true', + help='generate an optimized build. Overrides variant setting.') +group.add_argument('--no-optimize', action='store_false', + dest='optimize', + help='generate a non-optimized build. Overrides variant setting.') +group.set_defaults(optimize=None) +group = parser.add_mutually_exclusive_group() +group.add_argument('--debug', action='store_true', + help='generate a debug build. Overrides variant setting.') +group.add_argument('--no-debug', action='store_false', + dest='debug', + help='generate a non-debug build. Overrides variant setting.') +group.set_defaults(debug=None) +parser.add_argument('--run-tests', '--tests', type=str, metavar='TESTSUITE', + default='', + help="comma-separated set of test suites to add to the variant's default set") +parser.add_argument('--skip-tests', '--skip', type=str, metavar='TESTSUITE', + default='', + help="comma-separated set of test suites to remove from the variant's default set") +parser.add_argument('--build-only', '--build', + dest='skip_tests', action='store_const', const='all', + help="only do a build, do not run any tests") +parser.add_argument('--nobuild', action='store_true', + help='Do not do a build. Rerun tests on existing build.') +parser.add_argument('variant', type=str, + help='type of job requested, see variants/ subdir') +args = parser.parse_args() + + +def set_vars_from_script(script, vars): + '''Run a shell script, then dump out chosen environment variables. The build + system uses shell scripts to do some configuration that we need to + borrow. On Windows, the script itself must output the variable settings + (in the form "export FOO=<value>"), since otherwise there will be + problems with mismatched Windows/POSIX formats. + ''' + script_text = 'source %s' % script + if platform.system() == 'Windows': + parse_state = 'parsing exports' + else: + script_text += '; echo VAR SETTINGS:; ' + script_text += '; '.join('echo $' + var for var in vars) + parse_state = 'scanning' + stdout = subprocess.check_output(['sh', '-x', '-c', script_text]) + tograb = vars[:] + originals = {} + for line in stdout.splitlines(): + if parse_state == 'scanning': + if line == 'VAR SETTINGS:': + parse_state = 'grabbing' + elif parse_state == 'grabbing': + var = tograb.pop(0) + env[var] = line + elif parse_state == 'parsing exports': + m = re.match(r'export (\w+)=(.*)', line) + if m: + var, value = m.groups() + if var in tograb: + env[var] = value + print("Setting %s = %s" % (var, value)) + if var.startswith("ORIGINAL_"): + originals[var[9:]] = value + + # An added wrinkle: on Windows developer systems, the sourced script will + # blow away current settings for eg LIBS, to point to the ones that would + # be installed via automation. So we will append the original settings. (On + # an automation system, the original settings will be empty or point to + # nonexistent stuff.) + if platform.system() == 'Windows': + for var in vars: + if var in originals and len(originals[var]) > 0: + env[var] = "%s;%s" % (env[var], originals[var]) + + +def ensure_dir_exists(name, clobber=True): + if clobber: + shutil.rmtree(name, ignore_errors=True) + try: + os.mkdir(name) + except OSError: + if clobber: + raise + +with open(os.path.join(DIR.scripts, "variants", args.variant)) as fh: + variant = json.load(fh) + +if args.variant == 'nonunified': + # Rewrite js/src/**/moz.build to replace UNIFIED_SOURCES to SOURCES. + # Note that this modifies the current checkout. + for dirpath, dirnames, filenames in os.walk(DIR.js_src): + if 'moz.build' in filenames: + subprocess.check_call(['sed', '-i', 's/UNIFIED_SOURCES/SOURCES/', + os.path.join(dirpath, 'moz.build')]) + +OBJDIR = os.path.join(DIR.source, args.objdir) +OUTDIR = os.path.join(OBJDIR, "out") +POBJDIR = posixpath.join(PDIR.source, args.objdir) +AUTOMATION = env.get('AUTOMATION', False) +MAKE = env.get('MAKE', 'make') +MAKEFLAGS = env.get('MAKEFLAGS', '-j6') +UNAME_M = subprocess.check_output(['uname', '-m']).strip() + +CONFIGURE_ARGS = variant['configure-args'] + +opt = args.optimize +if opt is None: + opt = variant.get('optimize') +if opt is not None: + CONFIGURE_ARGS += (" --enable-optimize" if opt else " --disable-optimize") + +opt = args.debug +if opt is None and args.platform: + # Override variant['debug']. + opt = ('-debug' in args.platform) +if opt is None: + opt = variant.get('debug') +if opt is not None: + CONFIGURE_ARGS += (" --enable-debug" if opt else " --disable-debug") + +# Any jobs that wish to produce additional output can save them into the upload +# directory if there is such a thing, falling back to OBJDIR. +env.setdefault('MOZ_UPLOAD_DIR', OBJDIR) +ensure_dir_exists(env['MOZ_UPLOAD_DIR'], clobber=False) + +# Some of the variants request a particular word size (eg ARM simulators). +word_bits = variant.get('bits') + +# On Linux and Windows, we build 32- and 64-bit versions on a 64 bit +# host, so the caller has to specify what is desired. +if word_bits is None and args.platform: + platform_arch = args.platform.split('-')[0] + if platform_arch in ('win32', 'linux'): + word_bits = 32 + elif platform_arch in ('win64', 'linux64'): + word_bits = 64 + +# Fall back to the word size of the host. +if word_bits is None: + word_bits = 64 if UNAME_M == 'x86_64' else 32 + +if 'compiler' in variant: + compiler = variant['compiler'] +elif platform.system() == 'Darwin': + compiler = 'clang' +elif platform.system() == 'Windows': + compiler = 'cl' +else: + compiler = 'gcc' + +cxx = {'clang': 'clang++', 'gcc': 'g++', 'cl': 'cl'}.get(compiler) + +compiler_dir = env.get('GCCDIR', os.path.join(DIR.tooltool, compiler)) +if os.path.exists(os.path.join(compiler_dir, 'bin', compiler)): + env.setdefault('CC', os.path.join(compiler_dir, 'bin', compiler)) + env.setdefault('CXX', os.path.join(compiler_dir, 'bin', cxx)) + platlib = 'lib64' if word_bits == 64 else 'lib' + env.setdefault('LD_LIBRARY_PATH', os.path.join(compiler_dir, platlib)) +else: + env.setdefault('CC', compiler) + env.setdefault('CXX', cxx) + +if platform.system() == 'Darwin': + os.environ['SOURCE'] = DIR.source + set_vars_from_script(os.path.join(DIR.scripts, 'macbuildenv.sh'), + ['CC', 'CXX']) +elif platform.system() == 'Windows': + MAKE = env.get('MAKE', 'mozmake') + os.environ['SOURCE'] = DIR.source + if word_bits == 64: + os.environ['USE_64BIT'] = '1' + set_vars_from_script(posixpath.join(PDIR.scripts, 'winbuildenv.sh'), + ['PATH', 'INCLUDE', 'LIB', 'LIBPATH', 'CC', 'CXX', + 'WINDOWSSDKDIR']) + +# Compiler flags, based on word length +if word_bits == 32: + if compiler == 'clang': + env['CC'] = '{CC} -arch i386'.format(**env) + env['CXX'] = '{CXX} -arch i386'.format(**env) + elif compiler == 'gcc': + env['CC'] = '{CC} -m32'.format(**env) + env['CXX'] = '{CXX} -m32'.format(**env) + env['AR'] = 'ar' + +# Configure flags, based on word length and cross-compilation +if word_bits == 32: + if platform.system() == 'Windows': + CONFIGURE_ARGS += ' --target=i686-pc-mingw32 --host=i686-pc-mingw32' + elif platform.system() == 'Linux': + if UNAME_M != 'arm': + CONFIGURE_ARGS += ' --target=i686-pc-linux --host=i686-pc-linux' +else: + if platform.system() == 'Windows': + CONFIGURE_ARGS += ' --target=x86_64-pc-mingw32 --host=x86_64-pc-mingw32' + +# Timeouts. +ACTIVE_PROCESSES = set() + + +def killall(): + for proc in ACTIVE_PROCESSES: + proc.kill() + ACTIVE_PROCESSES.clear() + +timer = Timer(args.timeout, killall) +timer.daemon = True +timer.start() + +ensure_dir_exists(OBJDIR, clobber=not args.dep and not args.nobuild) +ensure_dir_exists(OUTDIR) + + +def run_command(command, check=False, **kwargs): + proc = Popen(command, cwd=OBJDIR, **kwargs) + ACTIVE_PROCESSES.add(proc) + stdout, stderr = None, None + try: + stdout, stderr = proc.communicate() + finally: + ACTIVE_PROCESSES.discard(proc) + status = proc.wait() + if check and status != 0: + raise subprocess.CalledProcessError(status, command, output=stderr) + return stdout, stderr, status + +# Add in environment variable settings for this variant. Normally used to +# modify the flags passed to the shell or to set the GC zeal mode. +for k, v in variant.get('env', {}).items(): + env[k] = v.format( + DIR=DIR.scripts, + TOOLTOOL_CHECKOUT=DIR.tooltool, + MOZ_UPLOAD_DIR=env['MOZ_UPLOAD_DIR'], + OUTDIR=OUTDIR, + ) + +if not args.nobuild: + CONFIGURE_ARGS += ' --enable-nspr-build' + CONFIGURE_ARGS += ' --prefix={OBJDIR}/dist'.format(OBJDIR=POBJDIR) + + # Generate a configure script from configure.in. + configure = os.path.join(DIR.js_src, 'configure') + if not os.path.exists(configure): + shutil.copyfile(configure + ".in", configure) + os.chmod(configure, 0755) + + # Run configure; make + run_command(['sh', '-c', posixpath.join(PDIR.js_src, 'configure') + ' ' + CONFIGURE_ARGS], check=True) + run_command('%s -s -w %s' % (MAKE, MAKEFLAGS), shell=True, check=True) + +COMMAND_PREFIX = [] +# On Linux, disable ASLR to make shell builds a bit more reproducible. +if subprocess.call("type setarch >/dev/null 2>&1", shell=True) == 0: + COMMAND_PREFIX.extend(['setarch', UNAME_M, '-R']) + + +def run_test_command(command, **kwargs): + _, _, status = run_command(COMMAND_PREFIX + command, check=False, **kwargs) + return status + +test_suites = set(['jstests', 'jittest', 'jsapitests', 'checks']) + + +def normalize_tests(tests): + if 'all' in tests: + return test_suites + return tests + +# Need a platform name to use as a key in variant files. +if args.platform: + variant_platform = args.platform.split("-")[0] +elif platform.system() == 'Windows': + variant_platform = 'win64' if word_bits == 64 else 'win32' +elif platform.system() == 'Linux': + variant_platform = 'linux64' if word_bits == 64 else 'linux' +elif platform.system() == 'Darwin': + variant_platform = 'macosx64' +else: + variant_platform = 'other' + +# Skip any tests that are not run on this platform (or the 'all' platform). +test_suites -= set(normalize_tests(variant.get('skip-tests', {}).get(variant_platform, []))) +test_suites -= set(normalize_tests(variant.get('skip-tests', {}).get('all', []))) + +# Add in additional tests for this platform (or the 'all' platform). +test_suites |= set(normalize_tests(variant.get('extra-tests', {}).get(variant_platform, []))) +test_suites |= set(normalize_tests(variant.get('extra-tests', {}).get('all', []))) + +# Now adjust the variant's default test list with command-line arguments. +test_suites |= set(normalize_tests(args.run_tests.split(","))) +test_suites -= set(normalize_tests(args.skip_tests.split(","))) + +# Always run all enabled tests, even if earlier ones failed. But return the +# first failed status. +results = [] + +# 'checks' is a superset of 'check-style'. +if 'checks' in test_suites: + results.append(run_test_command([MAKE, 'check'])) +elif 'check-style' in test_suites: + results.append(run_test_command([MAKE, 'check-style'])) + +if 'jittest' in test_suites: + results.append(run_test_command([MAKE, 'check-jit-test'])) +if 'jsapitests' in test_suites: + jsapi_test_binary = os.path.join(OBJDIR, 'dist', 'bin', 'jsapi-tests') + results.append(run_test_command([jsapi_test_binary])) +if 'jstests' in test_suites: + results.append(run_test_command([MAKE, 'check-jstests'])) + +# FIXME bug 1291449: This would be unnecessary if we could run msan with -mllvm +# -msan-keep-going, but in clang 3.8 it causes a hang during compilation. +if variant.get('ignore-test-failures'): + print("Ignoring test results %s" % (results,)) + results = [0] + +if args.variant in ('tsan', 'msan'): + files = filter(lambda f: f.startswith("sanitize_log."), os.listdir(OUTDIR)) + fullfiles = [os.path.join(OUTDIR, f) for f in files] + + # Summarize results + sites = Counter() + for filename in fullfiles: + with open(os.path.join(OUTDIR, filename), 'rb') as fh: + for line in fh: + m = re.match(r'^SUMMARY: \w+Sanitizer: (?:data race|use-of-uninitialized-value) (.*)', + line.strip()) + if m: + # Some reports include file:line:column, some just + # file:line. Just in case it's nondeterministic, we will + # canonicalize to just the line number. + site = re.sub(r'^(\S+?:\d+)(:\d+)* ', r'\1 ', m.group(1)) + sites[site] += 1 + + # Write a summary file and display it to stdout. + summary_filename = os.path.join(env['MOZ_UPLOAD_DIR'], "%s_summary.txt" % args.variant) + with open(summary_filename, 'wb') as outfh: + for location, count in sites.most_common(): + print >> outfh, "%d %s" % (count, location) + print(open(summary_filename, 'rb').read()) + + if 'max-errors' in variant: + print("Found %d errors out of %d allowed" % (len(sites), variant['max-errors'])) + if len(sites) > variant['max-errors']: + results.append(1) + + # Gather individual results into a tarball. Note that these are + # distinguished only by pid of the JS process running within each test, so + # given the 16-bit limitation of pids, it's totally possible that some of + # these files will be lost due to being overwritten. + command = ['tar', '-C', OUTDIR, '-zcf', + os.path.join(env['MOZ_UPLOAD_DIR'], '%s.tar.gz' % args.variant)] + command += files + subprocess.call(command) + +for st in results: + if st != 0: + sys.exit(st) |