tor-browser

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

runxpcshelltests.py (102859B)


      1 #!/usr/bin/env python
      2 #
      3 # This Source Code Form is subject to the terms of the Mozilla Public
      4 # License, v. 2.0. If a copy of the MPL was not distributed with this
      5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      6 
      7 import copy
      8 import json
      9 import os
     10 import platform
     11 import random
     12 import re
     13 import shlex
     14 import shutil
     15 import signal
     16 import subprocess
     17 import sys
     18 import tempfile
     19 import time
     20 import traceback
     21 from argparse import Namespace
     22 from collections import defaultdict, deque, namedtuple
     23 from contextlib import contextmanager
     24 from datetime import datetime, timedelta
     25 from functools import partial
     26 from multiprocessing import cpu_count
     27 from subprocess import PIPE, STDOUT, Popen
     28 from tempfile import gettempdir, mkdtemp
     29 from threading import Event, Thread, Timer, current_thread
     30 
     31 import mozdebug
     32 import six
     33 from mozgeckoprofiler import (
     34    symbolicate_profile_json,
     35    view_gecko_profile,
     36 )
     37 from mozserve import Http3Server
     38 
     39 try:
     40    import psutil
     41 
     42    HAVE_PSUTIL = True
     43 except Exception:
     44    HAVE_PSUTIL = False
     45 
     46 from xpcshellcommandline import parser_desktop
     47 
     48 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
     49 
     50 try:
     51    from mozbuild.base import MozbuildObject
     52 
     53    build = MozbuildObject.from_environment(cwd=SCRIPT_DIR)
     54 except ImportError:
     55    build = None
     56 
     57 HARNESS_TIMEOUT = 30
     58 TBPL_RETRY = 4  # defined in mozharness
     59 
     60 # Based on recent benchmarking on highcpu pools, this value gives the best
     61 # balance between runtime and memory usage
     62 #
     63 # Note:
     64 # - NUM_THREADS defines the maximum number of tests that can run in parallel
     65 # - With e10s/fission enabled, the actual number of underlying processes/threads
     66 #   can be much higher, so memory pressure may vary accordingly
     67 # - For ASan/TSan variants, the thread count is reduced by half to avoid OOM
     68 #
     69 # This value can be overridden via the --threadCount CLI option if adjustments
     70 # are needed for custom CPU/memory configurations
     71 NUM_THREADS = int(cpu_count() * 2.5)
     72 
     73 EXPECTED_LOG_ACTIONS = set([
     74    "crash_reporter_init",
     75    "test_status",
     76    "log",
     77 ])
     78 
     79 # --------------------------------------------------------------
     80 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
     81 #
     82 here = os.path.dirname(__file__)
     83 mozbase = os.path.realpath(os.path.join(os.path.dirname(here), "mozbase"))
     84 
     85 if os.path.isdir(mozbase):
     86    for package in os.listdir(mozbase):
     87        sys.path.append(os.path.join(mozbase, package))
     88 
     89 import mozcrash
     90 import mozfile
     91 import mozinfo
     92 from manifestparser import TestManifest
     93 from manifestparser.expression import parse
     94 from manifestparser.filters import chunk_by_slice, failures, pathprefix, tags
     95 from manifestparser.util import normsep
     96 from mozlog import commandline
     97 from mozprofile import Profile
     98 from mozprofile.cli import parse_key_value, parse_preferences
     99 from mozrunner.utils import get_stack_fixer_function
    100 
    101 # --------------------------------------------------------------
    102 
    103 # TODO: perhaps this should be in a more generally shared location?
    104 # This regex matches all of the C0 and C1 control characters
    105 # (U+0000 through U+001F; U+007F; U+0080 through U+009F),
    106 # except TAB (U+0009), CR (U+000D), LF (U+000A) and backslash (U+005C).
    107 # A raw string is deliberately not used.
    108 _cleanup_encoding_re = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\\\\]")
    109 
    110 
    111 def get_full_group_name(test):
    112    group = test["manifest"]
    113    if "ancestor_manifest" in test:
    114        ancestor_manifest = normsep(test["ancestor_manifest"])
    115        # Only change the group id if ancestor is not the generated root manifest.
    116        if "/" in ancestor_manifest:
    117            group = f"{ancestor_manifest}:{group}"
    118    return group
    119 
    120 
    121 def _cleanup_encoding_repl(m):
    122    c = m.group(0)
    123    return "\\\\" if c == "\\" else f"\\x{ord(c):02X}"
    124 
    125 
    126 def cleanup_encoding(s):
    127    """S is either a byte or unicode string.  Either way it may
    128    contain control characters, unpaired surrogates, reserved code
    129    points, etc.  If it is a byte string, it is assumed to be
    130    UTF-8, but it may not be *correct* UTF-8.  Return a
    131    sanitized unicode object."""
    132    if not isinstance(s, str):
    133        if isinstance(s, bytes):
    134            return six.ensure_str(s)
    135        else:
    136            return str(s)
    137    if isinstance(s, bytes):
    138        s = s.decode("utf-8", "replace")
    139    # Replace all C0 and C1 control characters with \xNN escapes.
    140    return _cleanup_encoding_re.sub(_cleanup_encoding_repl, s)
    141 
    142 
    143 @contextmanager
    144 def popenCleanupHack():
    145    """
    146    Hack to work around https://bugs.python.org/issue37380
    147    The basic idea is that on old versions of Python on Windows,
    148    we need to clear subprocess._cleanup before we call Popen(),
    149    then restore it afterwards.
    150    """
    151    savedCleanup = None
    152    if mozinfo.isWin and sys.version_info[0] == 3 and sys.version_info < (3, 7, 5):
    153        savedCleanup = subprocess._cleanup
    154        subprocess._cleanup = lambda: None
    155    try:
    156        yield
    157    finally:
    158        if savedCleanup:
    159            subprocess._cleanup = savedCleanup
    160 
    161 
    162 """ Control-C handling """
    163 gotSIGINT = False
    164 
    165 
    166 def markGotSIGINT(signum, stackFrame):
    167    global gotSIGINT
    168    gotSIGINT = True
    169 
    170 
    171 class XPCShellTestThread(Thread):
    172    def __init__(
    173        self,
    174        test_object,
    175        retry=None,
    176        verbose=False,
    177        usingTSan=False,
    178        usingCrashReporter=False,
    179        **kwargs,
    180    ):
    181        Thread.__init__(self)
    182        self.daemon = True
    183 
    184        self.test_object = test_object
    185        self.retry = retry
    186        if retry is None:
    187            # Retry in CI, but report results without retry when run locally to
    188            # avoid confusion and ease local debugging.
    189            self.retry = os.environ.get("MOZ_AUTOMATION") is not None
    190        self.verbose = verbose
    191        self.usingTSan = usingTSan
    192        self.usingCrashReporter = usingCrashReporter
    193 
    194        self.appPath = kwargs.get("appPath")
    195        self.xrePath = kwargs.get("xrePath")
    196        self.utility_path = kwargs.get("utility_path")
    197        self.testingModulesDir = kwargs.get("testingModulesDir")
    198        self.debuggerInfo = kwargs.get("debuggerInfo")
    199        self.jsDebuggerInfo = kwargs.get("jsDebuggerInfo")
    200        self.headJSPath = kwargs.get("headJSPath")
    201        self.testharnessdir = kwargs.get("testharnessdir")
    202        self.profileName = kwargs.get("profileName")
    203        self.singleFile = kwargs.get("singleFile")
    204        self.env = copy.deepcopy(kwargs.get("env"))
    205        self.symbolsPath = kwargs.get("symbolsPath")
    206        self.logfiles = kwargs.get("logfiles")
    207        self.app_binary = kwargs.get("app_binary")
    208        self.xpcshell = kwargs.get("xpcshell")
    209        self.xpcsRunArgs = kwargs.get("xpcsRunArgs")
    210        self.failureManifest = kwargs.get("failureManifest")
    211        self.jscovdir = kwargs.get("jscovdir")
    212        self.stack_fixer_function = kwargs.get("stack_fixer_function")
    213        self._rootTempDir = kwargs.get("tempDir")
    214        self.cleanup_dir_list = kwargs.get("cleanup_dir_list")
    215        self.pStdout = kwargs.get("pStdout")
    216        self.pStderr = kwargs.get("pStderr")
    217        self.keep_going = kwargs.get("keep_going")
    218        self.log = kwargs.get("log")
    219        self.app_dir_key = kwargs.get("app_dir_key")
    220        self.interactive = kwargs.get("interactive")
    221        self.rootPrefsFile = kwargs.get("rootPrefsFile")
    222        self.extraPrefs = kwargs.get("extraPrefs")
    223        self.verboseIfFails = kwargs.get("verboseIfFails")
    224        self.headless = kwargs.get("headless")
    225        self.selfTest = kwargs.get("selfTest")
    226        self.runFailures = kwargs.get("runFailures")
    227        self.timeoutAsPass = kwargs.get("timeoutAsPass")
    228        self.crashAsPass = kwargs.get("crashAsPass")
    229        self.conditionedProfileDir = kwargs.get("conditionedProfileDir")
    230        self.profiler = kwargs.get("profiler")
    231        if self.runFailures:
    232            self.retry = False
    233 
    234        # Default the test prefsFile to the rootPrefsFile.
    235        self.prefsFile = self.rootPrefsFile
    236 
    237        # only one of these will be set to 1. adding them to the totals in
    238        # the harness
    239        self.passCount = 0
    240        self.todoCount = 0
    241        self.failCount = 0
    242 
    243        # Context for output processing
    244        self.output_lines = []
    245        self.has_failure_output = False
    246        self.saw_crash_reporter_init = False
    247        self.saw_proc_start = False
    248        self.saw_proc_end = False
    249        self.command = None
    250        self.harness_timeout = kwargs.get("harness_timeout")
    251        self.timedout = False
    252        self.infra = False
    253 
    254        # event from main thread to signal work done
    255        self.event = kwargs.get("event")
    256        self.done = False  # explicitly set flag so we don't rely on thread.isAlive
    257 
    258    def run(self):
    259        try:
    260            self.run_test()
    261        except PermissionError as e:
    262            self.infra = True
    263            self.exception = e
    264            self.traceback = traceback.format_exc()
    265        except Exception as e:
    266            self.exception = e
    267            self.traceback = traceback.format_exc()
    268        else:
    269            self.exception = None
    270            self.traceback = None
    271        if self.retry:
    272            self.log.info(
    273                "%s failed or timed out, will retry." % self.test_object["id"]
    274            )
    275        self.done = True
    276        self.event.set()
    277 
    278    def kill(self, proc):
    279        """
    280        Simple wrapper to kill a process.
    281        On a remote system, this is overloaded to handle remote process communication.
    282        """
    283        return proc.kill()
    284 
    285    def removeDir(self, dirname):
    286        """
    287        Simple wrapper to remove (recursively) a given directory.
    288        On a remote system, we need to overload this to work on the remote filesystem.
    289        """
    290        mozfile.remove(dirname)
    291 
    292    def poll(self, proc):
    293        """
    294        Simple wrapper to check if a process has terminated.
    295        On a remote system, this is overloaded to handle remote process communication.
    296        """
    297        return proc.poll()
    298 
    299    def createLogFile(self, test_file, stdout):
    300        """
    301        For a given test file and stdout buffer, create a log file.
    302        On a remote system we have to fix the test name since it can contain directories.
    303        """
    304        with open(test_file + ".log", "w") as f:
    305            f.write(stdout)
    306 
    307    def getReturnCode(self, proc):
    308        """
    309        Simple wrapper to get the return code for a given process.
    310        On a remote system we overload this to work with the remote process management.
    311        """
    312        if proc is not None and hasattr(proc, "returncode"):
    313            return proc.returncode
    314        return -1
    315 
    316    def communicate(self, proc):
    317        """
    318        Simple wrapper to communicate with a process.
    319        On a remote system, this is overloaded to handle remote process communication.
    320        """
    321        # Processing of incremental output put here to
    322        # sidestep issues on remote platforms, where what we know
    323        # as proc is a file pulled off of a device.
    324        if proc.stdout:
    325            while True:
    326                line = proc.stdout.readline()
    327                if not line:
    328                    break
    329                self.process_line(line)
    330 
    331            if self.saw_proc_start and not self.saw_proc_end:
    332                self.has_failure_output = True
    333 
    334        return proc.communicate()
    335 
    336    def launchProcess(
    337        self, cmd, stdout, stderr, env, cwd, timeout=None, test_name=None
    338    ):
    339        """
    340        Simple wrapper to launch a process.
    341        On a remote system, this is more complex and we need to overload this function.
    342        """
    343        # timeout is needed by remote xpcshell to extend the
    344        # remote device timeout. It is not used in this function.
    345        if six.PY3:
    346            cwd = six.ensure_str(cwd)
    347            for i in range(len(cmd)):
    348                cmd[i] = six.ensure_str(cmd[i])
    349 
    350        if HAVE_PSUTIL:
    351            popen_func = psutil.Popen
    352        else:
    353            popen_func = Popen
    354 
    355        with popenCleanupHack():
    356            proc = popen_func(cmd, stdout=stdout, stderr=stderr, env=env, cwd=cwd)
    357 
    358        return proc
    359 
    360    def checkForCrashes(self, dump_directory, symbols_path, test_name=None):
    361        """
    362        Simple wrapper to check for crashes.
    363        On a remote system, this is more complex and we need to overload this function.
    364        """
    365        quiet = self.crashAsPass or self.retry
    366        # For selftests, set dump_save_path to prevent crash dumps from being saved
    367        # (they intentionally crash and the dumps aren't useful artifacts)
    368        dump_save_path = "" if self.selfTest else None
    369        return mozcrash.log_crashes(
    370            self.log,
    371            dump_directory,
    372            symbols_path,
    373            test=test_name,
    374            quiet=quiet,
    375            dump_save_path=dump_save_path,
    376        )
    377 
    378    def logCommand(self, name, completeCmd, testdir):
    379        self.log.info("%s | full command: %r" % (name, completeCmd))
    380        self.log.info("%s | current directory: %r" % (name, testdir))
    381        # Show only those environment variables that are changed from
    382        # the ambient environment.
    383        changedEnv = set("%s=%s" % i for i in self.env.items()) - set(
    384            "%s=%s" % i for i in os.environ.items()
    385        )
    386        self.log.info("%s | environment: %s" % (name, list(changedEnv)))
    387        shell_command_tokens = [
    388            shlex.quote(tok) for tok in list(changedEnv) + completeCmd
    389        ]
    390        self.log.info(
    391            "%s | as shell command: (cd %s; %s)"
    392            % (name, shlex.quote(testdir), " ".join(shell_command_tokens))
    393        )
    394 
    395    def killTimeout(self, proc):
    396        if proc is not None and hasattr(proc, "pid"):
    397            mozcrash.kill_and_get_minidump(
    398                proc.pid, self.tempDir, utility_path=self.utility_path
    399            )
    400        else:
    401            self.log.info("not killing -- proc or pid unknown")
    402 
    403    def postCheck(self, proc):
    404        """Checks for a still-running test process, kills it and fails the test if found.
    405        We can sometimes get here before the process has terminated, which would
    406        cause removeDir() to fail - so check for the process and kill it if needed.
    407        """
    408        if proc and self.poll(proc) is None:
    409            if HAVE_PSUTIL:
    410                try:
    411                    self.kill(proc)
    412                except psutil.NoSuchProcess:
    413                    pass
    414            else:
    415                self.kill(proc)
    416            message = "%s | Process still running after test!" % self.test_object["id"]
    417            if self.retry:
    418                self.log.info(message)
    419                return
    420 
    421            self.log.error(message)
    422            self.log_full_output()
    423            self.failCount = 1
    424 
    425    def testTimeout(self, proc):
    426        # Set these flags first to prevent test_end from being logged again
    427        # while we output the full log.
    428        self.done = True
    429        self.timedout = True
    430 
    431        # Kill the test process before calling log_full_output that can take a
    432        # a while due to stack fixing.
    433        self.killTimeout(proc)
    434 
    435        if self.test_object["expected"] == "pass":
    436            expected = "PASS"
    437        else:
    438            expected = "FAIL"
    439 
    440        extra = None
    441        if self.timeout_factor > 1:
    442            extra = {"timeoutfactor": self.timeout_factor}
    443 
    444        if self.retry:
    445            self.log.test_end(
    446                self.test_object["id"],
    447                "TIMEOUT",
    448                expected="TIMEOUT",
    449                message="Test timed out",
    450                extra=extra,
    451            )
    452            self.log_full_output(mark_failures_as_expected=True)
    453        else:
    454            result = "TIMEOUT"
    455            if self.timeoutAsPass:
    456                expected = "FAIL"
    457                result = "FAIL"
    458            self.failCount = 1
    459            self.log.test_end(
    460                self.test_object["id"],
    461                result,
    462                expected=expected,
    463                message="Test timed out",
    464                extra=extra,
    465            )
    466            self.log_full_output()
    467 
    468        self.log.info("xpcshell return code: %s" % self.getReturnCode(proc))
    469        self.postCheck(proc)
    470        self.clean_temp_dirs(self.test_object["path"])
    471 
    472    def updateTestPrefsFile(self):
    473        # If the Manifest file has some additional prefs, merge the
    474        # prefs set in the user.js file stored in the _rootTempdir
    475        # with the prefs from the manifest and the prefs specified
    476        # in the extraPrefs option.
    477        if "prefs" in self.test_object:
    478            # Merge the user preferences in a fake profile dir in a
    479            # local temporary dir (self.tempDir is the remoteTmpDir
    480            # for the RemoteXPCShellTestThread subclass and so we
    481            # can't use that tempDir here).
    482            localTempDir = mkdtemp(prefix="xpc-other-", dir=self._rootTempDir)
    483 
    484            filename = "user.js"
    485            interpolation = {"server": "dummyserver"}
    486            profile = Profile(profile=localTempDir, restore=False)
    487            # _rootTempDir contains a user.js file, generated by buildPrefsFile
    488            profile.merge(self._rootTempDir, interpolation=interpolation)
    489 
    490            prefs = self.test_object["prefs"].strip()
    491            if prefs:
    492                prefs = [p.strip() for p in prefs.split("\n")]
    493            name = self.test_object["id"]
    494            if self.verbose:
    495                self.log.info(
    496                    "%s: Per-test extra prefs will be set:\n  {}".format(
    497                        "\n  ".join(prefs)
    498                    )
    499                    % name
    500                )
    501 
    502            profile.set_preferences(parse_preferences(prefs), filename=filename)
    503            # Make sure that the extra prefs form the command line are overriding
    504            # any prefs inherited from the shared profile data or the manifest prefs.
    505            profile.set_preferences(
    506                parse_preferences(self.extraPrefs), filename=filename
    507            )
    508            return os.path.join(profile.profile, filename)
    509 
    510        # Return the root prefsFile if there is no other prefs to merge.
    511        # This is the path set by buildPrefsFile.
    512        return self.rootPrefsFile
    513 
    514    def updateTestEnvironment(self):
    515        # Add additional environment variables from the Manifest file
    516        extraEnv = {}
    517        if "environment" in self.test_object:
    518            extraEnv = self.test_object["environment"].strip().split()
    519            self.log.info(
    520                "The following extra environment variables will be set:\n  {}".format(
    521                    "\n  ".join(extraEnv)
    522                )
    523            )
    524            self.env.update(
    525                dict(
    526                    parse_key_value(
    527                        extraEnv, context="environment variables in manifest"
    528                    )
    529                )
    530            )
    531 
    532    @property
    533    def conditioned_profile_copy(self):
    534        """Returns a copy of the original conditioned profile that was created."""
    535 
    536        condprof_copy = os.path.join(tempfile.mkdtemp(), "profile")
    537        shutil.copytree(
    538            self.conditionedProfileDir,
    539            condprof_copy,
    540            ignore=shutil.ignore_patterns("lock"),
    541        )
    542        self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
    543        return condprof_copy
    544 
    545    def buildCmdTestFile(self, name):
    546        """
    547        Build the command line arguments for the test file.
    548        On a remote system, this may be overloaded to use a remote path structure.
    549        """
    550        return ["-e", 'const _TEST_FILE = ["%s"];' % name.replace("\\", "/")]
    551 
    552    def setupTempDir(self):
    553        tempDir = mkdtemp(prefix="xpc-other-", dir=self._rootTempDir)
    554        self.env["XPCSHELL_TEST_TEMP_DIR"] = tempDir
    555        if self.interactive:
    556            self.log.info("temp dir is %s" % tempDir)
    557        return tempDir
    558 
    559    def setupProfileDir(self):
    560        """
    561        Create a temporary folder for the profile and set appropriate environment variables.
    562        When running check-interactive and check-one, the directory is well-defined and
    563        retained for inspection once the tests complete.
    564 
    565        On a remote system, this may be overloaded to use a remote path structure.
    566        """
    567        if self.conditionedProfileDir:
    568            profileDir = self.conditioned_profile_copy
    569        elif self.interactive or (self.singleFile and not self.selfTest):
    570            profileDir = os.path.join(gettempdir(), self.profileName, "xpcshellprofile")
    571            try:
    572                # This could be left over from previous runs
    573                self.removeDir(profileDir)
    574            except Exception:
    575                pass
    576            os.makedirs(profileDir)
    577        else:
    578            profileDir = mkdtemp(prefix="xpc-profile-", dir=self._rootTempDir)
    579        self.env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir
    580        if self.interactive or self.singleFile:
    581            self.log.info("profile dir is %s" % profileDir)
    582        return profileDir
    583 
    584    def setupMozinfoJS(self):
    585        mozInfoJSPath = os.path.join(self.profileDir, "mozinfo.json")
    586        mozInfoJSPath = mozInfoJSPath.replace("\\", "\\\\")
    587        mozinfo.output_to_file(mozInfoJSPath)
    588        return mozInfoJSPath
    589 
    590    def buildCmdHead(self):
    591        """
    592        Build the command line arguments for the head files,
    593        along with the address of the webserver which some tests require.
    594 
    595        On a remote system, this is overloaded to resolve quoting issues over a
    596        secondary command line.
    597        """
    598        headfiles = self.getHeadFiles(self.test_object)
    599        cmdH = ", ".join(['"' + f.replace("\\", "/") + '"' for f in headfiles])
    600 
    601        dbgport = 0 if self.jsDebuggerInfo is None else self.jsDebuggerInfo.port
    602 
    603        return [
    604            "-e",
    605            "const _HEAD_FILES = [%s];" % cmdH,
    606            "-e",
    607            "const _JSDEBUGGER_PORT = %d;" % dbgport,
    608        ]
    609 
    610    def getHeadFiles(self, test):
    611        """Obtain lists of head- files.  Returns a list of head files."""
    612 
    613        def sanitize_list(s, kind):
    614            for f in s.strip().split(" "):
    615                f = f.strip()
    616                if len(f) < 1:
    617                    continue
    618 
    619                path = os.path.normpath(os.path.join(test["here"], f))
    620                if not os.path.exists(path):
    621                    raise Exception("%s file does not exist: %s" % (kind, path))
    622 
    623                if not os.path.isfile(path):
    624                    raise Exception("%s file is not a file: %s" % (kind, path))
    625 
    626                yield path
    627 
    628        headlist = test.get("head", "")
    629        return list(sanitize_list(headlist, "head"))
    630 
    631    def buildXpcsCmd(self):
    632        """
    633        Load the root head.js file as the first file in our test path, before other head,
    634        and test files. On a remote system, we overload this to add additional command
    635        line arguments, so this gets overloaded.
    636        """
    637        # - NOTE: if you rename/add any of the constants set here, update
    638        #   do_load_child_test_harness() in head.js
    639        if not self.appPath:
    640            self.appPath = self.xrePath
    641 
    642        if self.app_binary:
    643            xpcsCmd = [
    644                self.app_binary,
    645                "--xpcshell",
    646            ]
    647        else:
    648            xpcsCmd = [
    649                self.xpcshell,
    650            ]
    651 
    652        xpcsCmd += [
    653            "-g",
    654            self.xrePath,
    655            "-a",
    656            self.appPath,
    657            "-m",
    658            "-e",
    659            'const _HEAD_JS_PATH = "%s";' % self.headJSPath,
    660            "-e",
    661            'const _MOZINFO_JS_PATH = "%s";' % self.mozInfoJSPath,
    662            "-e",
    663            'const _PREFS_FILE = "%s";' % self.prefsFile.replace("\\", "\\\\"),
    664        ]
    665 
    666        if self.testingModulesDir:
    667            # Escape backslashes in string literal.
    668            sanitized = self.testingModulesDir.replace("\\", "\\\\")
    669            xpcsCmd.extend(["-e", 'const _TESTING_MODULES_DIR = "%s";' % sanitized])
    670 
    671        xpcsCmd.extend(["-f", os.path.join(self.testharnessdir, "head.js")])
    672 
    673        if self.debuggerInfo:
    674            xpcsCmd = [self.debuggerInfo.path] + self.debuggerInfo.args + xpcsCmd
    675 
    676        return xpcsCmd
    677 
    678    def cleanupDir(self, directory, name):
    679        if not os.path.exists(directory):
    680            return
    681 
    682        # up to TRY_LIMIT attempts (one every second), because
    683        # the Windows filesystem is slow to react to the changes
    684        TRY_LIMIT = 25
    685        try_count = 0
    686        while try_count < TRY_LIMIT:
    687            try:
    688                self.removeDir(directory)
    689            except OSError:
    690                self.log.info("Failed to remove directory: %s. Waiting." % directory)
    691                # We suspect the filesystem may still be making changes. Wait a
    692                # little bit and try again.
    693                time.sleep(1)
    694                try_count += 1
    695            else:
    696                # removed fine
    697                return
    698 
    699        # we try cleaning up again later at the end of the run
    700        self.cleanup_dir_list.append(directory)
    701 
    702    def clean_temp_dirs(self, name):
    703        # We don't want to delete the profile when running check-interactive
    704        # or check-one.
    705        if self.profileDir and not self.interactive and not self.singleFile:
    706            self.cleanupDir(self.profileDir, name)
    707 
    708        self.cleanupDir(self.tempDir, name)
    709 
    710    def parse_output(self, output):
    711        """Parses process output for structured messages and saves output as it is
    712        read. Sets self.has_failure_output in case of evidence of a failure"""
    713        for line_string in output.splitlines():
    714            self.process_line(line_string)
    715 
    716        if self.saw_proc_start and not self.saw_proc_end:
    717            self.has_failure_output = True
    718 
    719    def fix_text_output(self, line):
    720        line = cleanup_encoding(line)
    721        if self.stack_fixer_function is not None:
    722            line = self.stack_fixer_function(line)
    723 
    724        if isinstance(line, bytes):
    725            line = line.decode("utf-8")
    726        return line
    727 
    728    def log_line(self, line, time=None):
    729        """Log a line of output (either a parser json object or text output from
    730        the test process"""
    731        if isinstance(line, (str, bytes)):
    732            line = self.fix_text_output(line).rstrip("\r\n")
    733            kwargs = {"command": self.command, "test": self.test_object["id"]}
    734            if time is not None:
    735                kwargs["time"] = time
    736            self.log.process_output(self.proc_ident, line, **kwargs)
    737        else:
    738            if "message" in line:
    739                line["message"] = self.fix_text_output(line["message"])
    740            if "xpcshell_process" in line:
    741                line["thread"] = " ".join([
    742                    current_thread().name,
    743                    line["xpcshell_process"],
    744                ])
    745            else:
    746                line["thread"] = current_thread().name
    747            self.log.log_raw(line)
    748 
    749    def log_full_output(self, mark_failures_as_expected=False):
    750        """Logs any buffered output from the test process, and clears the buffer.
    751 
    752        Args:
    753            mark_failures_as_expected: If True, failures will be marked as expected
    754                (TEST-EXPECTED-FAIL instead of TEST-UNEXPECTED-FAIL). This is used
    755                when a test will be retried.
    756        """
    757        if not self.output_lines:
    758            return
    759        log_message = f"full log for {self.test_object['id']}"
    760        self.log.info(f">>>>>>> Begin of {log_message}")
    761        self.log.group_start("replaying " + log_message)
    762        for timestamp, line in self.output_lines:
    763            if isinstance(line, dict):
    764                # Always ensure the 'test' field is present for resource usage profiles
    765                if "test" not in line:
    766                    line["test"] = self.test_object["id"]
    767 
    768                if mark_failures_as_expected:
    769                    if line.get("action") == "test_status" and "expected" in line:
    770                        # Ensure the 'expected' field matches the 'status' to avoid failing the job
    771                        line["expected"] = line.get("status")
    772                    elif line.get("action") == "log" and line.get("level") == "ERROR":
    773                        # Convert ERROR log to test_status so it gets colored
    774                        # properly without causing a test failure.
    775                        line["action"] = "test_status"
    776                        line["status"] = "ERROR"
    777                        line["expected"] = "ERROR"
    778                        line["subtest"] = ""
    779                        del line["level"]
    780                self.log_line(line)
    781            else:
    782                # For text lines, replace text matching error patterns to avoid
    783                # mozharness log parsing forcing an error job exit code
    784                line = re.sub(
    785                    r"ERROR: ((Address|Leak)Sanitizer)", r"ERROR (will retry): \1", line
    786                )
    787                # Treeherder's log parser catches "fatal error" as an error
    788                line = re.sub(r"fatal error", r"error", line)
    789                # For text lines, we need to provide the timestamp that was
    790                # recorded when appending the message to self.output_lines
    791                self.log_line(line, time=timestamp)
    792        self.log.info(f"<<<<<<< End of {log_message}")
    793        self.log.group_end("replaying " + log_message)
    794        self.output_lines = []
    795 
    796    def report_message(self, message):
    797        """Stores or logs a json log message in mozlog format."""
    798        if self.verbose:
    799            self.log_line(message)
    800        else:
    801            # Store timestamp only for string messages (dicts already have timestamps)
    802            # We need valid timestamps to replay messages correctly in resource
    803            # usage profiles.
    804            import time
    805 
    806            timestamp = (
    807                int(time.time() * 1000) if isinstance(message, (str, bytes)) else None
    808            )
    809            self.output_lines.append((timestamp, message))
    810 
    811    def process_line(self, line_string):
    812        """Parses a single line of output, determining its significance and
    813        reporting a message.
    814        """
    815        if isinstance(line_string, bytes):
    816            # Transform binary to string representation
    817            line_string = line_string.decode(sys.stdout.encoding, errors="replace")
    818 
    819        if not line_string.strip():
    820            return
    821 
    822        try:
    823            line_object = json.loads(line_string)
    824            if not isinstance(line_object, dict):
    825                self.report_message(line_string)
    826                return
    827        except ValueError:
    828            self.report_message(line_string)
    829            return
    830 
    831        if (
    832            "action" not in line_object
    833            or line_object["action"] not in EXPECTED_LOG_ACTIONS
    834        ):
    835            # The test process output JSON.
    836            self.report_message(line_string)
    837            return
    838 
    839        if line_object["action"] == "crash_reporter_init":
    840            self.saw_crash_reporter_init = True
    841            return
    842 
    843        action = line_object["action"]
    844 
    845        self.has_failure_output = (
    846            self.has_failure_output
    847            or "expected" in line_object
    848            or action == "log"
    849            and line_object["level"] == "ERROR"
    850        )
    851 
    852        self.report_message(line_object)
    853 
    854        if action == "log" and line_object["message"] == "CHILD-TEST-STARTED":
    855            self.saw_proc_start = True
    856        elif action == "log" and line_object["message"] == "CHILD-TEST-COMPLETED":
    857            self.saw_proc_end = True
    858 
    859    def run_test(self):
    860        """Run an individual xpcshell test."""
    861        global gotSIGINT
    862 
    863        name = self.test_object["id"]
    864        path = self.test_object["path"]
    865        group = get_full_group_name(self.test_object)
    866 
    867        # Check for skipped tests
    868        if "disabled" in self.test_object:
    869            message = self.test_object["disabled"]
    870            if not message:
    871                message = "disabled from xpcshell manifest"
    872            self.log.test_start(name, group=group)
    873            self.log.test_end(name, "SKIP", message=message, group=group)
    874 
    875            self.retry = False
    876            self.keep_going = True
    877            return
    878 
    879        # Check for known-fail tests
    880        expect_pass = self.test_object["expected"] == "pass"
    881 
    882        # By default self.appPath will equal the gre dir. If specified in the
    883        # xpcshell.toml file, set a different app dir for this test.
    884        if self.app_dir_key and self.app_dir_key in self.test_object:
    885            rel_app_dir = self.test_object[self.app_dir_key]
    886            rel_app_dir = os.path.join(self.xrePath, rel_app_dir)
    887            self.appPath = os.path.abspath(rel_app_dir)
    888        else:
    889            self.appPath = None
    890 
    891        test_dir = os.path.dirname(path)
    892 
    893        # Create a profile and a temp dir that the JS harness can stick
    894        # a profile and temporary data in
    895        self.profileDir = self.setupProfileDir()
    896        self.tempDir = self.setupTempDir()
    897        self.mozInfoJSPath = self.setupMozinfoJS()
    898 
    899        # Setup per-manifest prefs and write them into the tempdir.
    900        self.prefsFile = self.updateTestPrefsFile()
    901 
    902        # Setup per-manifest env variables
    903        self.updateTestEnvironment()
    904 
    905        # The order of the command line is important:
    906        # 1) Arguments for xpcshell itself
    907        self.command = self.buildXpcsCmd()
    908 
    909        # 2) Arguments for the head files
    910        self.command.extend(self.buildCmdHead())
    911 
    912        # 3) Arguments for the test file
    913        self.command.extend(self.buildCmdTestFile(path))
    914        self.command.extend(["-e", 'const _TEST_NAME = "%s";' % name])
    915        self.command.extend([
    916            "-e",
    917            'const _EXPECTED = "%s";' % self.test_object["expected"],
    918        ])
    919 
    920        # 4) Arguments for code coverage
    921        if self.jscovdir:
    922            self.command.extend([
    923                "-e",
    924                'const _JSCOV_DIR = "%s";' % self.jscovdir.replace("\\", "/"),
    925            ])
    926 
    927        # 5) Runtime arguments
    928        if "debug" in self.test_object:
    929            self.command.append("-d")
    930 
    931        self.command.extend(self.xpcsRunArgs)
    932 
    933        if self.test_object.get("dmd") == "true":
    934            self.env["PYTHON"] = sys.executable
    935            self.env["BREAKPAD_SYMBOLS_PATH"] = self.symbolsPath
    936 
    937        if self.test_object.get("subprocess") == "true":
    938            self.env["PYTHON"] = sys.executable
    939 
    940        if self.profiler:
    941            if not self.singleFile:
    942                self.log.error(
    943                    "The --profiler flag is currently only supported when running a single test"
    944                )
    945            else:
    946                self.env["MOZ_PROFILER_STARTUP"] = "1"
    947                profile_path = os.path.join(
    948                    self.profileDir, "profile_" + os.path.basename(name) + ".json"
    949                )
    950                self.env["MOZ_PROFILER_SHUTDOWN"] = profile_path
    951 
    952        if (
    953            self.test_object.get("headless", "true" if self.headless else None)
    954            == "true"
    955        ):
    956            self.env["MOZ_HEADLESS"] = "1"
    957            self.env["DISPLAY"] = "77"  # Set a fake display.
    958 
    959        # Allow a test to request a multiple of the timeout if it is expected to take long
    960        self.timeout_factor = 1
    961        if "requesttimeoutfactor" in self.test_object:
    962            self.timeout_factor = int(self.test_object["requesttimeoutfactor"])
    963 
    964        testTimeoutInterval = self.harness_timeout * self.timeout_factor
    965 
    966        testTimer = None
    967        if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo:
    968            testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc))
    969            testTimer.start()
    970            self.env["MOZ_TEST_TIMEOUT_INTERVAL"] = str(testTimeoutInterval)
    971 
    972        proc = None
    973        process_output = None
    974 
    975        try:
    976            self.log.test_start(name, group=group)
    977            if self.verbose:
    978                self.logCommand(name, self.command, test_dir)
    979 
    980            proc = self.launchProcess(
    981                self.command,
    982                stdout=self.pStdout,
    983                stderr=self.pStderr,
    984                env=self.env,
    985                cwd=test_dir,
    986                timeout=testTimeoutInterval,
    987                test_name=name,
    988            )
    989 
    990            if hasattr(proc, "pid"):
    991                self.proc_ident = proc.pid
    992            else:
    993                # On mobile, "proc" is just a file.
    994                self.proc_ident = name
    995 
    996            if self.interactive:
    997                self.log.info("%s | Process ID: %d" % (name, self.proc_ident))
    998 
    999            # Communicate returns a tuple of (stdout, stderr), however we always
   1000            # redirect stderr to stdout, so the second element is ignored.
   1001            process_output, _ = self.communicate(proc)
   1002 
   1003            if self.interactive:
   1004                # Not sure what else to do here...
   1005                self.keep_going = True
   1006                return
   1007 
   1008            if testTimer:
   1009                testTimer.cancel()
   1010 
   1011            if process_output:
   1012                # For the remote case, stdout is not yet depleted, so we parse
   1013                # it here all at once.
   1014                self.parse_output(process_output)
   1015 
   1016            return_code = self.getReturnCode(proc)
   1017 
   1018            # TSan'd processes return 66 if races are detected.  This isn't
   1019            # good in the sense that there's no way to distinguish between
   1020            # a process that would normally have returned zero but has races,
   1021            # and a race-free process that returns 66.  But I don't see how
   1022            # to do better.  This ambiguity is at least constrained to the
   1023            # with-TSan case.  It doesn't affect normal builds.
   1024            #
   1025            # This also assumes that the magic value 66 isn't overridden by
   1026            # a TSAN_OPTIONS=exitcode=<number> environment variable setting.
   1027            #
   1028            TSAN_EXIT_CODE_WITH_RACES = 66
   1029 
   1030            return_code_ok = return_code == 0 or (
   1031                self.usingTSan and return_code == TSAN_EXIT_CODE_WITH_RACES
   1032            )
   1033 
   1034            # Due to the limitation on the remote xpcshell test, the process
   1035            # return code does not represent the process crash.
   1036            # If crash_reporter_init log has not been seen and the return code
   1037            # is 0, it means the process crashed before setting up the crash
   1038            # reporter.
   1039            #
   1040            # NOTE: Crash reporter is not enabled on some configuration, such
   1041            #       as ASAN and TSAN. Those configuration shouldn't be using
   1042            #       remote xpcshell test, and the crash should be caught by
   1043            #       the process return code.
   1044            # NOTE: self.saw_crash_reporter_init is False also when adb failed
   1045            #       to launch process, and in that case the return code is
   1046            #       not 0.
   1047            #       (see launchProcess in remotexpcshelltests.py)
   1048            ended_before_crash_reporter_init = (
   1049                return_code_ok
   1050                and self.usingCrashReporter
   1051                and not self.saw_crash_reporter_init
   1052                and len(process_output) > 0
   1053            )
   1054 
   1055            passed = (
   1056                (not self.has_failure_output)
   1057                and not ended_before_crash_reporter_init
   1058                and return_code_ok
   1059            )
   1060 
   1061            status = "PASS" if passed else "FAIL"
   1062            expected = "PASS" if expect_pass else "FAIL"
   1063            message = "xpcshell return code: %d" % return_code
   1064 
   1065            if self.timedout:
   1066                return
   1067 
   1068            # Check for crashes before logging test_end
   1069            found_crash = self.checkForCrashes(
   1070                self.tempDir, self.symbolsPath, test_name=name
   1071            )
   1072            if found_crash:
   1073                status = "CRASH"
   1074                message = "Test crashed"
   1075 
   1076            # Include timeout factor in extra data if not default
   1077            extra = None
   1078            if self.timeout_factor > 1:
   1079                extra = {"timeoutfactor": self.timeout_factor}
   1080 
   1081            if status != expected or ended_before_crash_reporter_init:
   1082                if ended_before_crash_reporter_init:
   1083                    self.log.test_end(
   1084                        name,
   1085                        "CRASH",
   1086                        expected=expected,
   1087                        message="Test ended before setting up the crash reporter",
   1088                        group=group,
   1089                        extra=extra,
   1090                    )
   1091                elif self.retry:
   1092                    retry_message = (
   1093                        "Test crashed, will retry"
   1094                        if status == "CRASH"
   1095                        else "Test failed or timed out, will retry"
   1096                    )
   1097                    self.log.test_end(
   1098                        name,
   1099                        status,
   1100                        expected=status,
   1101                        message=retry_message,
   1102                        group=group,
   1103                        extra=extra,
   1104                    )
   1105                    self.clean_temp_dirs(path)
   1106                    if self.verboseIfFails and not self.verbose:
   1107                        self.log_full_output()
   1108                    return
   1109                else:
   1110                    self.log.test_end(
   1111                        name,
   1112                        status,
   1113                        expected=expected,
   1114                        message=message,
   1115                        group=group,
   1116                        extra=extra,
   1117                    )
   1118                self.log_full_output()
   1119 
   1120                self.failCount += 1
   1121 
   1122                if self.failureManifest:
   1123                    with open(self.failureManifest, "a") as f:
   1124                        f.write("[%s]\n" % self.test_object["path"])
   1125                        for k, v in self.test_object.items():
   1126                            f.write("%s = %s\n" % (k, v))
   1127 
   1128            else:
   1129                # If TSan reports a race, dump the output, else we can't
   1130                # diagnose what the problem was.  See comments above about
   1131                # the significance of TSAN_EXIT_CODE_WITH_RACES.
   1132                if self.usingTSan and return_code == TSAN_EXIT_CODE_WITH_RACES:
   1133                    self.log_full_output()
   1134 
   1135                self.log.test_end(
   1136                    name,
   1137                    status,
   1138                    expected=expected,
   1139                    message=message,
   1140                    group=group,
   1141                    extra=extra,
   1142                )
   1143                if self.verbose:
   1144                    self.log_full_output()
   1145 
   1146                self.retry = False
   1147 
   1148                if expect_pass:
   1149                    self.passCount = 1
   1150                else:
   1151                    self.todoCount = 1
   1152 
   1153            if self.logfiles and process_output:
   1154                self.createLogFile(name, process_output)
   1155 
   1156        finally:
   1157            self.postCheck(proc)
   1158            if self.profiler and self.singleFile:
   1159                symbolicate_profile_json(profile_path, self.symbolsPath)
   1160                view_gecko_profile(profile_path)
   1161            self.clean_temp_dirs(path)
   1162 
   1163        if gotSIGINT:
   1164            self.log.error("Received SIGINT (control-C) during test execution")
   1165            if self.keep_going:
   1166                gotSIGINT = False
   1167            else:
   1168                self.keep_going = False
   1169                return
   1170 
   1171        self.keep_going = True
   1172 
   1173 
   1174 class XPCShellTests:
   1175    def __init__(self, log=None):
   1176        """Initializes node status and logger."""
   1177        self.log = log
   1178        self.harness_timeout = HARNESS_TIMEOUT
   1179        self.nodeProc = {}
   1180        self.http3Server = None
   1181        self.conditioned_profile_dir = None
   1182 
   1183    def getTestManifest(self, manifest):
   1184        if isinstance(manifest, TestManifest):
   1185            return manifest
   1186        elif manifest is not None:
   1187            manifest = os.path.normpath(os.path.abspath(manifest))
   1188            if os.path.isfile(manifest):
   1189                return TestManifest([manifest], strict=True)
   1190            else:
   1191                toml_path = os.path.join(manifest, "xpcshell.toml")
   1192        else:
   1193            toml_path = os.path.join(SCRIPT_DIR, "tests", "xpcshell.toml")
   1194 
   1195        if os.path.exists(toml_path):
   1196            return TestManifest([toml_path], strict=True)
   1197        else:
   1198            self.log.error(
   1199                "Failed to find manifest at %s; use --manifest "
   1200                "to set path explicitly." % toml_path
   1201            )
   1202            sys.exit(1)
   1203 
   1204    def normalizeTest(self, root, test_object):
   1205        path = test_object.get("file_relpath", test_object["relpath"])
   1206        if "dupe-manifest" in test_object and "ancestor_manifest" in test_object:
   1207            # Use same logic as get_full_group_name() to determine which manifest to use
   1208            ancestor_manifest = normsep(test_object["ancestor_manifest"])
   1209            # If ancestor is not the generated root (has path separator), use it
   1210            manifest_for_id = (
   1211                test_object["ancestor_manifest"]
   1212                if "/" in ancestor_manifest
   1213                else test_object["manifest"]
   1214            )
   1215            test_object["id"] = "%s:%s" % (os.path.basename(manifest_for_id), path)
   1216        else:
   1217            test_object["id"] = path
   1218 
   1219        if root:
   1220            test_object["manifest"] = os.path.relpath(test_object["manifest"], root)
   1221 
   1222        if os.sep != "/":
   1223            for key in ("id", "manifest"):
   1224                test_object[key] = test_object[key].replace(os.sep, "/")
   1225 
   1226        return test_object
   1227 
   1228    def buildTestList(self, test_tags=None, test_paths=None, verify=False):
   1229        """Reads the xpcshell.toml manifest and set self.alltests to an array.
   1230 
   1231        Given the parameters, this method compiles a list of tests to be run
   1232        that matches the criteria set by parameters.
   1233 
   1234        If any chunking of tests are to occur, it is also done in this method.
   1235 
   1236        If no tests are added to the list of tests to be run, an error
   1237        is logged. A sys.exit() signal is sent to the caller.
   1238 
   1239        Args:
   1240            test_tags (list, optional): list of strings.
   1241            test_paths (list, optional): list of strings derived from the command
   1242                                         line argument provided by user, specifying
   1243                                         tests to be run.
   1244            verify (bool, optional): boolean value.
   1245        """
   1246        if test_paths is None:
   1247            test_paths = []
   1248 
   1249        mp = self.getTestManifest(self.manifest)
   1250 
   1251        root = mp.rootdir
   1252        if build and not root:
   1253            root = build.topsrcdir
   1254        normalize = partial(self.normalizeTest, root)
   1255 
   1256        filters = []
   1257        if test_tags:
   1258            filters.extend([tags(x) for x in test_tags])
   1259 
   1260        path_filter = None
   1261        if test_paths:
   1262            path_filter = pathprefix(test_paths)
   1263            filters.append(path_filter)
   1264 
   1265        noDefaultFilters = False
   1266        if self.runFailures:
   1267            filters.append(failures(self.runFailures))
   1268            noDefaultFilters = True
   1269 
   1270        if self.totalChunks > 1:
   1271            filters.append(chunk_by_slice(self.thisChunk, self.totalChunks))
   1272        try:
   1273            self.alltests = list(
   1274                map(
   1275                    normalize,
   1276                    mp.active_tests(
   1277                        filters=filters,
   1278                        noDefaultFilters=noDefaultFilters,
   1279                        strictExpressions=True,
   1280                        **mozinfo.info,
   1281                    ),
   1282                )
   1283            )
   1284        except TypeError:
   1285            sys.stderr.write("*** offending mozinfo.info: %s\n" % repr(mozinfo.info))
   1286            raise
   1287 
   1288        if path_filter and path_filter.missing:
   1289            self.log.warning(
   1290                "The following path(s) didn't resolve any tests:\n  {}".format(
   1291                    "  \n".join(sorted(path_filter.missing))
   1292                )
   1293            )
   1294 
   1295        if len(self.alltests) == 0:
   1296            if (
   1297                test_paths
   1298                and path_filter.missing == set(test_paths)
   1299                and os.environ.get("MOZ_AUTOMATION") == "1"
   1300            ):
   1301                # This can happen in CI when a manifest doesn't exist due to a
   1302                # build config variable in moz.build traversal. Don't generate
   1303                # an error in this case. Adding a todo count avoids mozharness
   1304                # raising an error.
   1305                self.todoCount += len(path_filter.missing)
   1306            else:
   1307                self.log.error(
   1308                    "no tests to run using specified "
   1309                    f"combination of filters: {mp.fmt_filters()}"
   1310                )
   1311                sys.exit(1)
   1312 
   1313        # Count non-disabled tests for --profiler validation
   1314        enabled_tests = [t for t in self.alltests if "disabled" not in t]
   1315        if len(enabled_tests) == 1 and not verify:
   1316            self.singleFile = os.path.basename(enabled_tests[0]["path"])
   1317        else:
   1318            self.singleFile = None
   1319 
   1320        if self.dump_tests:
   1321            self.dump_tests = os.path.expanduser(self.dump_tests)
   1322            assert os.path.exists(os.path.dirname(self.dump_tests))
   1323            with open(self.dump_tests, "w") as dumpFile:
   1324                dumpFile.write(json.dumps({"active_tests": self.alltests}))
   1325 
   1326            self.log.info("Dumping active_tests to %s file." % self.dump_tests)
   1327            sys.exit()
   1328 
   1329    def setAbsPath(self):
   1330        """
   1331        Set the absolute path for xpcshell and xrepath. These 3 variables
   1332        depend on input from the command line and we need to allow for absolute paths.
   1333        This function is overloaded for a remote solution as os.path* won't work remotely.
   1334        """
   1335        self.testharnessdir = os.path.dirname(os.path.abspath(__file__))
   1336        self.headJSPath = self.testharnessdir.replace("\\", "/") + "/head.js"
   1337        if self.xpcshell is not None:
   1338            self.xpcshell = os.path.abspath(self.xpcshell)
   1339 
   1340        if self.app_binary is not None:
   1341            self.app_binary = os.path.abspath(self.app_binary)
   1342 
   1343        if self.xrePath is None:
   1344            binary_path = self.app_binary or self.xpcshell
   1345            self.xrePath = os.path.dirname(binary_path)
   1346            if mozinfo.isMac:
   1347                # Check if we're run from an OSX app bundle and override
   1348                # self.xrePath if we are.
   1349                appBundlePath = os.path.join(
   1350                    os.path.dirname(os.path.dirname(self.xpcshell)), "Resources"
   1351                )
   1352                if os.path.exists(os.path.join(appBundlePath, "application.ini")):
   1353                    self.xrePath = appBundlePath
   1354        else:
   1355            self.xrePath = os.path.abspath(self.xrePath)
   1356 
   1357        if self.mozInfo is None:
   1358            self.mozInfo = os.path.join(self.testharnessdir, "mozinfo.json")
   1359 
   1360    def buildPrefsFile(self, extraPrefs):
   1361        # Create the prefs.js file
   1362 
   1363        # In test packages used in CI, the profile_data directory is installed
   1364        # in the SCRIPT_DIR.
   1365        profile_data_dir = os.path.join(SCRIPT_DIR, "profile_data")
   1366        # If possible, read profile data from topsrcdir. This prevents us from
   1367        # requiring a re-build to pick up newly added extensions in the
   1368        # <profile>/extensions directory.
   1369        if build:
   1370            path = os.path.join(build.topsrcdir, "testing", "profiles")
   1371            if os.path.isdir(path):
   1372                profile_data_dir = path
   1373        # Still not found? Look for testing/profiles relative to testing/xpcshell.
   1374        if not os.path.isdir(profile_data_dir):
   1375            path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "profiles"))
   1376            if os.path.isdir(path):
   1377                profile_data_dir = path
   1378 
   1379        with open(os.path.join(profile_data_dir, "profiles.json")) as fh:
   1380            base_profiles = json.load(fh)["xpcshell"]
   1381 
   1382        # values to use when interpolating preferences
   1383        interpolation = {
   1384            "server": "dummyserver",
   1385        }
   1386 
   1387        profile = Profile(profile=self.tempDir, restore=False)
   1388        prefsFile = os.path.join(profile.profile, "user.js")
   1389 
   1390        # Empty the user.js file in case the file existed before.
   1391        with open(prefsFile, "w"):
   1392            pass
   1393 
   1394        for name in base_profiles:
   1395            path = os.path.join(profile_data_dir, name)
   1396            profile.merge(path, interpolation=interpolation)
   1397 
   1398        # add command line prefs
   1399        prefs = parse_preferences(extraPrefs)
   1400        profile.set_preferences(prefs)
   1401 
   1402        self.prefsFile = prefsFile
   1403        return prefs
   1404 
   1405    def buildCoreEnvironment(self):
   1406        """
   1407        Add environment variables likely to be used across all platforms, including
   1408        remote systems.
   1409        """
   1410        # Make assertions fatal
   1411        self.env["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
   1412        # Crash reporting interferes with debugging
   1413        if not self.debuggerInfo:
   1414            self.env["MOZ_CRASHREPORTER"] = "1"
   1415        # Don't launch the crash reporter client
   1416        self.env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
   1417        # Don't permit remote connections by default.
   1418        # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
   1419        # enable non-local connections for the purposes of local testing.
   1420        # Don't override the user's choice here.  See bug 1049688.
   1421        self.env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")
   1422        if self.mozInfo.get("topsrcdir") is not None:
   1423            self.env["MOZ_DEVELOPER_REPO_DIR"] = self.mozInfo["topsrcdir"]
   1424        if self.mozInfo.get("topobjdir") is not None:
   1425            self.env["MOZ_DEVELOPER_OBJ_DIR"] = self.mozInfo["topobjdir"]
   1426 
   1427        # Disable the content process sandbox for the xpcshell tests. They
   1428        # currently attempt to do things like bind() sockets, which is not
   1429        # compatible with the sandbox.
   1430        self.env["MOZ_DISABLE_CONTENT_SANDBOX"] = "1"
   1431        if os.getenv("MOZ_FETCHES_DIR", None):
   1432            self.env["MOZ_FETCHES_DIR"] = os.getenv("MOZ_FETCHES_DIR", None)
   1433 
   1434        if self.mozInfo.get("socketprocess_networking"):
   1435            self.env["MOZ_FORCE_USE_SOCKET_PROCESS"] = "1"
   1436        else:
   1437            self.env["MOZ_DISABLE_SOCKET_PROCESS"] = "1"
   1438 
   1439    def buildEnvironment(self):
   1440        """
   1441        Create and returns a dictionary of self.env to include all the appropriate env
   1442        variables and values. On a remote system, we overload this to set different
   1443        values and are missing things like os.environ and PATH.
   1444        """
   1445        self.env = dict(os.environ)
   1446        self.buildCoreEnvironment()
   1447        if sys.platform == "win32":
   1448            self.env["PATH"] = self.env["PATH"] + ";" + self.xrePath
   1449        elif sys.platform in ("os2emx", "os2knix"):
   1450            os.environ["BEGINLIBPATH"] = self.xrePath + ";" + self.env["BEGINLIBPATH"]
   1451            os.environ["LIBPATHSTRICT"] = "T"
   1452        elif sys.platform == "osx" or sys.platform == "darwin":
   1453            self.env["DYLD_LIBRARY_PATH"] = os.path.join(
   1454                os.path.dirname(self.xrePath), "MacOS"
   1455            )
   1456        elif "LD_LIBRARY_PATH" not in self.env or self.env["LD_LIBRARY_PATH"] is None:
   1457            self.env["LD_LIBRARY_PATH"] = self.xrePath
   1458        else:
   1459            self.env["LD_LIBRARY_PATH"] = ":".join([
   1460                self.xrePath,
   1461                self.env["LD_LIBRARY_PATH"],
   1462            ])
   1463 
   1464        usingASan = "asan" in self.mozInfo and self.mozInfo["asan"]
   1465        usingTSan = "tsan" in self.mozInfo and self.mozInfo["tsan"]
   1466        if usingASan or usingTSan:
   1467            # symbolizer support
   1468            if "ASAN_SYMBOLIZER_PATH" in self.env and os.path.isfile(
   1469                self.env["ASAN_SYMBOLIZER_PATH"]
   1470            ):
   1471                llvmsym = self.env["ASAN_SYMBOLIZER_PATH"]
   1472            else:
   1473                llvmsym = os.path.join(
   1474                    self.xrePath, "llvm-symbolizer" + self.mozInfo["bin_suffix"]
   1475                )
   1476            if os.path.isfile(llvmsym):
   1477                if usingASan:
   1478                    self.env["ASAN_SYMBOLIZER_PATH"] = llvmsym
   1479                else:
   1480                    oldTSanOptions = self.env.get("TSAN_OPTIONS", "")
   1481                    self.env["TSAN_OPTIONS"] = (
   1482                        f"external_symbolizer_path={llvmsym} {oldTSanOptions}"
   1483                    )
   1484                self.log.info("runxpcshelltests.py | using symbolizer at %s" % llvmsym)
   1485            else:
   1486                self.log.error(
   1487                    "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | "
   1488                    "Failed to find symbolizer at %s" % llvmsym
   1489                )
   1490 
   1491        return self.env
   1492 
   1493    def getPipes(self):
   1494        """
   1495        Determine the value of the stdout and stderr for the test.
   1496        Return value is a list (pStdout, pStderr).
   1497        """
   1498        if self.interactive:
   1499            pStdout = None
   1500            pStderr = None
   1501        elif self.debuggerInfo and self.debuggerInfo.interactive:
   1502            pStdout = None
   1503            pStderr = None
   1504        else:
   1505            if sys.platform == "os2emx":
   1506                pStdout = None
   1507            else:
   1508                pStdout = PIPE
   1509            pStderr = STDOUT
   1510        return pStdout, pStderr
   1511 
   1512    def verifyDirPath(self, dirname):
   1513        """
   1514        Simple wrapper to get the absolute path for a given directory name.
   1515        On a remote system, we need to overload this to work on the remote filesystem.
   1516        """
   1517        return os.path.abspath(dirname)
   1518 
   1519    def trySetupNode(self):
   1520        """
   1521        Run node for HTTP/2 tests, if available, and updates mozinfo as appropriate.
   1522        """
   1523        if os.getenv("MOZ_ASSUME_NODE_RUNNING", None):
   1524            self.log.info("Assuming required node servers are already running")
   1525            if not os.getenv("MOZHTTP2_PORT", None):
   1526                self.log.warning(
   1527                    "MOZHTTP2_PORT environment variable not set. "
   1528                    "Tests requiring http/2 will fail."
   1529                )
   1530            return
   1531 
   1532        # We try to find the node executable in the path given to us by the user in
   1533        # the MOZ_NODE_PATH environment variable
   1534        nodeBin = os.getenv("MOZ_NODE_PATH", None)
   1535        if not nodeBin and build:
   1536            nodeBin = build.substs.get("NODEJS")
   1537        if not nodeBin:
   1538            self.log.warning(
   1539                "MOZ_NODE_PATH environment variable not set. "
   1540                "Tests requiring http/2 will fail."
   1541            )
   1542            return
   1543 
   1544        if not os.path.exists(nodeBin) or not os.path.isfile(nodeBin):
   1545            error = "node not found at MOZ_NODE_PATH %s" % (nodeBin)
   1546            self.log.error(error)
   1547            raise OSError(error)
   1548 
   1549        self.log.info("Found node at %s" % (nodeBin,))
   1550 
   1551        def read_streams(name, proc, pipe):
   1552            output = "stdout" if pipe == proc.stdout else "stderr"
   1553            for line in iter(pipe.readline, ""):
   1554                self.log.info("node %s [%s] %s" % (name, output, line))
   1555 
   1556        def startServer(name, serverJs):
   1557            if not os.path.exists(serverJs):
   1558                error = "%s not found at %s" % (name, serverJs)
   1559                self.log.error(error)
   1560                raise OSError(error)
   1561 
   1562            # OK, we found our server, let's try to get it running
   1563            self.log.info("Found %s at %s" % (name, serverJs))
   1564            try:
   1565                # We pipe stdin to node because the server will exit when its
   1566                # stdin reaches EOF
   1567                with popenCleanupHack():
   1568                    process = Popen(
   1569                        [nodeBin, serverJs],
   1570                        stdin=PIPE,
   1571                        stdout=PIPE,
   1572                        stderr=PIPE,
   1573                        env=self.env,
   1574                        cwd=os.getcwd(),
   1575                        universal_newlines=True,
   1576                        start_new_session=True,
   1577                    )
   1578                self.nodeProc[name] = process
   1579 
   1580                # Check to make sure the server starts properly by waiting for it to
   1581                # tell us it's started
   1582                msg = process.stdout.readline()
   1583                if "server listening" in msg:
   1584                    searchObj = re.search(
   1585                        r"HTTP2 server listening on ports ([0-9]+),([0-9]+)", msg, 0
   1586                    )
   1587                    if searchObj:
   1588                        self.env["MOZHTTP2_PORT"] = searchObj.group(1)
   1589                        self.env["MOZNODE_EXEC_PORT"] = searchObj.group(2)
   1590                t1 = Thread(
   1591                    target=read_streams,
   1592                    args=(name, process, process.stdout),
   1593                    daemon=True,
   1594                )
   1595                t1.start()
   1596                t2 = Thread(
   1597                    target=read_streams,
   1598                    args=(name, process, process.stderr),
   1599                    daemon=True,
   1600                )
   1601                t2.start()
   1602            except OSError as e:
   1603                # This occurs if the subprocess couldn't be started
   1604                self.log.error("Could not run %s server: %s" % (name, str(e)))
   1605                raise
   1606 
   1607        myDir = os.path.split(os.path.abspath(__file__))[0]
   1608        startServer("moz-http2", os.path.join(myDir, "moz-http2", "moz-http2.js"))
   1609 
   1610    def shutdownNode(self):
   1611        """
   1612        Shut down our node process, if it exists
   1613        """
   1614        for name, proc in self.nodeProc.items():
   1615            self.log.info("Node %s server shutting down ..." % name)
   1616            if proc.poll() is not None:
   1617                self.log.info("Node server %s already dead %s" % (name, proc.poll()))
   1618            elif sys.platform != "win32":
   1619                # Kill process and all its spawned children.
   1620                os.killpg(proc.pid, signal.SIGTERM)
   1621            else:
   1622                proc.terminate()
   1623 
   1624        self.nodeProc = {}
   1625 
   1626    def startHttp3Server(self):
   1627        """
   1628        Start a Http3 test server.
   1629        """
   1630        binSuffix = ""
   1631        if sys.platform == "win32":
   1632            binSuffix = ".exe"
   1633        http3ServerPath = self.http3ServerPath
   1634        serverEnv = self.env.copy()
   1635        if not http3ServerPath:
   1636            if self.mozInfo["buildapp"] == "mobile/android":
   1637                # For android, use binary from host utilities.
   1638                http3ServerPath = os.path.join(self.xrePath, "http3server" + binSuffix)
   1639                serverEnv["LD_LIBRARY_PATH"] = self.xrePath
   1640            elif build:
   1641                http3ServerPath = os.path.join(
   1642                    build.topobjdir, "dist", "bin", "http3server" + binSuffix
   1643                )
   1644            else:
   1645                http3ServerPath = os.path.join(
   1646                    SCRIPT_DIR, "http3server", "http3server" + binSuffix
   1647                )
   1648 
   1649        # Treat missing http3server as a non-fatal error, because tests that do not
   1650        # depend on http3server may work just fine.
   1651        if not os.path.exists(http3ServerPath):
   1652            self.log.error("Cannot find http3server at path %s" % (http3ServerPath))
   1653            return
   1654 
   1655        dbPath = os.path.join(SCRIPT_DIR, "http3server", "http3serverDB")
   1656        if build:
   1657            dbPath = os.path.join(build.topsrcdir, "netwerk", "test", "http3serverDB")
   1658        options = {}
   1659        options["http3ServerPath"] = http3ServerPath
   1660        options["profilePath"] = dbPath
   1661        options["isMochitest"] = False
   1662        options["isWin"] = sys.platform == "win32"
   1663        serverLog = self.env.get("MOZHTTP3_SERVER_LOG")
   1664        if serverLog is not None:
   1665            serverEnv["RUST_LOG"] = serverLog
   1666        self.http3Server = Http3Server(options, serverEnv, self.log)
   1667        self.http3Server.start()
   1668        for key, value in self.http3Server.ports().items():
   1669            self.env[key] = value
   1670        self.env["MOZHTTP3_ECH"] = self.http3Server.echConfig()
   1671        self.env["MOZ_HTTP3_SERVER_PATH"] = http3ServerPath
   1672        self.env["MOZ_HTTP3_CERT_DB_PATH"] = dbPath
   1673 
   1674    def shutdownHttp3Server(self):
   1675        if self.http3Server is None:
   1676            return
   1677        self.http3Server.stop()
   1678        self.http3Server = None
   1679 
   1680    def buildXpcsRunArgs(self):
   1681        """
   1682        Add arguments to run the test or make it interactive.
   1683        """
   1684        if self.interactive:
   1685            self.xpcsRunArgs = [
   1686                "-e",
   1687                'print("To start the test, type |_execute_test();|.");',
   1688                "-i",
   1689            ]
   1690        else:
   1691            self.xpcsRunArgs = ["-e", "_execute_test(); quit(0);"]
   1692 
   1693    def addTestResults(self, test):
   1694        self.passCount += test.passCount
   1695        self.failCount += test.failCount
   1696        self.todoCount += test.todoCount
   1697 
   1698    def updateMozinfo(self, prefs, options):
   1699        # Handle filenames in mozInfo
   1700        if not isinstance(self.mozInfo, dict):
   1701            mozInfoFile = self.mozInfo
   1702            if not os.path.isfile(mozInfoFile):
   1703                self.log.error(
   1704                    "Error: couldn't find mozinfo.json at '%s'. Perhaps you "
   1705                    "need to use --build-info-json?" % mozInfoFile
   1706                )
   1707                return False
   1708            self.mozInfo = json.load(open(mozInfoFile))
   1709 
   1710        # mozinfo.info is used as kwargs.  Some builds are done with
   1711        # an older Python that can't handle Unicode keys in kwargs.
   1712        # All of the keys in question should be ASCII.
   1713        fixedInfo = {}
   1714        for k, v in self.mozInfo.items():
   1715            if isinstance(k, bytes):
   1716                k = k.decode("utf-8")
   1717            fixedInfo[k] = v
   1718        self.mozInfo = fixedInfo
   1719 
   1720        self.mozInfo["fission"] = prefs.get("fission.autostart", True)
   1721        self.mozInfo["sessionHistoryInParent"] = self.mozInfo[
   1722            "fission"
   1723        ] or not prefs.get("fission.disableSessionHistoryInParent", False)
   1724 
   1725        self.mozInfo["verify"] = options.get("verify", False)
   1726 
   1727        self.mozInfo["socketprocess_networking"] = prefs.get(
   1728            "network.http.network_access_on_socket_process.enabled", False
   1729        )
   1730 
   1731        self.mozInfo["inc_origin_init"] = (
   1732            os.environ.get("MOZ_ENABLE_INC_ORIGIN_INIT") == "1"
   1733        )
   1734 
   1735        self.mozInfo["condprof"] = options.get("conditionedProfile", False)
   1736        self.mozInfo["msix"] = options.get("variant", "") == "msix"
   1737 
   1738        self.mozInfo["is_ubuntu"] = "Ubuntu" in platform.version()
   1739 
   1740        # TODO: remove this when crashreporter is fixed on mac via bug 1910777
   1741        if self.mozInfo["os"] == "mac":
   1742            (release, versioninfo, machine) = platform.mac_ver()
   1743            versionNums = release.split(".")[:2]
   1744            os_version = "%s.%s" % (versionNums[0], versionNums[1].ljust(2, "0"))
   1745            if os_version.split(".")[0] in ["14", "15"]:
   1746                self.mozInfo["crashreporter"] = False
   1747 
   1748        # we default to false for e10s on xpcshell
   1749        self.mozInfo["e10s"] = self.mozInfo.get("e10s", False)
   1750 
   1751        mozinfo.update(self.mozInfo)
   1752        return True
   1753 
   1754    @property
   1755    def conditioned_profile_copy(self):
   1756        """Returns a copy of the original conditioned profile that was created."""
   1757        condprof_copy = os.path.join(tempfile.mkdtemp(), "profile")
   1758        shutil.copytree(
   1759            self.conditioned_profile_dir,
   1760            condprof_copy,
   1761            ignore=shutil.ignore_patterns("lock"),
   1762        )
   1763        self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
   1764        return condprof_copy
   1765 
   1766    def downloadConditionedProfile(self, profile_scenario, app):
   1767        from condprof.client import get_profile
   1768        from condprof.util import get_current_platform, get_version
   1769 
   1770        if self.conditioned_profile_dir:
   1771            # We already have a directory, so provide a copy that
   1772            # will get deleted after it's done with
   1773            return self.conditioned_profile_dir
   1774 
   1775        # create a temp file to help ensure uniqueness
   1776        temp_download_dir = tempfile.mkdtemp()
   1777        self.log.info(
   1778            f"Making temp_download_dir from inside get_conditioned_profile {temp_download_dir}"
   1779        )
   1780        # call condprof's client API to yield our platform-specific
   1781        # conditioned-profile binary
   1782        platform = get_current_platform()
   1783        version = None
   1784        if isinstance(app, str):
   1785            version = get_version(app)
   1786 
   1787        if not profile_scenario:
   1788            profile_scenario = "settled"
   1789        try:
   1790            cond_prof_target_dir = get_profile(
   1791                temp_download_dir,
   1792                platform,
   1793                profile_scenario,
   1794                repo="mozilla-central",
   1795                version=version,
   1796                retries=2,
   1797            )
   1798        except Exception:
   1799            if version is None:
   1800                # any other error is a showstopper
   1801                self.log.critical("Could not get the conditioned profile")
   1802                traceback.print_exc()
   1803                raise
   1804            version = None
   1805            try:
   1806                self.log.info("Retrying a profile with no version specified")
   1807                cond_prof_target_dir = get_profile(
   1808                    temp_download_dir,
   1809                    platform,
   1810                    profile_scenario,
   1811                    repo="mozilla-central",
   1812                    version=version,
   1813                )
   1814            except Exception:
   1815                self.log.critical("Could not get the conditioned profile")
   1816                traceback.print_exc()
   1817                raise
   1818 
   1819        # now get the full directory path to our fetched conditioned profile
   1820        self.conditioned_profile_dir = os.path.join(
   1821            temp_download_dir, cond_prof_target_dir
   1822        )
   1823        if not os.path.exists(cond_prof_target_dir):
   1824            self.log.critical(
   1825                f"Can't find target_dir {cond_prof_target_dir}, from get_profile()"
   1826                f"temp_download_dir {temp_download_dir}, platform {platform}, scenario {profile_scenario}"
   1827            )
   1828            raise OSError
   1829 
   1830        self.log.info(
   1831            f"Original self.conditioned_profile_dir is now set: {self.conditioned_profile_dir}"
   1832        )
   1833        return self.conditioned_profile_copy
   1834 
   1835    def runSelfTest(self):
   1836        import unittest
   1837        from concurrent.futures import ThreadPoolExecutor, as_completed
   1838 
   1839        import selftest
   1840 
   1841        this = self
   1842 
   1843        class XPCShellTestsTests(selftest.XPCShellTestsTests):
   1844            def __init__(self, name):
   1845                unittest.TestCase.__init__(self, name)
   1846                self.testing_modules = this.testingModulesDir
   1847                self.xpcshellBin = this.xpcshell
   1848                self.app_binary = this.app_binary
   1849                self.utility_path = this.utility_path
   1850                self.symbols_path = this.symbolsPath
   1851 
   1852        old_info = dict(mozinfo.info)
   1853        try:
   1854            suite = unittest.TestLoader().loadTestsFromTestCase(XPCShellTestsTests)
   1855            test_cases = list(suite)
   1856            group = "xpcshell-selftest"
   1857            tests_by_manifest = {
   1858                "xpcshell-selftest": [tc._testMethodName for tc in test_cases]
   1859            }
   1860            self.log.suite_start(tests_by_manifest, name=group)
   1861            self.log.group_start(name="selftests")
   1862 
   1863            if self.sequential or len(test_cases) <= 1:
   1864                return unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()
   1865 
   1866            def run_single_test(test_case):
   1867                result = unittest.TestResult()
   1868                test_name = test_case._testMethodName
   1869                this.log.test_start(test_name, group=group)
   1870                status = "PASS"
   1871                try:
   1872                    test_case.run(result)
   1873                    if not result.wasSuccessful():
   1874                        status = "FAIL"
   1875                except Exception as e:
   1876                    result.addError(test_case, (type(e), e, None))
   1877                    status = "ERROR"
   1878                finally:
   1879                    this.log.test_end(test_name, status, expected="PASS", group=group)
   1880                    return {
   1881                        "result": result,
   1882                        "name": test_name,
   1883                    }
   1884 
   1885            success = True
   1886 
   1887            # Limit parallel self-tests to 32 on macOS to avoid "too many open files" error.
   1888            max_workers = (
   1889                min(32, self.threadCount)
   1890                if sys.platform == "darwin"
   1891                else self.threadCount
   1892            )
   1893 
   1894            self.log.info(
   1895                f"Running {len(test_cases)} self-tests in parallel with up to {max_workers} workers..."
   1896            )
   1897            with ThreadPoolExecutor(max_workers=max_workers) as executor:
   1898                # Submit all tests
   1899                future_to_test = {
   1900                    executor.submit(run_single_test, test): test for test in test_cases
   1901                }
   1902 
   1903                # Print the status of tests as they finish
   1904                for future in as_completed(future_to_test):
   1905                    try:
   1906                        test_result = future.result()
   1907                        result_obj = test_result["result"]
   1908 
   1909                        if not result_obj.wasSuccessful():
   1910                            success = False
   1911                            test_name = test_result["name"]
   1912                            if result_obj.failures:
   1913                                self.log.error(f"FAIL: {test_name}")
   1914                                for test, traceback in result_obj.failures:
   1915                                    self.log.error(f"  Failure: {traceback}")
   1916                            if result_obj.errors:
   1917                                self.log.error(f"ERROR: {test_name}")
   1918                                for test, traceback in result_obj.errors:
   1919                                    self.log.error(f"  Error: {traceback}")
   1920 
   1921                    except Exception as e:
   1922                        self.log.error(f"Exception in test execution: {e}")
   1923                        success = False
   1924 
   1925            return success
   1926 
   1927        finally:
   1928            # The self tests modify mozinfo, so we need to reset it.
   1929            mozinfo.info.clear()
   1930            mozinfo.update(old_info)
   1931 
   1932            self.log.group_end(name="selftests")
   1933            self.log.suite_end()
   1934 
   1935    def runTests(self, options, testClass=XPCShellTestThread, mobileArgs=None):
   1936        """
   1937        Run xpcshell tests.
   1938        """
   1939 
   1940        # Number of times to repeat test(s) in --verify mode
   1941        VERIFY_REPEAT = 10
   1942 
   1943        if isinstance(options, Namespace):
   1944            options = vars(options)
   1945 
   1946        # Try to guess modules directory.
   1947        # This somewhat grotesque hack allows the buildbot machines to find the
   1948        # modules directory without having to configure the buildbot hosts. This
   1949        # code path should never be executed in local runs because the build system
   1950        # should always set this argument.
   1951        if not options.get("testingModulesDir"):
   1952            possible = os.path.join(here, os.path.pardir, "modules")
   1953 
   1954            if os.path.isdir(possible):
   1955                testingModulesDir = possible
   1956 
   1957        if options.get("rerun_failures"):
   1958            if os.path.exists(options.get("failure_manifest")):
   1959                rerun_manifest = os.path.join(
   1960                    os.path.dirname(options["failure_manifest"]), "rerun.toml"
   1961                )
   1962                shutil.copyfile(options["failure_manifest"], rerun_manifest)
   1963                os.remove(options["failure_manifest"])
   1964            else:
   1965                self.log.error("No failures were found to re-run.")
   1966                sys.exit(1)
   1967 
   1968        if options.get("testingModulesDir"):
   1969            # The resource loader expects native paths. Depending on how we were
   1970            # invoked, a UNIX style path may sneak in on Windows. We try to
   1971            # normalize that.
   1972            testingModulesDir = os.path.normpath(options["testingModulesDir"])
   1973 
   1974            if not os.path.isabs(testingModulesDir):
   1975                testingModulesDir = os.path.abspath(testingModulesDir)
   1976 
   1977            if not testingModulesDir.endswith(os.path.sep):
   1978                testingModulesDir += os.path.sep
   1979 
   1980        self.debuggerInfo = None
   1981 
   1982        if options.get("debugger"):
   1983            self.debuggerInfo = mozdebug.get_debugger_info(
   1984                options.get("debugger"),
   1985                options.get("debuggerArgs"),
   1986                options.get("debuggerInteractive"),
   1987            )
   1988 
   1989        self.jsDebuggerInfo = None
   1990        if options.get("jsDebugger"):
   1991            # A namedtuple let's us keep .port instead of ['port']
   1992            JSDebuggerInfo = namedtuple("JSDebuggerInfo", ["port"])
   1993            self.jsDebuggerInfo = JSDebuggerInfo(port=options["jsDebuggerPort"])
   1994 
   1995        # Apply timeout factor
   1996        timeout_factor = options.get("timeoutFactor", 1.0)
   1997        self.harness_timeout = int(HARNESS_TIMEOUT * timeout_factor)
   1998        self.log.info(
   1999            f"Using harness timeout of {self.harness_timeout}s "
   2000            f"(base={HARNESS_TIMEOUT}s, factor={timeout_factor})"
   2001        )
   2002 
   2003        self.app_binary = options.get("app_binary")
   2004        self.xpcshell = options.get("xpcshell")
   2005        self.http3ServerPath = options.get("http3server")
   2006        self.xrePath = options.get("xrePath")
   2007        self.utility_path = options.get("utility_path")
   2008        self.appPath = options.get("appPath")
   2009        self.symbolsPath = options.get("symbolsPath")
   2010        self.tempDir = os.path.normpath(options.get("tempDir") or tempfile.gettempdir())
   2011        self.manifest = options.get("manifest")
   2012        self.dump_tests = options.get("dump_tests")
   2013        self.interactive = options.get("interactive")
   2014        self.verbose = options.get("verbose")
   2015        self.verboseIfFails = options.get("verboseIfFails")
   2016        self.keepGoing = options.get("keepGoing")
   2017        self.logfiles = options.get("logfiles")
   2018        self.totalChunks = options.get("totalChunks", 1)
   2019        self.thisChunk = options.get("thisChunk")
   2020        self.profileName = options.get("profileName") or "xpcshell"
   2021        self.mozInfo = options.get("mozInfo")
   2022        self.testingModulesDir = testingModulesDir
   2023        self.sequential = options.get("sequential")
   2024        self.failure_manifest = options.get("failure_manifest")
   2025        self.threadCount = options.get("threadCount") or NUM_THREADS
   2026        self.jscovdir = options.get("jscovdir")
   2027        self.headless = options.get("headless")
   2028        self.selfTest = options.get("selfTest")
   2029        self.runFailures = options.get("runFailures")
   2030        self.timeoutAsPass = options.get("timeoutAsPass")
   2031        self.crashAsPass = options.get("crashAsPass")
   2032        self.conditionedProfile = options.get("conditionedProfile")
   2033        self.repeat = options.get("repeat", 0)
   2034        self.variant = options.get("variant", "")
   2035        self.profiler = options.get("profiler")
   2036 
   2037        if self.variant == "msix":
   2038            self.appPath = options.get("msixAppPath")
   2039            self.xrePath = options.get("msixXrePath")
   2040            self.app_binary = options.get("msix_app_binary")
   2041            self.xpcshell = None
   2042 
   2043        self.testCount = 0
   2044        self.passCount = 0
   2045        self.failCount = 0
   2046        self.todoCount = 0
   2047 
   2048        if self.conditionedProfile:
   2049            self.conditioned_profile_dir = self.downloadConditionedProfile(
   2050                "full", self.appPath
   2051            )
   2052            options["self_test"] = False
   2053 
   2054        self.setAbsPath()
   2055 
   2056        eprefs = options.get("extraPrefs") or []
   2057        # enable fission by default
   2058        if options.get("disableFission"):
   2059            eprefs.append("fission.autostart=false")
   2060        else:
   2061            # should be by default, just in case
   2062            eprefs.append("fission.autostart=true")
   2063 
   2064        prefs = self.buildPrefsFile(eprefs)
   2065        self.buildXpcsRunArgs()
   2066 
   2067        self.event = Event()
   2068 
   2069        if not self.updateMozinfo(prefs, options):
   2070            return False
   2071 
   2072        self.log.info(
   2073            "These variables are available in the mozinfo environment and "
   2074            "can be used to skip tests conditionally:"
   2075        )
   2076        for info in sorted(self.mozInfo.items(), key=lambda item: item[0]):
   2077            self.log.info(f"    {info[0]}: {info[1]}")
   2078 
   2079        if options.get("self_test"):
   2080            if not self.runSelfTest():
   2081                return False
   2082 
   2083        if (
   2084            ("tsan" in self.mozInfo and self.mozInfo["tsan"])
   2085            or ("asan" in self.mozInfo and self.mozInfo["asan"])
   2086        ) and not options.get("threadCount"):
   2087            # TSan/ASan require significantly more memory, so reduce the amount of parallel
   2088            # tests we run to avoid OOMs and timeouts. We always keep a minimum of 2 for
   2089            # non-sequential execution.
   2090            # pylint --py3k W1619
   2091            self.threadCount = max(self.threadCount / 2, 2)
   2092 
   2093        self.stack_fixer_function = None
   2094        if self.utility_path and os.path.exists(self.utility_path):
   2095            self.stack_fixer_function = get_stack_fixer_function(
   2096                self.utility_path, self.symbolsPath
   2097            )
   2098 
   2099        # buildEnvironment() needs mozInfo, so we call it after mozInfo is initialized.
   2100        self.buildEnvironment()
   2101        extraEnv = parse_key_value(options.get("extraEnv") or [], context="--setenv")
   2102        for k, v in extraEnv:
   2103            if k in self.env:
   2104                self.log.info(
   2105                    "Using environment variable %s instead of %s." % (v, self.env[k])
   2106                )
   2107            self.env[k] = v
   2108 
   2109        # The appDirKey is a optional entry in either the default or individual test
   2110        # sections that defines a relative application directory for test runs. If
   2111        # defined we pass 'grePath/$appDirKey' for the -a parameter of the xpcshell
   2112        # test harness.
   2113        appDirKey = None
   2114        if "appname" in self.mozInfo:
   2115            appDirKey = self.mozInfo["appname"] + "-appdir"
   2116 
   2117        # We have to do this before we run tests that depend on having the node
   2118        # http/2 server.
   2119        self.trySetupNode()
   2120 
   2121        self.startHttp3Server()
   2122 
   2123        pStdout, pStderr = self.getPipes()
   2124 
   2125        self.buildTestList(
   2126            options.get("test_tags"), options.get("testPaths"), options.get("verify")
   2127        )
   2128        if self.singleFile and not self.selfTest:
   2129            self.sequential = True
   2130 
   2131        if options.get("shuffle"):
   2132            random.shuffle(self.alltests)
   2133 
   2134        self.cleanup_dir_list = []
   2135 
   2136        # If any of the tests that are about to be run uses npm packages
   2137        # we should install them now. It would also be possible for tests
   2138        # to define the location where they want the npm modules to be
   2139        # installed, but for now only netwerk xpcshell tests use it.
   2140        installNPM = False
   2141        for test in self.alltests:
   2142            if "usesNPM" in test:
   2143                installNPM = True
   2144                break
   2145 
   2146        if installNPM:
   2147            env = os.environ.copy()
   2148            nodePath = os.environ.get("MOZ_NODE_PATH", "")
   2149            if nodePath:
   2150                node_bin_path = os.path.dirname(nodePath)
   2151                env["PATH"] = f"{node_bin_path}{os.pathsep}{env.get('PATH', '')}"
   2152 
   2153            # Try to find npm in PATH
   2154            npm_executable = shutil.which("npm", path=env.get("PATH"))
   2155 
   2156            if npm_executable:
   2157                command = [npm_executable, "ci"]
   2158                working_directory = os.path.join(SCRIPT_DIR, "moz-http2")
   2159                result = subprocess.run(
   2160                    command,
   2161                    cwd=working_directory,
   2162                    env=env,
   2163                    capture_output=True,
   2164                    text=True,
   2165                    check=False,
   2166                )
   2167 
   2168                # Print the output
   2169                self.log.info("npm output: " + result.stdout)
   2170                self.log.info("npm error: " + result.stderr)
   2171                self.log.info("npm return code: " + str(result.returncode))
   2172            else:
   2173                self.log.warning(
   2174                    "npm step was skipped because no executable could be resolved."
   2175                )
   2176 
   2177        kwargs = {
   2178            "appPath": self.appPath,
   2179            "xrePath": self.xrePath,
   2180            "utility_path": self.utility_path,
   2181            "testingModulesDir": self.testingModulesDir,
   2182            "debuggerInfo": self.debuggerInfo,
   2183            "jsDebuggerInfo": self.jsDebuggerInfo,
   2184            "headJSPath": self.headJSPath,
   2185            "tempDir": self.tempDir,
   2186            "testharnessdir": self.testharnessdir,
   2187            "profileName": self.profileName,
   2188            "singleFile": self.singleFile,
   2189            "env": self.env,  # making a copy of this in the testthreads
   2190            "symbolsPath": self.symbolsPath,
   2191            "logfiles": self.logfiles,
   2192            "app_binary": self.app_binary,
   2193            "xpcshell": self.xpcshell,
   2194            "xpcsRunArgs": self.xpcsRunArgs,
   2195            "failureManifest": self.failure_manifest,
   2196            "jscovdir": self.jscovdir,
   2197            "harness_timeout": self.harness_timeout,
   2198            "stack_fixer_function": self.stack_fixer_function,
   2199            "event": self.event,
   2200            "cleanup_dir_list": self.cleanup_dir_list,
   2201            "pStdout": pStdout,
   2202            "pStderr": pStderr,
   2203            "keep_going": self.keepGoing,
   2204            "log": self.log,
   2205            "interactive": self.interactive,
   2206            "app_dir_key": appDirKey,
   2207            "rootPrefsFile": self.prefsFile,
   2208            "extraPrefs": options.get("extraPrefs") or [],
   2209            "verboseIfFails": self.verboseIfFails,
   2210            "headless": self.headless,
   2211            "selfTest": self.selfTest,
   2212            "runFailures": self.runFailures,
   2213            "timeoutAsPass": self.timeoutAsPass,
   2214            "crashAsPass": self.crashAsPass,
   2215            "conditionedProfileDir": self.conditioned_profile_dir,
   2216            "repeat": self.repeat,
   2217            "profiler": self.profiler,
   2218        }
   2219 
   2220        # Only set retry if explicitly provided (avoid overriding default behavior)
   2221        if options.get("retry") is not None:
   2222            kwargs["retry"] = options.get("retry")
   2223 
   2224        if self.sequential:
   2225            # Allow user to kill hung xpcshell subprocess with SIGINT
   2226            # when we are only running tests sequentially.
   2227            signal.signal(signal.SIGINT, markGotSIGINT)
   2228 
   2229        if self.debuggerInfo:
   2230            # Force a sequential run
   2231            self.sequential = True
   2232 
   2233            # If we have an interactive debugger, disable SIGINT entirely.
   2234            if self.debuggerInfo.interactive:
   2235                signal.signal(signal.SIGINT, lambda signum, frame: None)
   2236 
   2237            if "lldb" in self.debuggerInfo.path:
   2238                # Ask people to start debugging using 'process launch', see bug 952211.
   2239                self.log.info(
   2240                    "It appears that you're using LLDB to debug this test.  "
   2241                    + "Please use the 'process launch' command instead of "
   2242                    "the 'run' command to start xpcshell."
   2243                )
   2244 
   2245        if self.jsDebuggerInfo:
   2246            # The js debugger magic needs more work to do the right thing
   2247            # if debugging multiple files.
   2248            if len(self.alltests) != 1:
   2249                self.log.error(
   2250                    "Error: --jsdebugger can only be used with a single test!"
   2251                )
   2252                return False
   2253 
   2254        # The test itself needs to know whether it is a tsan build, since
   2255        # that has an effect on interpretation of the process return value.
   2256        usingTSan = "tsan" in self.mozInfo and self.mozInfo["tsan"]
   2257 
   2258        usingCrashReporter = (
   2259            "crashreporter" in self.mozInfo and self.mozInfo["crashreporter"]
   2260        )
   2261 
   2262        # create a queue of all tests that will run
   2263        tests_queue = deque()
   2264        # also a list for the tests that need to be run sequentially
   2265        sequential_tests = []
   2266        status = None
   2267 
   2268        if options.get("repeat", 0) > 0:
   2269            self.sequential = True
   2270 
   2271        def _match_run_sequentially(value, **values):
   2272            """Helper function to evaluate run-sequentially conditions like skip-if/run-if"""
   2273            return any(parse(e, strict=True, **values) for e in value.splitlines() if e)
   2274 
   2275        if not options.get("verify"):
   2276            for test_object in self.alltests:
   2277                # Test identifiers are provided for the convenience of logging. These
   2278                # start as path names but are rewritten in case tests from the same path
   2279                # are re-run.
   2280 
   2281                path = test_object["path"]
   2282 
   2283                if self.singleFile and not path.endswith(self.singleFile):
   2284                    continue
   2285 
   2286                # if we have --repeat, duplicate the tests as needed
   2287                for i in range(0, options.get("repeat", 0) + 1):
   2288                    self.testCount += 1
   2289 
   2290                    test = testClass(
   2291                        test_object,
   2292                        verbose=self.verbose or test_object.get("verbose") == "true",
   2293                        usingTSan=usingTSan,
   2294                        usingCrashReporter=usingCrashReporter,
   2295                        mobileArgs=mobileArgs,
   2296                        **kwargs,
   2297                    )
   2298                    if (
   2299                        "run-sequentially" in test_object
   2300                        and _match_run_sequentially(
   2301                            test_object["run-sequentially"], **mozinfo.info
   2302                        )
   2303                    ) or self.sequential:
   2304                        sequential_tests.append(test)
   2305                    else:
   2306                        tests_queue.append(test)
   2307 
   2308            # Sort parallel tests by timeout factor (descending) to start slower tests first
   2309            # This helps optimize parallel execution by avoiding long-running tests at the end
   2310            if tests_queue:
   2311                tests_queue = deque(
   2312                    sorted(
   2313                        tests_queue,
   2314                        key=lambda t: int(t.test_object.get("requesttimeoutfactor", 1)),
   2315                        reverse=True,
   2316                    )
   2317                )
   2318 
   2319            status = self.runTestList(
   2320                tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
   2321            )
   2322        else:
   2323            #
   2324            # Test verification: Run each test many times, in various configurations,
   2325            # in hopes of finding intermittent failures.
   2326            #
   2327 
   2328            def step1():
   2329                # Run tests sequentially. Parallel mode would also work, except that
   2330                # the logging system gets confused when 2 or more tests with the same
   2331                # name run at the same time.
   2332                sequential_tests = []
   2333                for i in range(VERIFY_REPEAT):
   2334                    self.testCount += 1
   2335                    test = testClass(
   2336                        test_object, retry=False, mobileArgs=mobileArgs, **kwargs
   2337                    )
   2338                    sequential_tests.append(test)
   2339                status = self.runTestList(
   2340                    tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
   2341                )
   2342                return status
   2343 
   2344            def step2():
   2345                # Run tests sequentially, with MOZ_CHAOSMODE enabled.
   2346                sequential_tests = []
   2347                self.env["MOZ_CHAOSMODE"] = "0xfb"
   2348 
   2349                # for android, adjust flags to avoid slow down
   2350                if self.env.get("MOZ_ANDROID_DATA_DIR", ""):
   2351                    self.env["MOZ_CHAOSMODE"] = "0x3b"
   2352 
   2353                # chaosmode runs really slow, allow tests extra time to pass
   2354                kwargs["harness_timeout"] = self.harness_timeout * 2
   2355                for i in range(VERIFY_REPEAT):
   2356                    self.testCount += 1
   2357                    test = testClass(
   2358                        test_object, retry=False, mobileArgs=mobileArgs, **kwargs
   2359                    )
   2360                    sequential_tests.append(test)
   2361                status = self.runTestList(
   2362                    tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
   2363                )
   2364                kwargs["harness_timeout"] = self.harness_timeout
   2365                return status
   2366 
   2367            steps = [
   2368                ("1. Run each test %d times, sequentially." % VERIFY_REPEAT, step1),
   2369                (
   2370                    "2. Run each test %d times, sequentially, in chaos mode."
   2371                    % VERIFY_REPEAT,
   2372                    step2,
   2373                ),
   2374            ]
   2375            startTime = datetime.now()
   2376            maxTime = timedelta(seconds=options["verifyMaxTime"])
   2377            for test_object in self.alltests:
   2378                stepResults = {}
   2379                for descr, step in steps:
   2380                    stepResults[descr] = "not run / incomplete"
   2381                finalResult = "PASSED"
   2382                for descr, step in steps:
   2383                    if (datetime.now() - startTime) > maxTime:
   2384                        self.log.info(
   2385                            "::: Test verification is taking too long: Giving up!"
   2386                        )
   2387                        self.log.info(
   2388                            "::: So far, all checks passed, but not "
   2389                            "all checks were run."
   2390                        )
   2391                        break
   2392                    self.log.info(":::")
   2393                    self.log.info('::: Running test verification step "%s"...' % descr)
   2394                    self.log.info(":::")
   2395                    status = step()
   2396                    if status is not True:
   2397                        stepResults[descr] = "FAIL"
   2398                        finalResult = "FAILED!"
   2399                        break
   2400                    stepResults[descr] = "Pass"
   2401                self.log.info(":::")
   2402                self.log.info(
   2403                    "::: Test verification summary for: %s" % test_object["path"]
   2404                )
   2405                self.log.info(":::")
   2406                for descr in sorted(stepResults.keys()):
   2407                    self.log.info("::: %s : %s" % (descr, stepResults[descr]))
   2408                self.log.info(":::")
   2409                self.log.info("::: Test verification %s" % finalResult)
   2410                self.log.info(":::")
   2411 
   2412        self.shutdownNode()
   2413        self.shutdownHttp3Server()
   2414 
   2415        return status
   2416 
   2417    def start_test(self, test):
   2418        test.start()
   2419 
   2420    def test_ended(self, test):
   2421        pass
   2422 
   2423    def runTestList(
   2424        self, tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
   2425    ):
   2426        if self.sequential:
   2427            self.log.info("Running tests sequentially.")
   2428        else:
   2429            self.log.info("Using at most %d threads." % self.threadCount)
   2430 
   2431        # keep a set of threadCount running tests and start running the
   2432        # tests in the queue at most threadCount at a time
   2433        running_tests = set()
   2434        keep_going = True
   2435        infra_abort = False
   2436        exceptions = []
   2437        tracebacks = []
   2438        self.try_again_list = []
   2439 
   2440        tests_by_manifest = defaultdict(list)
   2441        for test in self.alltests:
   2442            group = get_full_group_name(test)
   2443            tests_by_manifest[group].append(test["id"])
   2444 
   2445        self.log.suite_start(tests_by_manifest, name="xpcshell")
   2446 
   2447        # Start group for parallel test execution
   2448        parallel_group_started = False
   2449        if tests_queue:
   2450            self.log.group_start(name="parallel")
   2451            parallel_group_started = True
   2452 
   2453        while tests_queue or running_tests:
   2454            # if we're not supposed to continue and all of the running tests
   2455            # are done, stop
   2456            if not keep_going and not running_tests:
   2457                break
   2458 
   2459            # if there's room to run more tests, start running them
   2460            while (
   2461                keep_going and tests_queue and (len(running_tests) < self.threadCount)
   2462            ):
   2463                test = tests_queue.popleft()
   2464                running_tests.add(test)
   2465                self.start_test(test)
   2466 
   2467            # queue is full (for now) or no more new tests,
   2468            # process the finished tests so far
   2469 
   2470            # wait for at least one of the tests to finish
   2471            self.event.wait(1)
   2472            self.event.clear()
   2473 
   2474            # find what tests are done (might be more than 1)
   2475            done_tests = set()
   2476            for test in running_tests:
   2477                if test.done:
   2478                    self.test_ended(test)
   2479                    done_tests.add(test)
   2480                    test.join(
   2481                        1
   2482                    )  # join with timeout so we don't hang on blocked threads
   2483                    # if the test had trouble, we will try running it again
   2484                    # at the end of the run
   2485                    if test.retry or test.is_alive():
   2486                        # if the join call timed out, test.is_alive => True
   2487                        self.try_again_list.append(test.test_object)
   2488                        # Print the failure output now, marking failures as expected
   2489                        # since we'll retry sequentially
   2490                        test.log_full_output(mark_failures_as_expected=True)
   2491                        continue
   2492                    # did the test encounter any exception?
   2493                    if test.exception:
   2494                        exceptions.append(test.exception)
   2495                        tracebacks.append(test.traceback)
   2496                        # we won't add any more tests, will just wait for
   2497                        # the currently running ones to finish
   2498                        keep_going = False
   2499                    infra_abort = infra_abort and test.infra
   2500                    keep_going = keep_going and test.keep_going
   2501                    self.addTestResults(test)
   2502 
   2503            # make room for new tests to run
   2504            running_tests.difference_update(done_tests)
   2505 
   2506        # End group for parallel test execution
   2507        if parallel_group_started:
   2508            self.log.group_end(name="parallel")
   2509 
   2510        if infra_abort:
   2511            return TBPL_RETRY  # terminate early
   2512 
   2513        if keep_going:
   2514            # run the other tests sequentially
   2515            if sequential_tests:
   2516                self.log.group_start(name="sequential")
   2517            for test in sequential_tests:
   2518                if not keep_going:
   2519                    self.log.error(
   2520                        "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so "
   2521                        "stopped run. (Use --keep-going to keep running tests "
   2522                        "after killing one with SIGINT)"
   2523                    )
   2524                    break
   2525                self.start_test(test)
   2526                test.join()
   2527                self.test_ended(test)
   2528                if (test.failCount > 0 or test.passCount <= 0) and test.retry:
   2529                    self.try_again_list.append(test.test_object)
   2530                    # Print the failure output now, marking failures as expected
   2531                    # since we'll retry sequentially
   2532                    test.log_full_output(mark_failures_as_expected=True)
   2533                    continue
   2534                self.addTestResults(test)
   2535                # did the test encounter any exception?
   2536                if test.exception:
   2537                    exceptions.append(test.exception)
   2538                    tracebacks.append(test.traceback)
   2539                    break
   2540                keep_going = test.keep_going
   2541 
   2542            if sequential_tests:
   2543                self.log.group_end(name="sequential")
   2544 
   2545        # retry tests that failed when run in parallel
   2546        if self.try_again_list:
   2547            self.log.info("Retrying tests that failed when run in parallel.")
   2548            self.log.group_start(name="retry")
   2549        for test_object in self.try_again_list:
   2550            test = testClass(
   2551                test_object,
   2552                retry=False,
   2553                verbose=self.verbose,
   2554                mobileArgs=mobileArgs,
   2555                **kwargs,
   2556            )
   2557            self.start_test(test)
   2558            test.join()
   2559            self.test_ended(test)
   2560            self.addTestResults(test)
   2561            # did the test encounter any exception?
   2562            if test.exception:
   2563                exceptions.append(test.exception)
   2564                tracebacks.append(test.traceback)
   2565                break
   2566            keep_going = test.keep_going
   2567 
   2568        if self.try_again_list:
   2569            self.log.group_end(name="retry")
   2570 
   2571        # restore default SIGINT behaviour
   2572        if self.sequential:
   2573            signal.signal(signal.SIGINT, signal.SIG_DFL)
   2574 
   2575        # Clean up any slacker directories that might be lying around
   2576        # Some might fail because of windows taking too long to unlock them.
   2577        # We don't do anything if this fails because the test machines will have
   2578        # their $TEMP dirs cleaned up on reboot anyway.
   2579        for directory in self.cleanup_dir_list:
   2580            try:
   2581                shutil.rmtree(directory)
   2582            except Exception:
   2583                self.log.info("%s could not be cleaned up." % directory)
   2584 
   2585        if exceptions:
   2586            self.log.info("Following exceptions were raised:")
   2587            for t in tracebacks:
   2588                self.log.error(t)
   2589            raise exceptions[0]
   2590 
   2591        if self.testCount == 0 and os.environ.get("MOZ_AUTOMATION") != "1":
   2592            self.log.error("No tests run. Did you pass an invalid --test-path?")
   2593            self.failCount = 1
   2594 
   2595        # doing this allows us to pass the mozharness parsers that
   2596        # report an orange job for failCount>0
   2597        if self.runFailures:
   2598            passed = self.passCount
   2599            self.passCount = self.failCount
   2600            self.failCount = passed
   2601 
   2602        self.log.info("INFO | Result summary:")
   2603        self.log.info("INFO | Passed: %d" % self.passCount)
   2604        self.log.info("INFO | Failed: %d" % self.failCount)
   2605        self.log.info("INFO | Todo: %d" % self.todoCount)
   2606        self.log.info("INFO | Retried: %d" % len(self.try_again_list))
   2607 
   2608        if gotSIGINT and not keep_going:
   2609            self.log.error(
   2610                "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. "
   2611                "(Use --keep-going to keep running tests after "
   2612                "killing one with SIGINT)"
   2613            )
   2614            return False
   2615 
   2616        self.log.suite_end()
   2617        return self.runFailures or self.failCount == 0
   2618 
   2619 
   2620 def main():
   2621    parser = parser_desktop()
   2622    options = parser.parse_args()
   2623 
   2624    log = commandline.setup_logging("XPCShell", options, {"tbpl": sys.stdout})
   2625 
   2626    if options.xpcshell is None and options.app_binary is None:
   2627        log.error(
   2628            "Must provide path to xpcshell using --xpcshell or Firefox using --app-binary"
   2629        )
   2630        sys.exit(1)
   2631 
   2632    if options.xpcshell is not None and options.app_binary is not None:
   2633        log.error(
   2634            "Cannot provide --xpcshell and --app-binary - they are mutually exclusive options. Choose one."
   2635        )
   2636        sys.exit(1)
   2637 
   2638    xpcsh = XPCShellTests(log)
   2639 
   2640    if options.interactive and not options.testPath:
   2641        log.error("Error: You must specify a test filename in interactive mode!")
   2642        sys.exit(1)
   2643 
   2644    result = xpcsh.runTests(options)
   2645    if result == TBPL_RETRY:
   2646        sys.exit(4)
   2647 
   2648    if not result:
   2649        sys.exit(1)
   2650 
   2651 
   2652 if __name__ == "__main__":
   2653    main()