tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

autospider.py (23325B)


      1 #!/usr/bin/env python3
      2 # This Source Code Form is subject to the terms of the Mozilla Public
      3 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
      4 # You can obtain one at http://mozilla.org/MPL/2.0/.
      5 
      6 
      7 import argparse
      8 import json
      9 import logging
     10 import multiprocessing
     11 import os
     12 import platform
     13 import shlex
     14 import shutil
     15 import subprocess
     16 import sys
     17 from collections import Counter, namedtuple
     18 from logging import info
     19 from os import environ as env
     20 from pathlib import Path
     21 from subprocess import Popen
     22 from threading import Timer
     23 
     24 Dirs = namedtuple("Dirs", ["scripts", "js_src", "source", "fetches"])
     25 
     26 
     27 def directories(pathmodule, cwd, fixup=lambda s: s):
     28    scripts = pathmodule.join(fixup(cwd), fixup(pathmodule.dirname(__file__)))
     29    js_src = pathmodule.abspath(pathmodule.join(scripts, "..", ".."))
     30    source = pathmodule.abspath(pathmodule.join(js_src, "..", ".."))
     31    mozbuild = pathmodule.abspath(
     32        # os.path.expanduser does not work on Windows.
     33        env.get("MOZBUILD_STATE_PATH") or pathmodule.join(Path.home(), ".mozbuild")
     34    )
     35    fetches = pathmodule.abspath(env.get("MOZ_FETCHES_DIR", mozbuild))
     36    return Dirs(scripts, js_src, source, fetches)
     37 
     38 
     39 def quote(s):
     40    # shlex quotes for the purpose of passing to the native shell, which is cmd
     41    # on Windows, and therefore will not replace backslashed paths with forward
     42    # slashes. When such a path is passed to sh, the backslashes will be
     43    # interpreted as escape sequences.
     44    return shlex.quote(s).replace("\\", "/")
     45 
     46 
     47 # Some scripts will be called with sh, which cannot use backslashed
     48 # paths. So for direct subprocess.* invocation, use normal paths from
     49 # DIR, but when running under the shell, use POSIX style paths.
     50 DIR = directories(os.path, os.getcwd())
     51 
     52 AUTOMATION = bool(env.get("AUTOMATION"))
     53 
     54 parser = argparse.ArgumentParser(description="Run a spidermonkey shell build job")
     55 parser.add_argument(
     56    "--verbose",
     57    action="store_true",
     58    default=AUTOMATION,
     59    help="display additional logging info",
     60 )
     61 parser.add_argument(
     62    "--dep", action="store_true", help="do not clobber the objdir before building"
     63 )
     64 parser.add_argument(
     65    "--keep",
     66    action="store_true",
     67    help="do not delete the sanitizer output directory (for testing)",
     68 )
     69 parser.add_argument(
     70    "--platform",
     71    "-p",
     72    type=str,
     73    metavar="PLATFORM",
     74    default="",
     75    help='build platform, including a suffix ("-debug" or "") used '
     76    'by buildbot to override the variant\'s "debug" setting. The platform can be '
     77    "used to specify 32 vs 64 bits.",
     78 )
     79 parser.add_argument(
     80    "--timeout",
     81    "-t",
     82    type=int,
     83    metavar="TIMEOUT",
     84    default=12600,
     85    help="kill job after TIMEOUT seconds",
     86 )
     87 parser.add_argument(
     88    "--objdir",
     89    type=str,
     90    metavar="DIR",
     91    # The real default must be set later so that OBJDIR can be
     92    # relative to the srcdir.
     93    default=env.get("OBJDIR"),
     94    help="object directory",
     95 )
     96 group = parser.add_mutually_exclusive_group()
     97 group.add_argument(
     98    "--optimize",
     99    action="store_true",
    100    help="generate an optimized build. Overrides variant setting.",
    101 )
    102 group.add_argument(
    103    "--no-optimize",
    104    action="store_false",
    105    dest="optimize",
    106    help="generate a non-optimized build. Overrides variant setting.",
    107 )
    108 group.set_defaults(optimize=None)
    109 group = parser.add_mutually_exclusive_group()
    110 group.add_argument(
    111    "--debug",
    112    action="store_true",
    113    help="generate a debug build. Overrides variant setting.",
    114 )
    115 group.add_argument(
    116    "--no-debug",
    117    action="store_false",
    118    dest="debug",
    119    help="generate a non-debug build. Overrides variant setting.",
    120 )
    121 group.set_defaults(debug=None)
    122 group = parser.add_mutually_exclusive_group()
    123 group.add_argument(
    124    "--jemalloc",
    125    action="store_true",
    126    dest="jemalloc",
    127    help="use mozilla's jemalloc instead of the default allocator",
    128 )
    129 group.add_argument(
    130    "--no-jemalloc",
    131    action="store_false",
    132    dest="jemalloc",
    133    help="use the default allocator instead of mozilla's jemalloc",
    134 )
    135 group.set_defaults(jemalloc=None)
    136 parser.add_argument(
    137    "--run-tests",
    138    "--tests",
    139    type=str,
    140    metavar="TESTSUITE",
    141    default="",
    142    help="comma-separated set of test suites to add to the variant's default set",
    143 )
    144 parser.add_argument(
    145    "--skip-tests",
    146    "--skip",
    147    type=str,
    148    metavar="TESTSUITE",
    149    default="",
    150    help="comma-separated set of test suites to remove from the variant's default set",
    151 )
    152 parser.add_argument(
    153    "--build-only",
    154    "--build",
    155    dest="skip_tests",
    156    action="store_const",
    157    const="all",
    158    help="only do a build, do not run any tests",
    159 )
    160 parser.add_argument(
    161    "--nobuild",
    162    action="store_true",
    163    help="Do not do a build. Rerun tests on existing build.",
    164 )
    165 parser.add_argument(
    166    "variant", type=str, help="type of job requested, see variants/ subdir"
    167 )
    168 args = parser.parse_args()
    169 
    170 logging.basicConfig(level=logging.INFO, format="%(message)s")
    171 
    172 env["CPP_UNIT_TESTS_DIR_JS_SRC"] = DIR.js_src
    173 if AUTOMATION and platform.system() == "Windows":
    174    # build/win{32,64}/mozconfig.vs-latest uses TOOLTOOL_DIR to set VSPATH.
    175    env["TOOLTOOL_DIR"] = DIR.fetches
    176 
    177 OBJDIR = args.objdir or os.path.join(DIR.source, "obj-spider")
    178 OBJDIR = os.path.abspath(OBJDIR)
    179 OUTDIR = os.path.join(OBJDIR, "out")
    180 MAKE = env.get("MAKE", "make")
    181 PYTHON = sys.executable
    182 MACH = os.path.join(DIR.source, "mach")
    183 
    184 for d in DIR._fields:
    185    info(f"DIR.{d} = {getattr(DIR, d)}")
    186 
    187 
    188 def ensure_dir_exists(
    189    name, clobber=True, creation_marker_filename="CREATED-BY-AUTOSPIDER"
    190 ):
    191    if creation_marker_filename is None:
    192        marker = None
    193    else:
    194        marker = os.path.join(name, creation_marker_filename)
    195    if clobber:
    196        if (
    197            not AUTOMATION
    198            and marker
    199            and os.path.exists(name)
    200            and not os.path.exists(marker)
    201        ):
    202            raise Exception(
    203                "Refusing to delete objdir %s because it was not created by autospider"
    204                % name
    205            )
    206        shutil.rmtree(name, ignore_errors=True)
    207    try:
    208        os.mkdir(name)
    209        if marker:
    210            open(marker, "a").close()
    211    except OSError:
    212        if clobber:
    213            raise
    214 
    215 
    216 with open(os.path.join(DIR.scripts, "variants", args.variant)) as fh:
    217    variant = json.load(fh)
    218 
    219 # Some of the variants request a particular word size (eg ARM simulators).
    220 word_bits = variant.get("bits")
    221 
    222 # On Linux and Windows, we build 32- and 64-bit versions on a 64 bit
    223 # host, so the caller has to specify what is desired.
    224 if word_bits is None and args.platform:
    225    platform_arch = args.platform.split("-")[0]
    226    if platform_arch in ("win32", "linux"):
    227        word_bits = 32
    228    elif platform_arch in ("win64", "linux64"):
    229        word_bits = 64
    230 
    231 # Fall back to the word size of the host.
    232 if word_bits is None:
    233    word_bits = 64 if platform.architecture()[0] == "64bit" else 32
    234 
    235 # Need a platform name to use as a key in variant files.
    236 if args.platform:
    237    variant_platform = args.platform.split("-")[0]
    238 elif platform.system() == "Windows":
    239    variant_platform = "win64" if word_bits == 64 else "win32"
    240 elif platform.system() == "Linux":
    241    variant_platform = "linux64" if word_bits == 64 else "linux"
    242 elif platform.system() == "Darwin":
    243    variant_platform = "macosx64"
    244 else:
    245    variant_platform = "other"
    246 
    247 CONFIGURE_ARGS = variant["configure-args"]
    248 
    249 if variant_platform in ("win32", "win64"):
    250    compiler = "clang-cl"
    251 else:
    252    compiler = variant.get("compiler")
    253 if compiler != "gcc" and "clang-plugin" not in CONFIGURE_ARGS:
    254    CONFIGURE_ARGS += " --enable-clang-plugin"
    255 
    256 if compiler == "gcc":
    257    if AUTOMATION:
    258        fetches = env["MOZ_FETCHES_DIR"]
    259        env["CC"] = os.path.join(fetches, "gcc", "bin", "gcc")
    260        env["CXX"] = os.path.join(fetches, "gcc", "bin", "g++")
    261    else:
    262        env["CC"] = "gcc"
    263        env["CXX"] = "g++"
    264 
    265 opt = args.optimize
    266 if opt is None:
    267    opt = variant.get("optimize")
    268 if opt is not None:
    269    CONFIGURE_ARGS += " --enable-optimize" if opt else " --disable-optimize"
    270 
    271 opt = args.debug
    272 if opt is None:
    273    opt = variant.get("debug")
    274 if opt is not None:
    275    CONFIGURE_ARGS += " --enable-debug" if opt else " --disable-debug"
    276 
    277 opt = args.jemalloc
    278 if opt is not None:
    279    CONFIGURE_ARGS += " --enable-jemalloc" if opt else " --disable-jemalloc"
    280 
    281 # By default, we build with NSPR, even if not specified. But we actively allow
    282 # builds to disable NSPR.
    283 opt = variant.get("nspr")
    284 if opt is None or opt:
    285    CONFIGURE_ARGS += " --enable-nspr-build"
    286 
    287 env["LD_LIBRARY_PATH"] = ":".join(
    288    d
    289    for d in [
    290        # for libnspr etc.
    291        os.path.join(OBJDIR, "dist", "bin"),
    292        # existing search path, if any
    293        env.get("LD_LIBRARY_PATH"),
    294    ]
    295    if d is not None
    296 )
    297 
    298 os.environ["SOURCE"] = DIR.source
    299 if platform.system() == "Windows":
    300    MAKE = env.get("MAKE", "mozmake")
    301 
    302 # Configure flags, based on word length and cross-compilation
    303 if word_bits == 32:
    304    if platform.system() == "Windows":
    305        CONFIGURE_ARGS += " --target=i686-pc-windows-msvc"
    306    elif platform.system() == "Linux":
    307        if not platform.machine().startswith("arm"):
    308            CONFIGURE_ARGS += " --target=i686-pc-linux"
    309 
    310    # Add SSE2 support for x86/x64 architectures.
    311    if not platform.machine().startswith("arm"):
    312        if platform.system() == "Windows":
    313            sse_flags = "-arch:SSE2"
    314        else:
    315            sse_flags = "-msse -msse2 -mfpmath=sse"
    316        env["CCFLAGS"] = "{} {}".format(env.get("CCFLAGS", ""), sse_flags)
    317        env["CXXFLAGS"] = "{} {}".format(env.get("CXXFLAGS", ""), sse_flags)
    318 elif platform.system() == "Windows":
    319    CONFIGURE_ARGS += " --target=x86_64-pc-windows-msvc"
    320 
    321 if platform.system() == "Linux" and AUTOMATION:
    322    CONFIGURE_ARGS = "--enable-stdcxx-compat " + CONFIGURE_ARGS
    323 
    324 # Timeouts.
    325 ACTIVE_PROCESSES = set()
    326 
    327 
    328 def killall():
    329    for proc in ACTIVE_PROCESSES:
    330        proc.kill()
    331    ACTIVE_PROCESSES.clear()
    332 
    333 
    334 timer = Timer(args.timeout, killall)
    335 timer.daemon = True
    336 timer.start()
    337 
    338 ensure_dir_exists(OBJDIR, clobber=not args.dep and not args.nobuild)
    339 ensure_dir_exists(OUTDIR, clobber=not args.keep)
    340 
    341 # Any jobs that wish to produce additional output can save them into the upload
    342 # directory if there is such a thing, falling back to OBJDIR.
    343 env.setdefault("MOZ_UPLOAD_DIR", OBJDIR)
    344 ensure_dir_exists(env["MOZ_UPLOAD_DIR"], clobber=False, creation_marker_filename=None)
    345 info("MOZ_UPLOAD_DIR = {}".format(env["MOZ_UPLOAD_DIR"]))
    346 
    347 
    348 def run_command(command, check=False, **kwargs):
    349    kwargs.setdefault("cwd", OBJDIR)
    350    info("in directory {}, running {}".format(kwargs["cwd"], command))
    351    if platform.system() == "Windows":
    352        # Windows will use cmd for the shell, which causes all sorts of
    353        # problems. Use sh instead, quoting appropriately. (Use sh in all
    354        # cases, not just when shell=True, because we want to be able to use
    355        # paths that sh understands and cmd does not.)
    356        if not isinstance(command, list):
    357            if kwargs.get("shell"):
    358                command = shlex.split(command)
    359            else:
    360                command = [command]
    361 
    362        command = " ".join(quote(c) for c in command)
    363        command = ["sh", "-c", command]
    364        kwargs["shell"] = False
    365    proc = Popen(command, **kwargs)
    366    ACTIVE_PROCESSES.add(proc)
    367    stdout, stderr = None, None
    368    try:
    369        stdout, stderr = proc.communicate()
    370    finally:
    371        ACTIVE_PROCESSES.discard(proc)
    372    status = proc.wait()
    373    if check and status != 0:
    374        raise subprocess.CalledProcessError(status, command, output=stderr)
    375    return stdout, stderr, status
    376 
    377 
    378 def run_mach_command(command, check=False, **kwargs):
    379    _stdout, _stderr, status = run_command(
    380        [PYTHON, MACH] + command, check=check, **kwargs
    381    )
    382    return status
    383 
    384 
    385 # Replacement strings in environment variables.
    386 REPLACEMENTS = {
    387    "DIR": DIR.scripts,
    388    "MOZ_FETCHES_DIR": DIR.fetches,
    389    "MOZ_UPLOAD_DIR": env["MOZ_UPLOAD_DIR"],
    390    "OUTDIR": OUTDIR,
    391 }
    392 
    393 # Add in environment variable settings for this variant. Normally used to
    394 # modify the flags passed to the shell or to set the GC zeal mode.
    395 for k, v in variant.get("env", {}).items():
    396    env[k] = v.format(**REPLACEMENTS)
    397 
    398 # Do similar substitution for the extra_args keyed lists.
    399 extra_args = {"jit-test": [], "jstests": [], "gdb": []}
    400 extra_args.update(variant.get("extra-args", {}))
    401 for v in extra_args.values():
    402    v[:] = [arg.format(**REPLACEMENTS) for arg in v]
    403 
    404 if AUTOMATION:
    405    # Currently only supported on linux64.
    406    if platform.system() == "Linux" and word_bits == 64:
    407        use_minidump = variant.get("use_minidump", True)
    408    else:
    409        use_minidump = False
    410 else:
    411    use_minidump = False
    412 
    413 
    414 def resolve_path(dirs, *components):
    415    if None in components:
    416        return None
    417    for dir in dirs:
    418        path = os.path.join(dir, *components)
    419        if os.path.exists(path):
    420            return path
    421 
    422 
    423 if use_minidump:
    424    env.setdefault("MINIDUMP_SAVE_PATH", env["MOZ_UPLOAD_DIR"])
    425 
    426    injector_basename = {
    427        "Linux": "libbreakpadinjector.so",
    428        "Darwin": "breakpadinjector.dylib",
    429    }.get(platform.system())
    430 
    431    injector_lib = resolve_path((DIR.fetches,), "injector", injector_basename)
    432    stackwalk = resolve_path((DIR.fetches,), "minidump-stackwalk", "minidump-stackwalk")
    433    if stackwalk is not None:
    434        env.setdefault("MINIDUMP_STACKWALK", stackwalk)
    435    dump_syms = resolve_path((DIR.fetches,), "dump_syms", "dump_syms")
    436    if dump_syms is not None:
    437        env.setdefault("DUMP_SYMS", dump_syms)
    438 
    439    if injector_lib is None:
    440        use_minidump = False
    441 
    442    info(f"use_minidump is {use_minidump}")
    443    info("  MINIDUMP_SAVE_PATH={}".format(env["MINIDUMP_SAVE_PATH"]))
    444    info(f"  injector lib is {injector_lib}")
    445    info("  MINIDUMP_STACKWALK={}".format(env.get("MINIDUMP_STACKWALK")))
    446 
    447 
    448 mozconfig = os.path.join(DIR.source, "mozconfig.autospider")
    449 CONFIGURE_ARGS += f" --prefix={quote(OBJDIR)}/dist"
    450 
    451 # Generate a mozconfig.
    452 with open(mozconfig, "w") as fh:
    453    if AUTOMATION and platform.system() == "Windows":
    454        fh.write('. "$topsrcdir/build/mozconfig.clang-cl"\n')
    455    fh.write("ac_add_options --enable-project=js\n")
    456    fh.write("ac_add_options " + CONFIGURE_ARGS + "\n")
    457    fh.write("mk_add_options MOZ_OBJDIR=" + quote(OBJDIR) + "\n")
    458 
    459 env["MOZCONFIG"] = mozconfig
    460 
    461 if not args.nobuild:
    462    # Do the build
    463    run_mach_command(["build"], check=True)
    464 
    465    if use_minidump:
    466        # Convert symbols to breakpad format.
    467        cmd_env = env.copy()
    468        cmd_env["MOZ_SOURCE_REPO"] = "file://" + DIR.source
    469        cmd_env["RUSTC_COMMIT"] = "0"
    470        cmd_env["MOZ_CRASHREPORTER"] = "1"
    471        cmd_env["MOZ_AUTOMATION_BUILD_SYMBOLS"] = "1"
    472        run_mach_command(
    473            [
    474                "build",
    475                "recurse_syms",
    476            ],
    477            check=True,
    478            env=cmd_env,
    479        )
    480 
    481 COMMAND_PREFIX = []
    482 # On Linux, disable ASLR to make shell builds a bit more reproducible.
    483 # Bug 1795718 - Disable in automation for now as call to setarch requires extra
    484 # docker privileges.
    485 if not AUTOMATION and subprocess.call("type setarch >/dev/null 2>&1", shell=True) == 0:
    486    COMMAND_PREFIX.extend(["setarch", platform.machine(), "-R"])
    487 
    488 
    489 def run_test_command(command, **kwargs):
    490    _, _, status = run_command(COMMAND_PREFIX + command, check=False, **kwargs)
    491    return status
    492 
    493 
    494 def run_jsapitests(args):
    495    test_env = env.copy()
    496    if use_minidump and platform.system() == "Linux":
    497        test_env["LD_PRELOAD"] = injector_lib
    498    st = run_mach_command(["jsapi-tests"], env=test_env)
    499    if st < 0:
    500        info = " ".join(["jsapi-tests"] + args)
    501        print(f"PROCESS-CRASH | {info} | application crashed")
    502        print(f"Return code: {st}")
    503    return st
    504 
    505 
    506 default_test_suites = frozenset(["jstests", "jittest", "jsapitests", "checks"])
    507 nondefault_test_suites = frozenset(["gdb"])
    508 all_test_suites = default_test_suites | nondefault_test_suites
    509 
    510 test_suites = set(default_test_suites)
    511 
    512 
    513 def normalize_tests(tests):
    514    if "all" in tests:
    515        return default_test_suites
    516    return tests
    517 
    518 
    519 # Override environment variant settings conditionally.
    520 for k, v in variant.get("conditional-env", {}).get(variant_platform, {}).items():
    521    env[k] = v.format(**REPLACEMENTS)
    522 
    523 # Skip any tests that are not run on this platform (or the 'all' platform).
    524 test_suites -= set(
    525    normalize_tests(variant.get("skip-tests", {}).get(variant_platform, []))
    526 )
    527 test_suites -= set(normalize_tests(variant.get("skip-tests", {}).get("all", [])))
    528 
    529 # Add in additional tests for this platform (or the 'all' platform).
    530 test_suites |= set(
    531    normalize_tests(variant.get("extra-tests", {}).get(variant_platform, []))
    532 )
    533 test_suites |= set(normalize_tests(variant.get("extra-tests", {}).get("all", [])))
    534 
    535 # Now adjust the variant's default test list with command-line arguments.
    536 test_suites |= set(normalize_tests(args.run_tests.split(",")))
    537 test_suites -= set(normalize_tests(args.skip_tests.split(",")))
    538 if "all" in args.skip_tests.split(","):
    539    test_suites = []
    540 
    541 # Bug 1557130 - Atomics tests can create many additional threads which can
    542 # lead to resource exhaustion, resulting in intermittent failures. This was
    543 # only seen on beefy machines (> 32 cores), so limit the number of parallel
    544 # workers for now.
    545 #
    546 # Bug 1391877 - Windows test runs are getting mysterious timeouts when run
    547 # through taskcluster, but only when running many jit-test jobs in parallel.
    548 # Even at 16, some tests can overflow the paging file.
    549 worker_max = multiprocessing.cpu_count()
    550 jstest_workers = worker_max
    551 jittest_workers = worker_max
    552 if platform.system() == "Windows":
    553    jstest_workers = min(worker_max, 16)
    554    extra_args["jstests"].append(f"-j{jstest_workers}")
    555    jittest_workers = min(worker_max, 8)
    556    extra_args["jit-test"].append(f"-j{jittest_workers}")
    557 print(
    558    f"using {jstest_workers}/{worker_max} workers for jstests, "
    559    f"{jittest_workers}/{worker_max} for jittest"
    560 )
    561 
    562 if use_minidump:
    563    # Set up later js invocations to run with the breakpad injector loaded.
    564    # Originally, I intended for this to be used with LD_PRELOAD, but when
    565    # cross-compiling from 64- to 32-bit, that will fail and produce stderr
    566    # output when running any 64-bit commands, which breaks eg mozconfig
    567    # processing. So use the --dll command line mechanism universally.
    568    for suite in ("jstests", "jit-test"):
    569        extra_args[suite].append(f"--args=--dll {injector_lib}")
    570 
    571 # Report longest running tests in automation.
    572 for suite in ("jstests", "jit-test"):
    573    extra_args[suite].append("--show-slow")
    574 
    575 # Always run all enabled tests, even if earlier ones failed. But return the
    576 # first failed status.
    577 results = [("(make-nonempty)", 0)]
    578 
    579 if "checks" in test_suites:
    580    results.append(("make check", run_test_command([MAKE, "check"])))
    581 
    582 if "jittest" in test_suites:
    583    auto_args = []
    584    if AUTOMATION:
    585        auto_args = [
    586            "--no-slow",
    587            "--no-progress",
    588            "--format=automation",
    589            "--timeout=300",
    590            "--jitflags=all",
    591        ]
    592    results.append((
    593        "mach jit-test",
    594        run_mach_command(["jit-test", "--", *auto_args, *extra_args["jit-test"]]),
    595    ))
    596 if "jsapitests" in test_suites:
    597    st = run_jsapitests([])
    598    if st == 0:
    599        st = run_jsapitests(["--frontend-only"])
    600    results.append(("jsapi-tests", st))
    601 if "jstests" in test_suites:
    602    auto_args = []
    603    if AUTOMATION:
    604        auto_args = ["--no-progress", "--format=automation", "--timeout=300"]
    605    results.append((
    606        "mach jstests",
    607        run_mach_command(["jstests", "--", *auto_args, *extra_args["jstests"]]),
    608    ))
    609 if "gdb" in test_suites:
    610    test_script = os.path.join(DIR.js_src, "gdb", "run-tests.py")
    611    auto_args = ["-s", "-o", "--no-progress"] if AUTOMATION else []
    612    results.append((
    613        "gdb",
    614        run_test_command([PYTHON, test_script, *auto_args, *extra_args["gdb"], OBJDIR]),
    615    ))
    616 
    617 # FIXME bug 1291449: This would be unnecessary if we could run msan with -mllvm
    618 # -msan-keep-going, but in clang 3.8 it causes a hang during compilation.
    619 if variant.get("ignore-test-failures"):
    620    logging.warning("Ignoring test results %s" % (results,))
    621    results = [("ignored", 0)]
    622 
    623 if args.variant == "msan":
    624    files = filter(lambda f: f.startswith("sanitize_log."), os.listdir(OUTDIR))
    625    fullfiles = [os.path.join(OUTDIR, f) for f in files]
    626 
    627    # Summarize results
    628    sites = Counter()
    629    errors = Counter()
    630    for filename in fullfiles:
    631        with open(os.path.join(OUTDIR, filename), "rb") as fh:
    632            for line in fh:
    633                m = re.match(
    634                    r"^SUMMARY: \w+Sanitizer: (?:data race|use-of-uninitialized-value) (.*)",  # NOQA: E501
    635                    line.strip(),
    636                )
    637                if m:
    638                    # Some reports include file:line:column, some just
    639                    # file:line. Just in case it's nondeterministic, we will
    640                    # canonicalize to just the line number.
    641                    site = re.sub(r"^(\S+?:\d+)(:\d+)* ", r"\1 ", m.group(1))
    642                    sites[site] += 1
    643 
    644    # Write a summary file and display it to stdout.
    645    summary_filename = os.path.join(
    646        env["MOZ_UPLOAD_DIR"], "%s_summary.txt" % args.variant
    647    )
    648    with open(summary_filename, "wb") as outfh:
    649        for location, count in sites.most_common():
    650            print >> outfh, "%d %s" % (count, location)
    651    print(open(summary_filename, "rb").read())
    652 
    653    if "max-errors" in variant:
    654        max_allowed = variant["max-errors"]
    655        print("Found %d errors out of %d allowed" % (len(sites), max_allowed))
    656        if len(sites) > max_allowed:
    657            results.append(("too many msan errors", 1))
    658 
    659    # Gather individual results into a tarball. Note that these are
    660    # distinguished only by pid of the JS process running within each test, so
    661    # given the 16-bit limitation of pids, it's totally possible that some of
    662    # these files will be lost due to being overwritten.
    663    command = [
    664        "tar",
    665        "-C",
    666        OUTDIR,
    667        "-zcf",
    668        os.path.join(env["MOZ_UPLOAD_DIR"], "%s.tar.gz" % args.variant),
    669    ]
    670    command += files
    671    subprocess.call(command)
    672 
    673 # Upload dist/bin/js as js.wasm for the WASI build.
    674 if args.variant == "wasi":
    675    command = [
    676        "cp",
    677        os.path.join(OBJDIR, "dist/bin/js"),
    678        os.path.join(env["MOZ_UPLOAD_DIR"], "js.wasm"),
    679    ]
    680    subprocess.call(command)
    681 
    682 # Generate stacks from minidumps.
    683 if use_minidump:
    684    run_mach_command([
    685        "python",
    686        "--virtualenv=build",
    687        os.path.join(DIR.source, "testing/mozbase/mozcrash/mozcrash/mozcrash.py"),
    688        os.getenv("TMPDIR", "/tmp"),
    689        os.path.join(OBJDIR, "dist/crashreporter-symbols"),
    690    ])
    691 
    692 for name, st in results:
    693    print("exit status %d for '%s'" % (st, name))
    694 
    695 # Pick the "worst" exit status. SIGSEGV might give a status of -11, so use the
    696 # maximum absolute value instead of just the maximum.
    697 exit_status = max((st for _, st in results), key=abs)
    698 
    699 # The exit status on Windows can be something like 2147483651 (0x80000003),
    700 # which will be converted to status zero in the caller. Mask off the high bits,
    701 # but if the result is zero then fall back to returning 1.
    702 if exit_status & 0xFF:
    703    sys.exit(exit_status & 0xFF)
    704 else:
    705    sys.exit(1 if exit_status else 0)