tor-browser

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

runtests.py (171956B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 """
      6 Runs the Mochitest test harness.
      7 """
      8 
      9 import os
     10 import sys
     11 
     12 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
     13 sys.path.insert(0, SCRIPT_DIR)
     14 
     15 import ctypes
     16 import glob
     17 import json
     18 import numbers
     19 import platform
     20 import re
     21 import shlex
     22 import shutil
     23 import signal
     24 import socket
     25 import subprocess
     26 import sys
     27 import tempfile
     28 import time
     29 import traceback
     30 import uuid
     31 import zipfile
     32 from argparse import Namespace
     33 from collections import defaultdict
     34 from contextlib import closing
     35 from ctypes.util import find_library
     36 from datetime import datetime, timedelta
     37 from pathlib import Path
     38 from shutil import which
     39 from urllib.parse import quote_plus as encodeURIComponent
     40 from urllib.request import urlopen
     41 
     42 import bisection
     43 import mozcrash
     44 import mozdebug
     45 import mozinfo
     46 import mozprocess
     47 import mozrunner
     48 from manifestparser import TestManifest
     49 from manifestparser.filters import (
     50    chunk_by_dir,
     51    chunk_by_runtime,
     52    chunk_by_slice,
     53    failures,
     54    pathprefix,
     55    subsuite,
     56    tags,
     57 )
     58 from manifestparser.util import normsep
     59 from mozgeckoprofiler import (
     60    symbolicate_profile_json,
     61    view_gecko_profile,
     62 )
     63 from mozserve import DoHServer, Http2Server, Http3Server
     64 
     65 try:
     66    from marionette_driver.addons import Addons
     67    from marionette_driver.marionette import Marionette
     68 except ImportError as e:  # noqa
     69    error = e
     70 
     71    # Defer ImportError until attempt to use Marionette
     72    def reraise(*args, **kwargs):
     73        raise error  # noqa
     74 
     75    Marionette = reraise
     76 
     77 import mozleak
     78 from leaks import LSANLeaks, ShutdownLeaks
     79 from mochitest_options import (
     80    MochitestArgumentParser,
     81    build_obj,
     82    get_default_valgrind_suppression_files,
     83 )
     84 from mozlog import commandline, get_proxy_logger
     85 from mozprofile import Profile
     86 from mozprofile.cli import KeyValueParseError, parse_key_value, parse_preferences
     87 from mozprofile.permissions import ServerLocations
     88 from mozrunner.utils import get_stack_fixer_function, test_environment
     89 from mozscreenshot import dump_screen
     90 
     91 HAVE_PSUTIL = False
     92 try:
     93    import psutil
     94 
     95    HAVE_PSUTIL = True
     96 except ImportError:
     97    pass
     98 
     99 try:
    100    from mozbuild.base import MozbuildObject
    101 
    102    build = MozbuildObject.from_environment(cwd=SCRIPT_DIR)
    103 except ImportError:
    104    build = None
    105 
    106 here = os.path.abspath(os.path.dirname(__file__))
    107 
    108 NO_TESTS_FOUND = """
    109 No tests were found for flavor '{}' and the following manifest filters:
    110 {}
    111 
    112 Make sure the test paths (if any) are spelt correctly and the corresponding
    113 --flavor and --subsuite are being used. See `mach mochitest --help` for a
    114 list of valid flavors.
    115 """.lstrip()
    116 
    117 
    118 ########################################
    119 # Option for MOZ (former NSPR) logging #
    120 ########################################
    121 
    122 # Set the desired log modules you want a log be produced
    123 # by a try run for, or leave blank to disable the feature.
    124 # This will be passed to MOZ_LOG environment variable.
    125 # Try run will then put a download link for a zip archive
    126 # of all the log files on treeherder.
    127 MOZ_LOG = ""
    128 
    129 ########################################
    130 # Option for web server log            #
    131 ########################################
    132 
    133 # If True, debug logging from the web server will be
    134 # written to mochitest-server-%d.txt artifacts on
    135 # treeherder.
    136 MOCHITEST_SERVER_LOGGING = False
    137 
    138 #####################
    139 # Test log handling #
    140 #####################
    141 
    142 # output processing
    143 TBPL_RETRY = 4  # Defined in mozharness
    144 
    145 
    146 class MessageLogger:
    147    """File-like object for logging messages (structured logs)"""
    148 
    149    BUFFERING_THRESHOLD = 100
    150    # This is a delimiter used by the JS side to avoid logs interleaving
    151    DELIMITER = "\ue175\uee31\u2c32\uacbf"
    152    BUFFERED_ACTIONS = set(["test_status", "log"])
    153    VALID_ACTIONS = set([
    154        "suite_start",
    155        "suite_end",
    156        "group_start",
    157        "group_end",
    158        "test_start",
    159        "test_end",
    160        "test_status",
    161        "log",
    162        "assertion_count",
    163        "buffering_on",
    164        "buffering_off",
    165    ])
    166    # Regexes that will be replaced with an empty string if found in a test
    167    # name. We do this to normalize test names which may contain URLs and test
    168    # package prefixes.
    169    TEST_PATH_PREFIXES = [
    170        r"^/tests/",
    171        r"^\w+://[\w\.]+(:\d+)?(/\w+)?/(tests?|a11y|chrome)/",
    172        r"^\w+://[\w\.]+(:\d+)?(/\w+)?/(tests?|browser)/",
    173    ]
    174 
    175    def __init__(self, logger, buffering=True, structured=True):
    176        self.logger = logger
    177        self.structured = structured
    178        self.gecko_id = "GECKO"
    179        self.is_test_running = False
    180        self._manifest = None
    181 
    182        # Even if buffering is enabled, we only want to buffer messages between
    183        # TEST-START/TEST-END. So it is off to begin, but will be enabled after
    184        # a TEST-START comes in.
    185        self._buffering = False
    186        self.restore_buffering = buffering
    187 
    188        # Guard to ensure we never buffer if this value was initially `False`
    189        self._buffering_initially_enabled = buffering
    190 
    191        # Message buffering
    192        self.buffered_messages = []
    193 
    194    def setManifest(self, name):
    195        self._manifest = name
    196 
    197    def validate(self, obj):
    198        """Tests whether the given object is a valid structured message
    199        (only does a superficial validation)"""
    200        if not (
    201            isinstance(obj, dict)
    202            and "action" in obj
    203            and obj["action"] in MessageLogger.VALID_ACTIONS
    204        ):
    205            raise ValueError
    206 
    207    def _fix_subtest_name(self, message):
    208        """Ensure test_status messages have a subtest field and convert it to a string"""
    209        if message.get("action") == "test_status" and "subtest" not in message:
    210            message["subtest"] = None
    211        elif message.get("subtest") is not None:
    212            message["subtest"] = str(message["subtest"])
    213 
    214    def _fix_test_name(self, message):
    215        """Normalize a logged test path to match the relative path from the sourcedir."""
    216        if message.get("test") is not None:
    217            test = message["test"]
    218            for pattern in MessageLogger.TEST_PATH_PREFIXES:
    219                test = re.sub(pattern, "", test)
    220                if test != message["test"]:
    221                    message["test"] = test
    222                    break
    223 
    224    def _fix_message_format(self, message):
    225        if "message" in message:
    226            if isinstance(message["message"], bytes):
    227                message["message"] = message["message"].decode("utf-8", "replace")
    228            elif not isinstance(message["message"], str):
    229                message["message"] = str(message["message"])
    230 
    231    def parse_line(self, line):
    232        """Takes a given line of input (structured or not) and
    233        returns a list of structured messages"""
    234        if isinstance(line, bytes):
    235            # if line is a sequence of bytes, let's decode it
    236            line = line.rstrip().decode("UTF-8", "replace")
    237        else:
    238            # line is in unicode - so let's use it as it is
    239            line = line.rstrip()
    240 
    241        messages = []
    242        for fragment in line.split(MessageLogger.DELIMITER):
    243            if not fragment:
    244                continue
    245            try:
    246                message = json.loads(fragment)
    247                self.validate(message)
    248            except ValueError:
    249                if self.structured:
    250                    message = dict(
    251                        action="process_output",
    252                        process=self.gecko_id,
    253                        data=fragment,
    254                    )
    255                else:
    256                    message = dict(
    257                        action="log",
    258                        level="info",
    259                        message=fragment,
    260                    )
    261 
    262            self._fix_subtest_name(message)
    263            self._fix_test_name(message)
    264            self._fix_message_format(message)
    265            message["group"] = self._manifest
    266            messages.append(message)
    267 
    268        return messages
    269 
    270    @property
    271    def buffering(self):
    272        if not self._buffering_initially_enabled:
    273            return False
    274        return self._buffering
    275 
    276    @buffering.setter
    277    def buffering(self, val):
    278        self._buffering = val
    279 
    280    def process_message(self, message):
    281        """Processes a structured message. Takes into account buffering, errors, ..."""
    282        # Activation/deactivating message buffering from the JS side
    283        if message["action"] == "buffering_on":
    284            if self.is_test_running:
    285                self.buffering = True
    286            return
    287        if message["action"] == "buffering_off":
    288            self.buffering = False
    289            return
    290 
    291        # Error detection also supports "raw" errors (in log messages) because some tests
    292        # manually dump 'TEST-UNEXPECTED-FAIL'.
    293        if "expected" in message or (
    294            message["action"] == "log"
    295            and message.get("message", "").startswith("TEST-UNEXPECTED")
    296        ):
    297            self.restore_buffering = self.restore_buffering or self.buffering
    298            self.buffering = False
    299            if self.buffered_messages:
    300                snipped = len(self.buffered_messages) - self.BUFFERING_THRESHOLD
    301                if snipped > 0:
    302                    self.logger.info(
    303                        f"<snipped {snipped} output lines - "
    304                        "if you need more context, please use "
    305                        "SimpleTest.requestCompleteLog() in your test>"
    306                    )
    307                # Dumping previously buffered messages
    308                self.dump_buffered(limit=True)
    309 
    310            # Logging the error message
    311            self.logger.log_raw(message)
    312        # Determine if message should be buffered
    313        elif (
    314            self.buffering
    315            and self.structured
    316            and message["action"] in self.BUFFERED_ACTIONS
    317        ):
    318            self.buffered_messages.append(message)
    319        # Otherwise log the message directly
    320        else:
    321            self.logger.log_raw(message)
    322 
    323        # If a test ended, we clean the buffer
    324        if message["action"] == "test_end":
    325            self.is_test_running = False
    326            self.buffered_messages = []
    327            self.restore_buffering = self.restore_buffering or self.buffering
    328            self.buffering = False
    329 
    330        if message["action"] == "test_start":
    331            self.is_test_running = True
    332            if self.restore_buffering:
    333                self.restore_buffering = False
    334                self.buffering = True
    335 
    336    def write(self, line):
    337        messages = self.parse_line(line)
    338        for message in messages:
    339            self.process_message(message)
    340        return messages
    341 
    342    def flush(self):
    343        sys.stdout.flush()
    344 
    345    def dump_buffered(self, limit=False):
    346        if limit:
    347            dumped_messages = self.buffered_messages[-self.BUFFERING_THRESHOLD :]
    348        else:
    349            dumped_messages = self.buffered_messages
    350 
    351        last_timestamp = None
    352        for buf in dumped_messages:
    353            # pylint --py3k W1619
    354            timestamp = datetime.fromtimestamp(buf["time"] / 1000).strftime("%H:%M:%S")
    355            if timestamp != last_timestamp:
    356                self.logger.info(f"Buffered messages logged at {timestamp}")
    357            last_timestamp = timestamp
    358 
    359            self.logger.log_raw(buf)
    360        self.logger.info("Buffered messages finished")
    361        # Cleaning the list of buffered messages
    362        self.buffered_messages = []
    363 
    364    def finish(self):
    365        self.dump_buffered()
    366        self.buffering = False
    367        self.logger.suite_end()
    368 
    369 
    370 ####################
    371 # PROCESS HANDLING #
    372 ####################
    373 
    374 
    375 def call(*args, **kwargs):
    376    """wraps mozprocess.run_and_wait with process output logging"""
    377    log = get_proxy_logger("mochitest")
    378 
    379    def on_output(proc, line):
    380        cmdline = subprocess.list2cmdline(proc.args)
    381        log.process_output(
    382            process=proc.pid,
    383            data=line,
    384            command=cmdline,
    385        )
    386 
    387    process = mozprocess.run_and_wait(*args, output_line_handler=on_output, **kwargs)
    388    return process.returncode
    389 
    390 
    391 def killPid(pid, log):
    392    # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58
    393 
    394    if HAVE_PSUTIL:
    395        # Kill a process tree (including grandchildren) with signal.SIGTERM
    396        if pid == os.getpid():
    397            raise RuntimeError("Error: trying to kill ourselves, not another process")
    398        try:
    399            parent = psutil.Process(pid)
    400            children = parent.children(recursive=True)
    401            children.append(parent)
    402            for p in children:
    403                p.send_signal(signal.SIGTERM)
    404            gone, alive = psutil.wait_procs(children, timeout=30)
    405            for p in gone:
    406                log.info("psutil found pid %s dead" % p.pid)
    407            for p in alive:
    408                log.info("failed to kill pid %d after 30s" % p.pid)
    409        except Exception as e:
    410            log.info("Error: Failed to kill process %d: %s" % (pid, str(e)))
    411    else:
    412        try:
    413            os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
    414        except Exception as e:
    415            log.info("Failed to kill process %d: %s" % (pid, str(e)))
    416 
    417 
    418 if mozinfo.isWin:
    419    import ctypes.wintypes
    420 
    421    def isPidAlive(pid):
    422        STILL_ACTIVE = 259
    423        PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
    424        pHandle = ctypes.windll.kernel32.OpenProcess(
    425            PROCESS_QUERY_LIMITED_INFORMATION, 0, pid
    426        )
    427        if not pHandle:
    428            return False
    429 
    430        try:
    431            pExitCode = ctypes.wintypes.DWORD()
    432            ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
    433 
    434            if pExitCode.value != STILL_ACTIVE:
    435                return False
    436 
    437            # We have a live process handle.  But Windows aggressively
    438            # re-uses pids, so let's attempt to verify that this is
    439            # actually Firefox.
    440            namesize = 1024
    441            pName = ctypes.create_string_buffer(namesize)
    442            namelen = ctypes.windll.psapi.GetProcessImageFileNameA(
    443                pHandle, pName, namesize
    444            )
    445            if namelen == 0:
    446                # Still an active process, so conservatively assume it's Firefox.
    447                return True
    448 
    449            return pName.value.endswith((b"firefox.exe", b"plugin-container.exe"))
    450        finally:
    451            ctypes.windll.kernel32.CloseHandle(pHandle)
    452 
    453 else:
    454    import errno
    455 
    456    def isPidAlive(pid):
    457        try:
    458            # kill(pid, 0) checks for a valid PID without actually sending a signal
    459            # The method throws OSError if the PID is invalid, which we catch
    460            # below.
    461            os.kill(pid, 0)
    462 
    463            # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
    464            # the process terminates before we get to this point.
    465            wpid, wstatus = os.waitpid(pid, os.WNOHANG)
    466            return wpid == 0
    467        except OSError as err:
    468            # Catch the errors we might expect from os.kill/os.waitpid,
    469            # and re-raise any others
    470            if err.errno in (errno.ESRCH, errno.ECHILD, errno.EPERM):
    471                return False
    472            raise
    473 
    474 
    475 # TODO: ^ upstream isPidAlive to mozprocess
    476 
    477 #######################
    478 # HTTP SERVER SUPPORT #
    479 #######################
    480 
    481 
    482 class MochitestServer:
    483    "Web server used to serve Mochitests, for closer fidelity to the real web."
    484 
    485    instance_count = 0
    486 
    487    def __init__(self, options, logger):
    488        if isinstance(options, Namespace):
    489            options = vars(options)
    490        self._log = logger
    491        self._keep_open = bool(options["keep_open"])
    492        self._utilityPath = options["utilityPath"]
    493        self._xrePath = options["xrePath"]
    494        self._profileDir = options["profilePath"]
    495        self.webServer = options["webServer"]
    496        self.httpPort = options["httpPort"]
    497        if options.get("remoteWebServer") == "10.0.2.2":
    498            # probably running an Android emulator and 10.0.2.2 will
    499            # not be visible from host
    500            shutdownServer = "127.0.0.1"
    501        else:
    502            shutdownServer = self.webServer
    503        self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % {
    504            "server": shutdownServer,
    505            "port": self.httpPort,
    506        }
    507        self.debugURL = "http://%(server)s:%(port)s/server/debug?2" % {
    508            "server": shutdownServer,
    509            "port": self.httpPort,
    510        }
    511        self.testPrefix = "undefined"
    512 
    513        if options.get("httpdPath"):
    514            self._httpdPath = options["httpdPath"]
    515        else:
    516            self._httpdPath = SCRIPT_DIR
    517        self._httpdPath = os.path.abspath(self._httpdPath)
    518 
    519        self._trainHop = "browser.newtabpage.trainhopAddon.version=any" in options.get(
    520            "extraPrefs", []
    521        )
    522 
    523        MochitestServer.instance_count += 1
    524 
    525    def start(self):
    526        "Run the Mochitest server, returning the process ID of the server."
    527 
    528        # get testing environment
    529        env = test_environment(xrePath=self._xrePath, log=self._log)
    530        env["XPCOM_DEBUG_BREAK"] = "warn"
    531        if "LD_LIBRARY_PATH" not in env or env["LD_LIBRARY_PATH"] is None:
    532            env["LD_LIBRARY_PATH"] = self._xrePath
    533        else:
    534            env["LD_LIBRARY_PATH"] = ":".join([self._xrePath, env["LD_LIBRARY_PATH"]])
    535 
    536        if self._trainHop:
    537            env["LD_LIBRARY_PATH"] = ":".join([
    538                os.path.join(os.path.dirname(here), "bin"),
    539                env["LD_LIBRARY_PATH"],
    540            ])
    541 
    542        # When running with an ASan build, our xpcshell server will also be ASan-enabled,
    543        # thus consuming too much resources when running together with the browser on
    544        # the test machines. Try to limit the amount of resources by disabling certain
    545        # features.
    546        env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5"
    547 
    548        # Likewise, when running with a TSan build, our xpcshell server will
    549        # also be TSan-enabled. Except that in this case, we don't really
    550        # care about races in xpcshell. So disable TSan for the server.
    551        env["TSAN_OPTIONS"] = "report_bugs=0"
    552 
    553        # Don't use socket process for the xpcshell server.
    554        env["MOZ_DISABLE_SOCKET_PROCESS"] = "1"
    555 
    556        if mozinfo.isWin:
    557            env["PATH"] = env["PATH"] + ";" + str(self._xrePath)
    558 
    559        args = [
    560            "-g",
    561            self._xrePath,
    562            "-e",
    563            "const _PROFILE_PATH = '%(profile)s'; const _SERVER_PORT = '%(port)s'; "
    564            "const _SERVER_ADDR = '%(server)s'; const _TEST_PREFIX = %(testPrefix)s; "
    565            "const _DISPLAY_RESULTS = %(displayResults)s; "
    566            "const _HTTPD_PATH = '%(httpdPath)s';"
    567            % {
    568                "httpdPath": self._httpdPath.replace("\\", "\\\\"),
    569                "profile": self._profileDir.replace("\\", "\\\\"),
    570                "port": self.httpPort,
    571                "server": self.webServer,
    572                "testPrefix": self.testPrefix,
    573                "displayResults": str(self._keep_open).lower(),
    574            },
    575            "-f",
    576            os.path.join(SCRIPT_DIR, "server.js"),
    577        ]
    578 
    579        xpcshell = os.path.join(
    580            self._utilityPath, "xpcshell" + mozinfo.info["bin_suffix"]
    581        )
    582        command = [xpcshell] + args
    583        if MOCHITEST_SERVER_LOGGING and "MOZ_UPLOAD_DIR" in os.environ:
    584            server_logfile_path = os.path.join(
    585                os.environ["MOZ_UPLOAD_DIR"],
    586                "mochitest-server-%d.txt" % MochitestServer.instance_count,
    587            )
    588            self.server_logfile = open(server_logfile_path, "w")
    589            self._process = subprocess.Popen(
    590                command,
    591                cwd=SCRIPT_DIR,
    592                env=env,
    593                stdout=self.server_logfile,
    594                stderr=subprocess.STDOUT,
    595            )
    596        else:
    597            self.server_logfile = None
    598            self._process = subprocess.Popen(
    599                command,
    600                cwd=SCRIPT_DIR,
    601                env=env,
    602            )
    603        self._log.info("%s : launching %s" % (self.__class__.__name__, command))
    604        pid = self._process.pid
    605        self._log.info("runtests.py | Server pid: %d" % pid)
    606        if MOCHITEST_SERVER_LOGGING and "MOZ_UPLOAD_DIR" in os.environ:
    607            self._log.info("runtests.py enabling server debugging...")
    608            i = 0
    609            while i < 5:
    610                try:
    611                    with closing(urlopen(self.debugURL)) as c:
    612                        self._log.info(c.read().decode("utf-8"))
    613                    break
    614                except Exception as e:
    615                    self._log.info("exception when enabling debugging: %s" % str(e))
    616                    time.sleep(1)
    617                    i += 1
    618 
    619    def ensureReady(self, timeout):
    620        assert timeout >= 0
    621 
    622        aliveFile = os.path.join(self._profileDir, "server_alive.txt")
    623        i = 0
    624        while i < timeout:
    625            if os.path.exists(aliveFile):
    626                break
    627            time.sleep(0.05)
    628            i += 0.05
    629        else:
    630            self._log.error(
    631                "TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup."
    632            )
    633            self.stop()
    634            sys.exit(1)
    635 
    636    def stop(self):
    637        try:
    638            with closing(urlopen(self.shutdownURL)) as c:
    639                self._log.info(c.read().decode("utf-8"))
    640        except Exception:
    641            self._log.info("Failed to stop web server on %s" % self.shutdownURL)
    642            traceback.print_exc()
    643        finally:
    644            if self.server_logfile is not None:
    645                self.server_logfile.close()
    646            if self._process is not None:
    647                # Kill the server immediately to avoid logging intermittent
    648                # shutdown crashes, sometimes observed on Windows 10.
    649                self._process.kill()
    650                self._log.info("Web server killed.")
    651 
    652 
    653 class WebSocketServer:
    654    "Class which encapsulates the mod_pywebsocket server"
    655 
    656    def __init__(self, options, scriptdir, logger, debuggerInfo=None):
    657        self.port = options.webSocketPort
    658        self.debuggerInfo = debuggerInfo
    659        self._log = logger
    660        self._scriptdir = scriptdir
    661 
    662    def start(self):
    663        # Invoke pywebsocket through a wrapper which adds special SIGINT handling.
    664        #
    665        # If we're in an interactive debugger, the wrapper causes the server to
    666        # ignore SIGINT so the server doesn't capture a ctrl+c meant for the
    667        # debugger.
    668        #
    669        # If we're not in an interactive debugger, the wrapper causes the server to
    670        # die silently upon receiving a SIGINT.
    671        scriptPath = "pywebsocket_wrapper.py"
    672        script = os.path.join(self._scriptdir, scriptPath)
    673 
    674        cmd = [sys.executable, script]
    675        if self.debuggerInfo and self.debuggerInfo.interactive:
    676            cmd += ["--interactive"]
    677        # We need to use 0.0.0.0 to listen on all interfaces because
    678        # Android tests connect from a different hosts
    679        cmd += [
    680            "-H",
    681            "0.0.0.0",
    682            "-p",
    683            str(self.port),
    684            "-w",
    685            self._scriptdir,
    686            "-l",
    687            os.path.join(self._scriptdir, "websock.log"),
    688            "--log-level=debug",
    689            "--allow-handlers-outside-root-dir",
    690        ]
    691        env = dict(os.environ)
    692        env["PYTHONPATH"] = os.pathsep.join(sys.path)
    693        # Start the process. Ignore stderr so that exceptions from the server
    694        # are not treated as failures when parsing the test log.
    695        self._process = subprocess.Popen(
    696            cmd, cwd=SCRIPT_DIR, env=env, stderr=subprocess.DEVNULL
    697        )
    698        pid = self._process.pid
    699        self._log.info("runtests.py | Websocket server pid: %d" % pid)
    700 
    701    def stop(self):
    702        if self._process is not None:
    703            self._process.kill()
    704 
    705 
    706 class SSLTunnel:
    707    def __init__(self, options, logger):
    708        self.log = logger
    709        self.process = None
    710        self.utilityPath = options.utilityPath
    711        self.xrePath = options.xrePath
    712        self.certPath = options.certPath
    713        self.sslPort = options.sslPort
    714        self.httpPort = options.httpPort
    715        self.webServer = options.webServer
    716        self.webSocketPort = options.webSocketPort
    717 
    718        self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ -]+)")
    719        self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
    720        self.redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
    721 
    722    def writeLocation(self, config, loc):
    723        for option in loc.options:
    724            match = self.customCertRE.match(option)
    725            if match:
    726                customcert = match.group("nickname")
    727                config.write(
    728                    "listen:%s:%s:%s:%s\n"
    729                    % (loc.host, loc.port, self.sslPort, customcert)
    730                )
    731 
    732            match = self.clientAuthRE.match(option)
    733            if match:
    734                clientauth = match.group("clientauth")
    735                config.write(
    736                    "clientauth:%s:%s:%s:%s\n"
    737                    % (loc.host, loc.port, self.sslPort, clientauth)
    738                )
    739 
    740            match = self.redirRE.match(option)
    741            if match:
    742                redirhost = match.group("redirhost")
    743                config.write(
    744                    "redirhost:%s:%s:%s:%s\n"
    745                    % (loc.host, loc.port, self.sslPort, redirhost)
    746                )
    747 
    748            if option in (
    749                "tls1",
    750                "tls1_1",
    751                "tls1_2",
    752                "tls1_3",
    753                "ssl3",
    754                "3des",
    755                "failHandshake",
    756            ):
    757                config.write(
    758                    "%s:%s:%s:%s\n" % (option, loc.host, loc.port, self.sslPort)
    759                )
    760 
    761    def buildConfig(self, locations, public=None):
    762        """Create the ssltunnel configuration file"""
    763        configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg")
    764        with os.fdopen(configFd, "w") as config:
    765            config.write("httpproxy:1\n")
    766            config.write("certdbdir:%s\n" % self.certPath)
    767            config.write("forward:127.0.0.1:%s\n" % self.httpPort)
    768 
    769            wsserver = self.webServer
    770            if self.webServer == "10.0.2.2":
    771                wsserver = "127.0.0.1"
    772 
    773            config.write("websocketserver:%s:%s\n" % (wsserver, self.webSocketPort))
    774            # Use "*" to tell ssltunnel to listen on the public ip
    775            # address instead of the loopback address 127.0.0.1. This
    776            # may have the side-effect of causing firewall warnings on
    777            # macOS and Windows. Use "127.0.0.1" to listen on the
    778            # loopback address.  Remote tests using physical or
    779            # emulated Android devices must use the public ip address
    780            # in order for the sslproxy to work but Desktop tests
    781            # which run on the same host as ssltunnel may use the
    782            # loopback address.
    783            listen_address = "*" if public else "127.0.0.1"
    784            config.write("listen:%s:%s:pgoserver\n" % (listen_address, self.sslPort))
    785 
    786            for loc in locations:
    787                if loc.scheme == "https" and "nocert" not in loc.options:
    788                    self.writeLocation(config, loc)
    789 
    790    def start(self):
    791        """Starts the SSL Tunnel"""
    792 
    793        # start ssltunnel to provide https:// URLs capability
    794        ssltunnel = os.path.join(self.utilityPath, "ssltunnel")
    795        if os.name == "nt":
    796            ssltunnel += ".exe"
    797        if not os.path.exists(ssltunnel):
    798            self.log.error(
    799                "INFO | runtests.py | expected to find ssltunnel at %s" % ssltunnel
    800            )
    801            sys.exit(1)
    802 
    803        env = test_environment(xrePath=self.xrePath, log=self.log)
    804        env["LD_LIBRARY_PATH"] = self.xrePath
    805        self.process = subprocess.Popen([ssltunnel, self.configFile], env=env)
    806        self.log.info("runtests.py | SSL tunnel pid: %d" % self.process.pid)
    807 
    808    def stop(self):
    809        """Stops the SSL Tunnel and cleans up"""
    810        if self.process is not None:
    811            self.process.kill()
    812        if os.path.exists(self.configFile):
    813            os.remove(self.configFile)
    814 
    815 
    816 def checkAndConfigureV4l2loopback(device):
    817    """
    818    Determine if a given device path is a v4l2loopback device, and if so
    819    toggle a few settings on it via fcntl. Very linux-specific.
    820 
    821    Returns (status, device name) where status is a boolean.
    822    """
    823    if not mozinfo.isLinux:
    824        return False, ""
    825 
    826    libc = ctypes.cdll.LoadLibrary(find_library("c"))
    827    O_RDWR = 2
    828    # These are from linux/videodev2.h
    829 
    830    class v4l2_capability(ctypes.Structure):
    831        _fields_ = [
    832            ("driver", ctypes.c_char * 16),
    833            ("card", ctypes.c_char * 32),
    834            ("bus_info", ctypes.c_char * 32),
    835            ("version", ctypes.c_uint32),
    836            ("capabilities", ctypes.c_uint32),
    837            ("device_caps", ctypes.c_uint32),
    838            ("reserved", ctypes.c_uint32 * 3),
    839        ]
    840 
    841    VIDIOC_QUERYCAP = 0x80685600
    842 
    843    fd = libc.open(device.encode("ascii"), O_RDWR)
    844    if fd < 0:
    845        return False, ""
    846 
    847    vcap = v4l2_capability()
    848    if libc.ioctl(fd, VIDIOC_QUERYCAP, ctypes.byref(vcap)) != 0:
    849        return False, ""
    850 
    851    if vcap.driver.decode("utf-8") != "v4l2 loopback":
    852        return False, ""
    853 
    854    class v4l2_control(ctypes.Structure):
    855        _fields_ = [("id", ctypes.c_uint32), ("value", ctypes.c_int32)]
    856 
    857    # These are private v4l2 control IDs, see:
    858    # https://github.com/umlaeute/v4l2loopback/blob/fd822cf0faaccdf5f548cddd9a5a3dcebb6d584d/v4l2loopback.c#L131
    859    KEEP_FORMAT = 0x8000000
    860    SUSTAIN_FRAMERATE = 0x8000001
    861    VIDIOC_S_CTRL = 0xC008561C
    862 
    863    control = v4l2_control()
    864    control.id = KEEP_FORMAT
    865    control.value = 1
    866    libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
    867 
    868    control.id = SUSTAIN_FRAMERATE
    869    control.value = 1
    870    libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
    871    libc.close(fd)
    872 
    873    return True, vcap.card.decode("utf-8")
    874 
    875 
    876 def findTestMediaDevices(log):
    877    """
    878    Find the test media devices configured on this system, and return a dict
    879    containing information about them. The dict will have keys for 'audio'
    880    and 'video', each containing the name of the media device to use.
    881 
    882    If audio and video devices could not be found, return None.
    883 
    884    This method is only currently implemented for Linux.
    885    """
    886    if not mozinfo.isLinux:
    887        return None
    888 
    889    info = {}
    890    # Look for a v4l2loopback device.
    891    name = None
    892    device = None
    893    for dev in sorted(glob.glob("/dev/video*")):
    894        result, name_ = checkAndConfigureV4l2loopback(dev)
    895        if result:
    896            name = name_
    897            device = dev
    898            break
    899 
    900    if not (name and device):
    901        log.error("Couldn't find a v4l2loopback video device")
    902        return None
    903 
    904    # Feed it a frame of output so it has something to display
    905    gst01 = which("gst-launch-0.1")
    906    gst010 = which("gst-launch-0.10")
    907    gst10 = which("gst-launch-1.0")
    908    if gst01:
    909        gst = gst01
    910    if gst010:
    911        gst = gst010
    912    else:
    913        gst = gst10
    914    process = subprocess.Popen([
    915        gst,
    916        "--no-fault",
    917        "videotestsrc",
    918        "pattern=green",
    919        "num-buffers=1",
    920        "!",
    921        "v4l2sink",
    922        "device=%s" % device,
    923    ])
    924    info["video"] = {"name": name, "process": process}
    925    info["speaker"] = {"name": "44100Hz Null Output"}
    926    info["audio"] = {"name": "Monitor of {}".format(info["speaker"]["name"])}
    927    return info
    928 
    929 
    930 def create_zip(path):
    931    """
    932    Takes a `path` on disk and creates a zipfile with its contents. Returns a
    933    path to the location of the temporary zip file.
    934    """
    935    with tempfile.NamedTemporaryFile() as f:
    936        # `shutil.make_archive` writes to "{f.name}.zip", so we're really just
    937        # using `NamedTemporaryFile` as a way to get a random path.
    938        return shutil.make_archive(f.name, "zip", path)
    939 
    940 
    941 def update_mozinfo():
    942    """walk up directories to find mozinfo.json update the info"""
    943    # TODO: This should go in a more generic place, e.g. mozinfo
    944 
    945    path = SCRIPT_DIR
    946    dirs = set()
    947    while path != os.path.expanduser("~"):
    948        if path in dirs:
    949            break
    950        dirs.add(path)
    951        path = os.path.split(path)[0]
    952 
    953    mozinfo.find_and_update_from_json(*dirs)
    954 
    955 
    956 class MochitestDesktop:
    957    """
    958    Mochitest class for desktop firefox.
    959    """
    960 
    961    oldcwd = os.getcwd()
    962 
    963    # Path to the test script on the server
    964    TEST_PATH = "tests"
    965    CHROME_PATH = "redirect.html"
    966 
    967    certdbNew = False
    968    sslTunnel = None
    969    DEFAULT_TIMEOUT = 60.0
    970    mediaDevices = None
    971    mozinfo_variables_shown = False
    972 
    973    patternFiles = {}
    974 
    975    # XXX use automation.py for test name to avoid breaking legacy
    976    # TODO: replace this with 'runtests.py' or 'mochitest' or the like
    977    test_name = "automation.py"
    978 
    979    def __init__(self, flavor, logger_options, staged_addons=None, quiet=False):
    980        update_mozinfo()
    981        self.flavor = flavor
    982        self.staged_addons = staged_addons
    983        self.server = None
    984        self.wsserver = None
    985        self.websocketProcessBridge = None
    986        self.sslTunnel = None
    987        self.manifest = None
    988        self.tests_by_manifest = defaultdict(list)
    989        self.args_by_manifest = defaultdict(set)
    990        self.prefs_by_manifest = defaultdict(set)
    991        self.env_vars_by_manifest = defaultdict(set)
    992        self.tests_dirs_by_manifest = defaultdict(set)
    993        self._active_tests = None
    994        self.currentTests = None
    995        self._locations = None
    996        self.browserEnv = None
    997 
    998        self.marionette = None
    999        self.start_script = None
   1000        self.mozLogs = None
   1001        self.start_script_kwargs = {}
   1002        self.extraArgs = []
   1003        self.extraPrefs = {}
   1004        self.extraEnv = {}
   1005        self.extraTestsDirs = []
   1006        self.conditioned_profile_dir = None
   1007        self.perfherder_data = []
   1008 
   1009        if logger_options.get("log"):
   1010            self.log = logger_options["log"]
   1011        else:
   1012            self.log = commandline.setup_logging(
   1013                "mochitest", logger_options, {"tbpl": sys.stdout}
   1014            )
   1015 
   1016        self.message_logger = MessageLogger(
   1017            logger=self.log, buffering=quiet, structured=True
   1018        )
   1019 
   1020        # Max time in seconds to wait for server startup before tests will fail -- if
   1021        # this seems big, it's mostly for debug machines where cold startup
   1022        # (particularly after a build) takes forever.
   1023        self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get("debug") else 90
   1024 
   1025        # metro browser sub process id
   1026        self.browserProcessId = None
   1027 
   1028        self.haveDumpedScreen = False
   1029        # Create variables to count the number of passes, fails, todos.
   1030        self.countpass = 0
   1031        self.countfail = 0
   1032        self.counttodo = 0
   1033 
   1034        self.expectedError = {}
   1035        self.result = {}
   1036 
   1037        self.start_script = os.path.join(here, "start_desktop.js")
   1038 
   1039        # Used to temporarily serve a performance profile
   1040        self.profiler_tempdir = None
   1041 
   1042    def environment(self, **kwargs):
   1043        kwargs["log"] = self.log
   1044        return test_environment(**kwargs)
   1045 
   1046    def getFullPath(self, path):
   1047        "Get an absolute path relative to self.oldcwd."
   1048        return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
   1049 
   1050    def getLogFilePath(self, logFile):
   1051        """return the log file path relative to the device we are testing on, in most cases
   1052        it will be the full path on the local system
   1053        """
   1054        return self.getFullPath(logFile)
   1055 
   1056    @property
   1057    def locations(self):
   1058        if self._locations is not None:
   1059            return self._locations
   1060        locations_file = os.path.join(SCRIPT_DIR, "server-locations.txt")
   1061        self._locations = ServerLocations(locations_file)
   1062        return self._locations
   1063 
   1064    def buildURLOptions(self, options, env):
   1065        """Add test control options from the command line to the url
   1066 
   1067        URL parameters to test URL:
   1068 
   1069        autorun -- kick off tests automatically
   1070        closeWhenDone -- closes the browser after the tests
   1071        hideResultsTable -- hides the table of individual test results
   1072        logFile -- logs test run to an absolute path
   1073        startAt -- name of test to start at
   1074        endAt -- name of test to end at
   1075        timeout -- per-test timeout in seconds
   1076        repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice.
   1077        """
   1078        self.urlOpts = []
   1079 
   1080        if not hasattr(options, "logFile"):
   1081            options.logFile = ""
   1082        if not hasattr(options, "fileLevel"):
   1083            options.fileLevel = "INFO"
   1084 
   1085        # allow relative paths for logFile
   1086        if options.logFile:
   1087            options.logFile = self.getLogFilePath(options.logFile)
   1088 
   1089        if options.flavor in ("a11y", "browser", "chrome"):
   1090            self.makeTestConfig(options)
   1091        else:
   1092            if options.autorun:
   1093                self.urlOpts.append("autorun=1")
   1094            if options.timeout:
   1095                self.urlOpts.append("timeout=%d" % options.timeout)
   1096            if options.maxTimeouts:
   1097                self.urlOpts.append("maxTimeouts=%d" % options.maxTimeouts)
   1098            if not options.keep_open:
   1099                self.urlOpts.append("closeWhenDone=1")
   1100            if options.logFile:
   1101                self.urlOpts.append("logFile=" + encodeURIComponent(options.logFile))
   1102                self.urlOpts.append(
   1103                    "fileLevel=" + encodeURIComponent(options.fileLevel)
   1104                )
   1105            if options.consoleLevel:
   1106                self.urlOpts.append(
   1107                    "consoleLevel=" + encodeURIComponent(options.consoleLevel)
   1108                )
   1109            if options.startAt:
   1110                self.urlOpts.append("startAt=%s" % options.startAt)
   1111            if options.endAt:
   1112                self.urlOpts.append("endAt=%s" % options.endAt)
   1113            if options.shuffle:
   1114                self.urlOpts.append("shuffle=1")
   1115            if "MOZ_HIDE_RESULTS_TABLE" in env and env["MOZ_HIDE_RESULTS_TABLE"] == "1":
   1116                self.urlOpts.append("hideResultsTable=1")
   1117            if options.runUntilFailure:
   1118                self.urlOpts.append("runUntilFailure=1")
   1119            if options.repeat:
   1120                self.urlOpts.append("repeat=%d" % options.repeat)
   1121            if len(options.test_paths) == 1 and os.path.isfile(
   1122                os.path.join(
   1123                    self.oldcwd,
   1124                    os.path.dirname(__file__),
   1125                    self.TEST_PATH,
   1126                    options.test_paths[0],
   1127                )
   1128            ):
   1129                self.urlOpts.append(
   1130                    "testname=%s" % "/".join([self.TEST_PATH, options.test_paths[0]])
   1131                )
   1132            if options.manifestFile:
   1133                self.urlOpts.append("manifestFile=%s" % options.manifestFile)
   1134            if options.failureFile:
   1135                self.urlOpts.append(
   1136                    "failureFile=%s" % self.getFullPath(options.failureFile)
   1137                )
   1138            if options.runSlower:
   1139                self.urlOpts.append("runSlower=true")
   1140            if options.debugOnFailure:
   1141                self.urlOpts.append("debugOnFailure=true")
   1142            if options.dumpOutputDirectory:
   1143                self.urlOpts.append(
   1144                    "dumpOutputDirectory=%s"
   1145                    % encodeURIComponent(options.dumpOutputDirectory)
   1146                )
   1147            if options.dumpAboutMemoryAfterTest:
   1148                self.urlOpts.append("dumpAboutMemoryAfterTest=true")
   1149            if options.dumpDMDAfterTest:
   1150                self.urlOpts.append("dumpDMDAfterTest=true")
   1151            if options.debugger or options.jsdebugger:
   1152                self.urlOpts.append("interactiveDebugger=true")
   1153            if options.jscov_dir_prefix:
   1154                self.urlOpts.append("jscovDirPrefix=%s" % options.jscov_dir_prefix)
   1155            if options.cleanupCrashes:
   1156                self.urlOpts.append("cleanupCrashes=true")
   1157            if "MOZ_XORIGIN_MOCHITEST" in env and env["MOZ_XORIGIN_MOCHITEST"] == "1":
   1158                options.xOriginTests = True
   1159            if options.xOriginTests:
   1160                self.urlOpts.append("xOriginTests=true")
   1161            if options.comparePrefs:
   1162                self.urlOpts.append("comparePrefs=true")
   1163            self.urlOpts.append("ignorePrefsFile=ignorePrefs.json")
   1164 
   1165    def normflavor(self, flavor):
   1166        """
   1167        In some places the string 'browser-chrome' is expected instead of
   1168        'browser' and 'mochitest' instead of 'plain'. Normalize the flavor
   1169        strings for those instances.
   1170        """
   1171        # TODO Use consistent flavor strings everywhere and remove this
   1172        if flavor == "browser":
   1173            return "browser-chrome"
   1174        elif flavor == "plain":
   1175            return "mochitest"
   1176        return flavor
   1177 
   1178    # This check can be removed when bug 983867 is fixed.
   1179    def isTest(self, options, filename):
   1180        allow_js_css = False
   1181        if options.flavor == "browser":
   1182            allow_js_css = True
   1183            testPattern = re.compile(r"browser_.+\.js")
   1184        elif options.flavor in ("a11y", "chrome"):
   1185            testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)")
   1186        else:
   1187            testPattern = re.compile(r"test_")
   1188 
   1189        if not allow_js_css and (".js" in filename or ".css" in filename):
   1190            return False
   1191 
   1192        pathPieces = filename.split("/")
   1193 
   1194        return testPattern.match(pathPieces[-1]) and not re.search(
   1195            r"\^headers\^$", filename
   1196        )
   1197 
   1198    def setTestRoot(self, options):
   1199        if options.flavor != "plain":
   1200            self.testRoot = options.flavor
   1201        else:
   1202            self.testRoot = self.TEST_PATH
   1203        self.testRootAbs = os.path.join(SCRIPT_DIR, self.testRoot)
   1204 
   1205    def buildTestURL(self, options, scheme="http"):
   1206        if scheme == "https":
   1207            testHost = "https://example.com:443"
   1208        elif options.xOriginTests:
   1209            testHost = "http://mochi.xorigin-test:8888"
   1210        else:
   1211            testHost = "http://mochi.test:8888"
   1212        testURL = "/".join([testHost, self.TEST_PATH])
   1213 
   1214        if len(options.test_paths) == 1:
   1215            if os.path.isfile(
   1216                os.path.join(
   1217                    self.oldcwd,
   1218                    os.path.dirname(__file__),
   1219                    self.TEST_PATH,
   1220                    options.test_paths[0],
   1221                )
   1222            ):
   1223                testURL = "/".join([testURL, os.path.dirname(options.test_paths[0])])
   1224            else:
   1225                testURL = "/".join([testURL, options.test_paths[0]])
   1226 
   1227        if options.flavor in ("a11y", "chrome"):
   1228            testURL = "/".join([testHost, self.CHROME_PATH])
   1229        elif options.flavor == "browser":
   1230            testURL = "about:blank"
   1231        return testURL
   1232 
   1233    def parseAndCreateTestsDirs(self, m):
   1234        testsDirs = list(self.tests_dirs_by_manifest[m])[0]
   1235        self.extraTestsDirs = []
   1236        if testsDirs:
   1237            self.extraTestsDirs = testsDirs.strip().split()
   1238            self.log.info(
   1239                "The following extra test directories will be created:\n  {}".format(
   1240                    "\n  ".join(self.extraTestsDirs)
   1241                )
   1242            )
   1243            self.createExtraTestsDirs(self.extraTestsDirs, m)
   1244 
   1245    def createExtraTestsDirs(self, extraTestsDirs=None, manifest=None):
   1246        """Take a list of directories that might be needed to exist by the test
   1247        prior to even the main process be executed, and:
   1248         - verify it does not already exists
   1249         - create it if it does
   1250        Removal of those directories is handled in cleanup()
   1251        """
   1252        if type(extraTestsDirs) is not list:
   1253            return
   1254 
   1255        for d in extraTestsDirs:
   1256            if os.path.exists(d):
   1257                raise FileExistsError(
   1258                    f"Directory '{d}' already exists. This is a member of "
   1259                    f"test-directories in manifest {manifest}."
   1260                )
   1261 
   1262        created = []
   1263        for d in extraTestsDirs:
   1264            os.makedirs(d)
   1265            created += [d]
   1266 
   1267        if created != extraTestsDirs:
   1268            raise OSError(
   1269                f"Not all directories were created: extraTestsDirs={extraTestsDirs} -- created={created}"
   1270            )
   1271 
   1272    def getTestsByScheme(
   1273        self, options, testsToFilter=None, disabled=True, manifestToFilter=None
   1274    ):
   1275        """Build the url path to the specific test harness and test file or directory
   1276        Build a manifest of tests to run and write out a json file for the harness to read
   1277        testsToFilter option is used to filter/keep the tests provided in the list
   1278 
   1279        disabled -- This allows to add all disabled tests on the build side
   1280                    and then on the run side to only run the enabled ones
   1281        """
   1282 
   1283        tests = self.getActiveTests(options, disabled)
   1284        paths = []
   1285        for test in tests:
   1286            if testsToFilter and (test["path"] not in testsToFilter):
   1287                continue
   1288            # If we are running a specific manifest, the previously computed set of active
   1289            # tests should be filtered out based on the manifest that contains that entry.
   1290            #
   1291            # This is especially important when a test file is listed in multiple
   1292            # manifests (e.g. because the same test runs under a different configuration,
   1293            # and so it is being included in multiple manifests), without filtering the
   1294            # active tests based on the current manifest (configuration) that we are
   1295            # running for each of the N manifests we would be executing the active tests
   1296            # exactly N times (and so NxN runs instead of the expected N runs, one for each
   1297            # manifest).
   1298            if manifestToFilter and (test["manifest"] not in manifestToFilter):
   1299                continue
   1300            paths.append(test)
   1301 
   1302        # Generate test by schemes
   1303        for scheme, grouped_tests in self.groupTestsByScheme(paths).items():
   1304            # Bug 883865 - add this functionality into manifestparser
   1305            with open(
   1306                os.path.join(SCRIPT_DIR, options.testRunManifestFile), "w"
   1307            ) as manifestFile:
   1308                manifestFile.write(json.dumps({"tests": grouped_tests}))
   1309            options.manifestFile = options.testRunManifestFile
   1310            yield (scheme, grouped_tests)
   1311 
   1312    def startWebSocketServer(self, options, debuggerInfo):
   1313        """Launch the websocket server"""
   1314        self.wsserver = WebSocketServer(options, SCRIPT_DIR, self.log, debuggerInfo)
   1315        self.wsserver.start()
   1316 
   1317    def startWebServer(self, options):
   1318        """Create the webserver and start it up"""
   1319 
   1320        self.server = MochitestServer(options, self.log)
   1321        self.server.start()
   1322 
   1323        if options.pidFile != "":
   1324            with open(options.pidFile + ".xpcshell.pid", "w") as f:
   1325                f.write("%s" % self.server._process.pid)
   1326 
   1327    def startWebsocketProcessBridge(self, options):
   1328        """Create a websocket server that can launch various processes that
   1329        JS needs (eg; ICE server for webrtc testing)
   1330        """
   1331 
   1332        command = [
   1333            sys.executable,
   1334            os.path.join("websocketprocessbridge", "websocketprocessbridge.py"),
   1335            "--port",
   1336            options.websocket_process_bridge_port,
   1337        ]
   1338        self.websocketProcessBridge = subprocess.Popen(command, cwd=SCRIPT_DIR)
   1339        self.log.info(
   1340            "runtests.py | websocket/process bridge pid: %d"
   1341            % self.websocketProcessBridge.pid
   1342        )
   1343 
   1344        # ensure the server is up, wait for at most ten seconds
   1345        for i in range(1, 100):
   1346            if self.websocketProcessBridge.poll() is not None:
   1347                self.log.error(
   1348                    "runtests.py | websocket/process bridge failed "
   1349                    "to launch. Are all the dependencies installed?"
   1350                )
   1351                return
   1352 
   1353            try:
   1354                sock = socket.create_connection(("127.0.0.1", 8191))
   1355                sock.close()
   1356                break
   1357            except Exception:
   1358                time.sleep(0.1)
   1359        else:
   1360            self.log.error(
   1361                "runtests.py | Timed out while waiting for "
   1362                "websocket/process bridge startup."
   1363            )
   1364 
   1365    def needsWebsocketProcessBridge(self, options):
   1366        """
   1367        Returns a bool indicating if the current test configuration needs
   1368        to start the websocket process bridge or not. The boils down to if
   1369        WebRTC tests that need the bridge are present.
   1370        """
   1371        tests = self.getActiveTests(options)
   1372        is_webrtc_tag_present = False
   1373        for test in tests:
   1374            if "webrtc" in test.get("tags", ""):
   1375                is_webrtc_tag_present = True
   1376                break
   1377        return is_webrtc_tag_present and options.subsuite in ["media"]
   1378 
   1379    def startHttp3Server(self, options):
   1380        """
   1381        Start a Http3 test server.
   1382        """
   1383        http3ServerPath = os.path.join(
   1384            options.utilityPath, "http3server" + mozinfo.info["bin_suffix"]
   1385        )
   1386        serverOptions = {}
   1387        serverOptions["http3ServerPath"] = http3ServerPath
   1388        serverOptions["profilePath"] = options.profilePath
   1389        serverOptions["isMochitest"] = True
   1390        serverOptions["isWin"] = mozinfo.isWin
   1391        serverOptions["proxyPort"] = options.http3ServerPort
   1392        env = test_environment(xrePath=options.xrePath, log=self.log)
   1393        serverEnv = env.copy()
   1394        serverLog = env.get("MOZHTTP3_SERVER_LOG")
   1395        if serverLog is not None:
   1396            serverEnv["RUST_LOG"] = serverLog
   1397        self.http3Server = Http3Server(serverOptions, serverEnv, self.log)
   1398        self.http3Server.start()
   1399 
   1400        port = self.http3Server.ports().get("MOZHTTP3_PORT_PROXY")
   1401        if int(port) != options.http3ServerPort:
   1402            self.http3Server = None
   1403            raise RuntimeError("Error: Unable to start Http/3 server")
   1404 
   1405    def findNodeBin(self):
   1406        # We try to find the node executable in the path given to us by the user in
   1407        # the MOZ_NODE_PATH environment variable
   1408        nodeBin = os.getenv("MOZ_NODE_PATH", None)
   1409        self.log.info("Use MOZ_NODE_PATH at %s" % (nodeBin))
   1410        if not nodeBin and build:
   1411            nodeBin = build.substs.get("NODEJS")
   1412            self.log.info("Use build node at %s" % (nodeBin))
   1413        return nodeBin
   1414 
   1415    def startHttp2Server(self, options):
   1416        """
   1417        Start a Http2 test server.
   1418        """
   1419        serverOptions = {}
   1420        serverOptions["serverPath"] = os.path.join(
   1421            SCRIPT_DIR, "Http2Server", "http2_server.js"
   1422        )
   1423        serverOptions["nodeBin"] = self.findNodeBin()
   1424        serverOptions["isWin"] = mozinfo.isWin
   1425        serverOptions["port"] = options.http2ServerPort
   1426        env = test_environment(xrePath=options.xrePath, log=self.log)
   1427        self.http2Server = Http2Server(serverOptions, env, self.log)
   1428        self.http2Server.start()
   1429 
   1430        port = self.http2Server.port()
   1431        if port != options.http2ServerPort:
   1432            raise RuntimeError("Error: Unable to start Http2 server")
   1433 
   1434    def startDoHServer(self, options, dstServerPort, alpn):
   1435        serverOptions = {}
   1436        serverOptions["serverPath"] = os.path.join(
   1437            SCRIPT_DIR, "DoHServer", "doh_server.js"
   1438        )
   1439        serverOptions["nodeBin"] = self.findNodeBin()
   1440        serverOptions["dstServerPort"] = dstServerPort
   1441        serverOptions["isWin"] = mozinfo.isWin
   1442        serverOptions["port"] = options.dohServerPort
   1443        serverOptions["alpn"] = alpn
   1444        env = test_environment(xrePath=options.xrePath, log=self.log)
   1445        self.dohServer = DoHServer(serverOptions, env, self.log)
   1446        self.dohServer.start()
   1447 
   1448        port = self.dohServer.port()
   1449        if port != options.dohServerPort:
   1450            raise RuntimeError("Error: Unable to start DoH server")
   1451 
   1452    def startServers(self, options, debuggerInfo, public=None):
   1453        # start servers and set ports
   1454        # TODO: pass these values, don't set on `self`
   1455        self.webServer = options.webServer
   1456        self.httpPort = options.httpPort
   1457        self.sslPort = options.sslPort
   1458        self.webSocketPort = options.webSocketPort
   1459 
   1460        # httpd-path is specified by standard makefile targets and may be specified
   1461        # on the command line to select a particular version of httpd.js. If not
   1462        # specified, try to select the one from hostutils.zip, as required in
   1463        # bug 882932.
   1464        if not options.httpdPath:
   1465            options.httpdPath = os.path.join(options.utilityPath, "components")
   1466 
   1467        self.startWebServer(options)
   1468        self.startWebSocketServer(options, debuggerInfo)
   1469 
   1470        # Only webrtc mochitests in the media suite need the websocketprocessbridge.
   1471        if self.needsWebsocketProcessBridge(options):
   1472            self.startWebsocketProcessBridge(options)
   1473 
   1474        # start SSL pipe
   1475        self.sslTunnel = SSLTunnel(options, logger=self.log)
   1476        self.sslTunnel.buildConfig(self.locations, public=public)
   1477        self.sslTunnel.start()
   1478 
   1479        # If we're lucky, the server has fully started by now, and all paths are
   1480        # ready, etc.  However, xpcshell cold start times suck, at least for debug
   1481        # builds.  We'll try to connect to the server for awhile, and if we fail,
   1482        # we'll try to kill the server and exit with an error.
   1483        if self.server is not None:
   1484            self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
   1485 
   1486        self.log.info("use http3 server: %d" % options.useHttp3Server)
   1487        self.http3Server = None
   1488        self.http2Server = None
   1489        self.dohServer = None
   1490        if options.useHttp3Server:
   1491            self.startHttp3Server(options)
   1492            self.startDoHServer(options, options.http3ServerPort, "h3")
   1493        elif options.useHttp2Server:
   1494            self.startHttp2Server(options)
   1495            self.startDoHServer(options, options.http2ServerPort, "h2")
   1496 
   1497    def stopServers(self):
   1498        """Servers are no longer needed, and perhaps more importantly, anything they
   1499        might spew to console might confuse things."""
   1500        if self.server is not None:
   1501            try:
   1502                self.log.info("Stopping web server")
   1503                self.server.stop()
   1504            except Exception:
   1505                self.log.critical("Exception when stopping web server")
   1506 
   1507        if self.wsserver is not None:
   1508            try:
   1509                self.log.info("Stopping web socket server")
   1510                self.wsserver.stop()
   1511            except Exception:
   1512                self.log.critical("Exception when stopping web socket server")
   1513 
   1514        if self.sslTunnel is not None:
   1515            try:
   1516                self.log.info("Stopping ssltunnel")
   1517                self.sslTunnel.stop()
   1518            except Exception:
   1519                self.log.critical("Exception stopping ssltunnel")
   1520 
   1521        if self.websocketProcessBridge is not None:
   1522            try:
   1523                self.websocketProcessBridge.kill()
   1524                self.websocketProcessBridge.wait()
   1525                self.log.info("Stopping websocket/process bridge")
   1526            except Exception:
   1527                self.log.critical("Exception stopping websocket/process bridge")
   1528        if self.http3Server is not None:
   1529            try:
   1530                self.http3Server.stop()
   1531            except Exception:
   1532                self.log.critical("Exception stopping http3 server")
   1533        if self.http2Server is not None:
   1534            try:
   1535                self.http2Server.stop()
   1536            except Exception:
   1537                self.log.critical("Exception stopping http2 server")
   1538        if self.dohServer is not None:
   1539            try:
   1540                self.dohServer.stop()
   1541            except Exception:
   1542                self.log.critical("Exception stopping doh server")
   1543 
   1544        if hasattr(self, "gstForV4l2loopbackProcess"):
   1545            try:
   1546                self.gstForV4l2loopbackProcess.kill()
   1547                self.gstForV4l2loopbackProcess.wait()
   1548                self.log.info("Stopping gst for v4l2loopback")
   1549            except Exception:
   1550                self.log.critical("Exception stopping gst for v4l2loopback")
   1551 
   1552    def copyExtraFilesToProfile(self, options):
   1553        "Copy extra files or dirs specified on the command line to the testing profile."
   1554        for f in options.extraProfileFiles:
   1555            abspath = self.getFullPath(f)
   1556            if os.path.isfile(abspath):
   1557                shutil.copy2(abspath, options.profilePath)
   1558            elif os.path.isdir(abspath):
   1559                dest = os.path.join(options.profilePath, os.path.basename(abspath))
   1560                shutil.copytree(abspath, dest)
   1561            else:
   1562                self.log.warning("runtests.py | Failed to copy %s to profile" % abspath)
   1563 
   1564    def getChromeTestDir(self, options):
   1565        dir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/"
   1566        if mozinfo.isWin:
   1567            dir = "file:///" + dir.replace("\\", "/")
   1568        return dir
   1569 
   1570    def writeChromeManifest(self, options):
   1571        manifest = os.path.join(options.profilePath, "tests.manifest")
   1572        with open(manifest, "w") as manifestFile:
   1573            # Register chrome directory.
   1574            chrometestDir = self.getChromeTestDir(options)
   1575            manifestFile.write(
   1576                "content mochitests %s contentaccessible=yes\n" % chrometestDir
   1577            )
   1578            manifestFile.write(
   1579                "content mochitests-any %s contentaccessible=yes remoteenabled=yes\n"
   1580                % chrometestDir
   1581            )
   1582            manifestFile.write(
   1583                "content mochitests-content %s contentaccessible=yes remoterequired=yes\n"
   1584                % chrometestDir
   1585            )
   1586 
   1587            if options.testingModulesDir is not None:
   1588                manifestFile.write(
   1589                    "resource testing-common file:///%s\n" % options.testingModulesDir
   1590                )
   1591        if options.store_chrome_manifest:
   1592            shutil.copyfile(manifest, options.store_chrome_manifest)
   1593        return manifest
   1594 
   1595    def addChromeToProfile(self, options):
   1596        "Adds MochiKit chrome tests to the profile."
   1597 
   1598        # Create (empty) chrome directory.
   1599        chromedir = os.path.join(options.profilePath, "chrome")
   1600        os.mkdir(chromedir)
   1601 
   1602        # Write userChrome.css.
   1603        chrome = """
   1604 /* set default namespace to XUL */
   1605 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
   1606 toolbar,
   1607 toolbarpalette {
   1608  background-color: rgb(235, 235, 235) !important;
   1609 }
   1610 toolbar#nav-bar {
   1611  background-image: none !important;
   1612 }
   1613 """
   1614        with open(
   1615            os.path.join(options.profilePath, "userChrome.css"), "a"
   1616        ) as chromeFile:
   1617            chromeFile.write(chrome)
   1618 
   1619        manifest = self.writeChromeManifest(options)
   1620 
   1621        return manifest
   1622 
   1623    def getExtensionsToInstall(self, options):
   1624        "Return a list of extensions to install in the profile"
   1625        extensions = []
   1626        appDir = (
   1627            options.app[: options.app.rfind(os.sep)]
   1628            if options.app
   1629            else options.utilityPath
   1630        )
   1631 
   1632        extensionDirs = [
   1633            # Extensions distributed with the test harness.
   1634            os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
   1635        ]
   1636        if appDir:
   1637            # Extensions distributed with the application.
   1638            extensionDirs.append(os.path.join(appDir, "distribution", "extensions"))
   1639 
   1640        for extensionDir in extensionDirs:
   1641            if os.path.isdir(extensionDir):
   1642                for dirEntry in os.listdir(extensionDir):
   1643                    if dirEntry not in options.extensionsToExclude:
   1644                        path = os.path.join(extensionDir, dirEntry)
   1645                        if os.path.isdir(path) or (
   1646                            os.path.isfile(path) and path.endswith(".xpi")
   1647                        ):
   1648                            extensions.append(path)
   1649        extensions.extend(options.extensionsToInstall)
   1650        return extensions
   1651 
   1652    def logPreamble(self, tests):
   1653        """Logs a suite_start message and test_start/test_end at the beginning of a run."""
   1654        self.log.suite_start(self.tests_by_manifest, name=f"mochitest-{self.flavor}")
   1655        for test in tests:
   1656            if "disabled" in test:
   1657                self.log.test_start(test["path"])
   1658                self.log.test_end(test["path"], "SKIP", message=test["disabled"])
   1659 
   1660    def loadFailurePatternFile(self, pat_file):
   1661        if pat_file in self.patternFiles:
   1662            return self.patternFiles[pat_file]
   1663        if not os.path.isfile(pat_file):
   1664            self.log.warning(
   1665                "runtests.py | Cannot find failure pattern file " + pat_file
   1666            )
   1667            return None
   1668 
   1669        # Using ":error" to ensure it shows up in the failure summary.
   1670        self.log.warning(
   1671            f"[runtests.py:error] Using {pat_file} to filter failures. If there "
   1672            "is any number mismatch below, you could have fixed "
   1673            "something documented in that file. Please reduce the "
   1674            "failure count appropriately."
   1675        )
   1676        patternRE = re.compile(
   1677            r"""
   1678            ^\s*\*\s*               # list bullet
   1679            (test_\S+|\.{3})        # test name
   1680            (?:\s*(`.+?`|asserts))? # failure pattern
   1681            (?::.+)?                # optional description
   1682            \s*\[(\d+|\*)\]         # expected count
   1683            \s*$
   1684        """,
   1685            re.X,
   1686        )
   1687        patterns = {}
   1688        with open(pat_file) as f:
   1689            last_name = None
   1690            for line in f:
   1691                match = patternRE.match(line)
   1692                if not match:
   1693                    continue
   1694                name = match.group(1)
   1695                name = last_name if name == "..." else name
   1696                last_name = name
   1697                pat = match.group(2)
   1698                if pat is not None:
   1699                    pat = "ASSERTION" if pat == "asserts" else pat[1:-1]
   1700                count = match.group(3)
   1701                count = None if count == "*" else int(count)
   1702                if name not in patterns:
   1703                    patterns[name] = []
   1704                patterns[name].append((pat, count))
   1705        self.patternFiles[pat_file] = patterns
   1706        return patterns
   1707 
   1708    def getFailurePatterns(self, pat_file, test_name):
   1709        patterns = self.loadFailurePatternFile(pat_file)
   1710        if patterns:
   1711            return patterns.get(test_name, None)
   1712 
   1713    def getActiveTests(self, options, disabled=True):
   1714        """
   1715        This method is used to parse the manifest and return active filtered tests.
   1716        """
   1717        if self._active_tests:
   1718            return self._active_tests
   1719 
   1720        tests = []
   1721        manifest = self.getTestManifest(options)
   1722        if manifest:
   1723            if options.extra_mozinfo_json:
   1724                mozinfo.update(options.extra_mozinfo_json)
   1725 
   1726            info = mozinfo.info
   1727 
   1728            filters = [
   1729                subsuite(options.subsuite),
   1730            ]
   1731 
   1732            if options.test_tags:
   1733                filters.append(tags(options.test_tags))
   1734 
   1735            if options.test_paths:
   1736                options.test_paths = self.normalize_paths(options.test_paths)
   1737                filters.append(pathprefix(options.test_paths))
   1738 
   1739            # Add chunking filters if specified
   1740            if options.totalChunks:
   1741                if options.chunkByDir:
   1742                    filters.append(
   1743                        chunk_by_dir(
   1744                            options.thisChunk, options.totalChunks, options.chunkByDir
   1745                        )
   1746                    )
   1747                elif options.chunkByRuntime:
   1748                    if mozinfo.info["os"] == "android":
   1749                        platkey = "android"
   1750                    elif mozinfo.isWin:
   1751                        platkey = "windows"
   1752                    else:
   1753                        platkey = "unix"
   1754 
   1755                    runtime_file = os.path.join(
   1756                        SCRIPT_DIR,
   1757                        "runtimes",
   1758                        f"manifest-runtimes-{platkey}.json",
   1759                    )
   1760                    if not os.path.exists(runtime_file):
   1761                        self.log.error("runtime file %s not found!" % runtime_file)
   1762                        sys.exit(1)
   1763 
   1764                    # Given the mochitest flavor, load the runtimes information
   1765                    # for only that flavor due to manifest runtime format change in Bug 1637463.
   1766                    with open(runtime_file) as f:
   1767                        if "suite_name" in options:
   1768                            runtimes = json.load(f).get(options.suite_name, {})
   1769                        else:
   1770                            runtimes = {}
   1771 
   1772                    filters.append(
   1773                        chunk_by_runtime(
   1774                            options.thisChunk, options.totalChunks, runtimes
   1775                        )
   1776                    )
   1777                else:
   1778                    filters.append(
   1779                        chunk_by_slice(options.thisChunk, options.totalChunks)
   1780                    )
   1781 
   1782            noDefaultFilters = False
   1783            if options.runFailures:
   1784                filters.append(failures(options.runFailures))
   1785                noDefaultFilters = True
   1786 
   1787            # TODO: remove this when crashreporter is fixed on mac via bug 1910777
   1788            if info["os"] == "mac" and info["os_version"].split(".")[0] in ["14", "15"]:
   1789                info["crashreporter"] = False
   1790 
   1791            tests = manifest.active_tests(
   1792                exists=False,
   1793                disabled=disabled,
   1794                filters=filters,
   1795                noDefaultFilters=noDefaultFilters,
   1796                strictExpressions=True,
   1797                **info,
   1798            )
   1799 
   1800            if len(tests) == 0:
   1801                self.log.error(
   1802                    NO_TESTS_FOUND.format(options.flavor, manifest.fmt_filters())
   1803                )
   1804 
   1805        paths = []
   1806        for test in tests:
   1807            if len(tests) == 1 and "disabled" in test:
   1808                del test["disabled"]
   1809 
   1810            pathAbs = os.path.abspath(test["path"])
   1811            assert os.path.normcase(pathAbs).startswith(
   1812                os.path.normcase(self.testRootAbs)
   1813            )
   1814            tp = pathAbs[len(self.testRootAbs) :].replace("\\", "/").strip("/")
   1815 
   1816            if not self.isTest(options, tp):
   1817                self.log.warning(
   1818                    "Warning: %s from manifest %s is not a valid test"
   1819                    % (test["name"], test["manifest"])
   1820                )
   1821                continue
   1822 
   1823            manifest_key = test["manifest_relpath"]
   1824            # Ignore ancestor_manifests that live at the root (e.g, don't have a
   1825            # path separator).
   1826            if "ancestor_manifest" in test and "/" in normsep(
   1827                test["ancestor_manifest"]
   1828            ):
   1829                manifest_key = "{}:{}".format(test["ancestor_manifest"], manifest_key)
   1830 
   1831            manifest_key = manifest_key.replace("\\", "/")
   1832            self.tests_by_manifest[manifest_key].append(tp)
   1833            self.args_by_manifest[manifest_key].add(test.get("args"))
   1834            self.prefs_by_manifest[manifest_key].add(test.get("prefs"))
   1835            self.env_vars_by_manifest[manifest_key].add(test.get("environment"))
   1836            self.tests_dirs_by_manifest[manifest_key].add(test.get("test-directories"))
   1837 
   1838            for key in ["args", "prefs", "environment", "test-directories"]:
   1839                if key in test and not options.runByManifest and "disabled" not in test:
   1840                    self.log.error(
   1841                        "parsing {}: runByManifest mode must be enabled to "
   1842                        "set the `{}` key".format(test["manifest_relpath"], key)
   1843                    )
   1844                    sys.exit(1)
   1845 
   1846            testob = {"path": tp, "manifest": manifest_key}
   1847            if "disabled" in test:
   1848                testob["disabled"] = test["disabled"]
   1849            if "expected" in test:
   1850                testob["expected"] = test["expected"]
   1851            if "https_first_disabled" in test:
   1852                testob["https_first_disabled"] = test["https_first_disabled"] == "true"
   1853            if "allow_xul_xbl" in test:
   1854                testob["allow_xul_xbl"] = test["allow_xul_xbl"] == "true"
   1855            if "scheme" in test:
   1856                testob["scheme"] = test["scheme"]
   1857            if "tags" in test:
   1858                testob["tags"] = test["tags"]
   1859            if options.failure_pattern_file:
   1860                pat_file = os.path.join(
   1861                    os.path.dirname(test["manifest"]), options.failure_pattern_file
   1862                )
   1863                patterns = self.getFailurePatterns(pat_file, test["name"])
   1864                if patterns:
   1865                    testob["expected"] = patterns
   1866            paths.append(testob)
   1867 
   1868        # The 'args' key needs to be set in the DEFAULT section, unfortunately
   1869        # we can't tell what comes from DEFAULT or not. So to validate this, we
   1870        # stash all args from tests in the same manifest into a set. If the
   1871        # length of the set > 1, then we know 'args' didn't come from DEFAULT.
   1872        args_not_default = [m for m, p in self.args_by_manifest.items() if len(p) > 1]
   1873        if args_not_default:
   1874            self.log.error(
   1875                "The 'args' key must be set in the DEFAULT section of a "
   1876                "manifest. Fix the following manifests: {}".format(
   1877                    "\n".join(args_not_default)
   1878                )
   1879            )
   1880            sys.exit(1)
   1881 
   1882        # The 'prefs' key needs to be set in the DEFAULT section too.
   1883        pref_not_default = [m for m, p in self.prefs_by_manifest.items() if len(p) > 1]
   1884        if pref_not_default:
   1885            self.log.error(
   1886                "The 'prefs' key must be set in the DEFAULT section of a "
   1887                "manifest. Fix the following manifests: {}".format(
   1888                    "\n".join(pref_not_default)
   1889                )
   1890            )
   1891            sys.exit(1)
   1892        # The 'environment' key needs to be set in the DEFAULT section too.
   1893        env_not_default = [
   1894            m for m, p in self.env_vars_by_manifest.items() if len(p) > 1
   1895        ]
   1896        if env_not_default:
   1897            self.log.error(
   1898                "The 'environment' key must be set in the DEFAULT section of a "
   1899                "manifest. Fix the following manifests: {}".format(
   1900                    "\n".join(env_not_default)
   1901                )
   1902            )
   1903            sys.exit(1)
   1904 
   1905        paths.sort(key=lambda p: p["path"].split("/"))
   1906        if options.dump_tests:
   1907            options.dump_tests = os.path.expanduser(options.dump_tests)
   1908            assert os.path.exists(os.path.dirname(options.dump_tests))
   1909            with open(options.dump_tests, "w") as dumpFile:
   1910                dumpFile.write(json.dumps({"active_tests": paths}))
   1911 
   1912            self.log.info("Dumping active_tests to %s file." % options.dump_tests)
   1913            sys.exit()
   1914 
   1915        # Upload a list of test manifests that were executed in this run.
   1916        if "MOZ_UPLOAD_DIR" in os.environ:
   1917            artifact = os.path.join(os.environ["MOZ_UPLOAD_DIR"], "manifests.list")
   1918            with open(artifact, "a") as fh:
   1919                fh.write("\n".join(sorted(self.tests_by_manifest.keys())))
   1920 
   1921        self._active_tests = paths
   1922        return self._active_tests
   1923 
   1924    def getTestManifest(self, options):
   1925        if isinstance(options.manifestFile, TestManifest):
   1926            manifest = options.manifestFile
   1927        elif options.manifestFile and os.path.isfile(options.manifestFile):
   1928            manifestFileAbs = os.path.abspath(options.manifestFile)
   1929            assert manifestFileAbs.startswith(SCRIPT_DIR)
   1930            manifest = TestManifest([options.manifestFile], strict=False)
   1931        elif options.manifestFile and os.path.isfile(
   1932            os.path.join(SCRIPT_DIR, options.manifestFile)
   1933        ):
   1934            manifestFileAbs = os.path.abspath(
   1935                os.path.join(SCRIPT_DIR, options.manifestFile)
   1936            )
   1937            assert manifestFileAbs.startswith(SCRIPT_DIR)
   1938            manifest = TestManifest([manifestFileAbs], strict=False)
   1939        else:
   1940            masterName = self.normflavor(options.flavor) + ".toml"
   1941            masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
   1942 
   1943            if not os.path.exists(masterPath):
   1944                masterName = self.normflavor(options.flavor) + ".ini"
   1945                masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
   1946 
   1947            if os.path.exists(masterPath):
   1948                manifest = TestManifest([masterPath], strict=False)
   1949            else:
   1950                manifest = None
   1951                self.log.warning(
   1952                    "TestManifest masterPath %s does not exist" % masterPath
   1953                )
   1954 
   1955        return manifest
   1956 
   1957    def makeTestConfig(self, options):
   1958        "Creates a test configuration file for customizing test execution."
   1959        options.logFile = options.logFile.replace("\\", "\\\\")
   1960 
   1961        if (
   1962            "MOZ_HIDE_RESULTS_TABLE" in os.environ
   1963            and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1"
   1964        ):
   1965            options.hideResultsTable = True
   1966 
   1967        # strip certain unnecessary items to avoid serialization errors in json.dumps()
   1968        d = dict(
   1969            (k, v)
   1970            for k, v in options.__dict__.items()
   1971            if (v is None) or isinstance(v, (str, numbers.Number))
   1972        )
   1973        d["testRoot"] = self.testRoot
   1974        if options.jscov_dir_prefix:
   1975            d["jscovDirPrefix"] = options.jscov_dir_prefix
   1976        if not options.keep_open:
   1977            d["closeWhenDone"] = "1"
   1978 
   1979        d["runFailures"] = False
   1980        if options.runFailures:
   1981            d["runFailures"] = True
   1982 
   1983        shutil.copy(
   1984            os.path.join(SCRIPT_DIR, "ignorePrefs.json"),
   1985            os.path.join(options.profilePath, "ignorePrefs.json"),
   1986        )
   1987        d["ignorePrefsFile"] = "ignorePrefs.json"
   1988        content = json.dumps(d)
   1989 
   1990        with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
   1991            config.write(content)
   1992 
   1993    def buildBrowserEnv(self, options, debugger=False, env=None):
   1994        """build the environment variables for the specific test and operating system"""
   1995        if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
   1996            useLSan = True
   1997        else:
   1998            useLSan = False
   1999 
   2000        browserEnv = self.environment(
   2001            xrePath=options.xrePath, env=env, debugger=debugger, useLSan=useLSan
   2002        )
   2003 
   2004        if options.headless:
   2005            browserEnv["MOZ_HEADLESS"] = "1"
   2006 
   2007        if not options.e10s:
   2008            browserEnv["MOZ_FORCE_DISABLE_E10S"] = "1"
   2009 
   2010        if options.dmd:
   2011            browserEnv["DMD"] = os.environ.get("DMD", "1")
   2012 
   2013        # bug 1443327: do not set MOZ_CRASHREPORTER_SHUTDOWN during browser-chrome
   2014        # tests, since some browser-chrome tests test content process crashes;
   2015        # also exclude non-e10s since at least one non-e10s mochitest is problematic
   2016        if (
   2017            options.flavor == "browser" or not options.e10s
   2018        ) and "MOZ_CRASHREPORTER_SHUTDOWN" in browserEnv:
   2019            del browserEnv["MOZ_CRASHREPORTER_SHUTDOWN"]
   2020 
   2021        try:
   2022            browserEnv.update(
   2023                dict(
   2024                    parse_key_value(
   2025                        self.extraEnv, context="environment variable in manifest"
   2026                    )
   2027                )
   2028            )
   2029        except KeyValueParseError as e:
   2030            self.log.error(str(e))
   2031            return None
   2032 
   2033        # These variables are necessary for correct application startup; change
   2034        # via the commandline at your own risk.
   2035        browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
   2036 
   2037        # interpolate environment passed with options
   2038        try:
   2039            browserEnv.update(
   2040                dict(parse_key_value(options.environment, context="--setenv"))
   2041            )
   2042        except KeyValueParseError as e:
   2043            self.log.error(str(e))
   2044            return None
   2045 
   2046        if (
   2047            "MOZ_PROFILER_STARTUP_FEATURES" not in browserEnv
   2048            or "nativeallocations"
   2049            not in browserEnv["MOZ_PROFILER_STARTUP_FEATURES"].split(",")
   2050        ):
   2051            # Only turn on the bloat log if the profiler's native allocation feature is
   2052            # not enabled. The two are not compatible.
   2053            browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
   2054 
   2055        # If profiling options are enabled, turn on the gecko profiler by using the
   2056        # profiler environmental variables.
   2057        if options.profiler:
   2058            if "MOZ_PROFILER_SHUTDOWN" not in os.environ:
   2059                # The user wants to capture a profile, and automatically view it. The
   2060                # profile will be saved to a temporary folder, then deleted after
   2061                # opening in profiler.firefox.com.
   2062                self.profiler_tempdir = tempfile.mkdtemp()
   2063                browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join(
   2064                    self.profiler_tempdir, "profile_mochitest.json"
   2065                )
   2066            else:
   2067                profile_path = Path(os.getenv("MOZ_PROFILER_SHUTDOWN"))
   2068                if profile_path.suffix == "":
   2069                    if not profile_path.exists():
   2070                        profile_path.mkdir(parents=True, exist_ok=True)
   2071                    profile_path = profile_path / "profile_mochitest.json"
   2072                elif not profile_path.parent.exists():
   2073                    profile_path.parent.mkdir(parents=True, exist_ok=True)
   2074                browserEnv["MOZ_PROFILER_SHUTDOWN"] = str(profile_path)
   2075 
   2076            browserEnv["MOZ_PROFILER_STARTUP"] = "1"
   2077 
   2078        if options.profilerSaveOnly:
   2079            # The user wants to capture a profile, but only to save it. This defaults
   2080            # to the MOZ_UPLOAD_DIR.
   2081            browserEnv["MOZ_PROFILER_STARTUP"] = "1"
   2082            if "MOZ_UPLOAD_DIR" in browserEnv:
   2083                browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join(
   2084                    browserEnv["MOZ_UPLOAD_DIR"], "profile_mochitest.json"
   2085                )
   2086            else:
   2087                self.log.error(
   2088                    "--profiler-save-only was specified, but no MOZ_UPLOAD_DIR "
   2089                    "environment variable was provided. Please set this "
   2090                    "environment variable to a directory path in order to save "
   2091                    "a performance profile."
   2092                )
   2093                return None
   2094 
   2095        try:
   2096            gmp_path = self.getGMPPluginPath(options)
   2097            if gmp_path is not None:
   2098                browserEnv["MOZ_GMP_PATH"] = gmp_path
   2099        except OSError:
   2100            self.log.error("Could not find path to gmp-fake plugin!")
   2101            return None
   2102 
   2103        if options.fatalAssertions:
   2104            browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
   2105 
   2106        # Produce a mozlog, if setup (see MOZ_LOG global at the top of
   2107        # this script).
   2108        self.mozLogs = MOZ_LOG and "MOZ_UPLOAD_DIR" in os.environ
   2109        if self.mozLogs:
   2110            browserEnv["MOZ_LOG"] = MOZ_LOG
   2111 
   2112        return browserEnv
   2113 
   2114    def killNamedProc(self, pname, orphans=True):
   2115        """Kill processes matching the given command name"""
   2116        self.log.info("Checking for %s processes..." % pname)
   2117 
   2118        if HAVE_PSUTIL:
   2119            for proc in psutil.process_iter():
   2120                try:
   2121                    if proc.name() == pname:
   2122                        procd = proc.as_dict(attrs=["pid", "ppid", "name", "username"])
   2123                        if proc.ppid() == 1 or not orphans:
   2124                            self.log.info("killing %s" % procd)
   2125                            killPid(proc.pid, self.log)
   2126                        else:
   2127                            self.log.info("NOT killing %s (not an orphan?)" % procd)
   2128                except Exception as e:
   2129                    self.log.info(
   2130                        "Warning: Unable to kill process %s: %s" % (pname, str(e))
   2131                    )
   2132                    # may not be able to access process info for all processes
   2133                    continue
   2134        else:
   2135 
   2136            def _psInfo(_, line):
   2137                if pname in line:
   2138                    self.log.info(line)
   2139 
   2140            mozprocess.run_and_wait(
   2141                ["ps", "-f"],
   2142                output_line_handler=_psInfo,
   2143            )
   2144 
   2145            def _psKill(_, line):
   2146                parts = line.split()
   2147                if len(parts) == 3 and parts[0].isdigit():
   2148                    pid = int(parts[0])
   2149                    ppid = int(parts[1])
   2150                    if parts[2] == pname:
   2151                        if ppid == 1 or not orphans:
   2152                            self.log.info("killing %s (pid %d)" % (pname, pid))
   2153                            killPid(pid, self.log)
   2154                        else:
   2155                            self.log.info(
   2156                                "NOT killing %s (pid %d) (not an orphan?)"
   2157                                % (pname, pid)
   2158                            )
   2159 
   2160            mozprocess.run_and_wait(
   2161                ["ps", "-o", "pid,ppid,comm"],
   2162                output_line_handler=_psKill,
   2163            )
   2164 
   2165    def execute_start_script(self):
   2166        if not self.start_script or not self.marionette:
   2167            return
   2168 
   2169        if os.path.isfile(self.start_script):
   2170            with open(self.start_script) as fh:
   2171                script = fh.read()
   2172        else:
   2173            script = self.start_script
   2174 
   2175        with self.marionette.using_context("chrome"):
   2176            return self.marionette.execute_script(
   2177                script, script_args=(self.start_script_kwargs,)
   2178            )
   2179 
   2180    def fillCertificateDB(self, options):
   2181        # TODO: move -> mozprofile:
   2182        # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35
   2183 
   2184        pwfilePath = os.path.join(options.profilePath, ".crtdbpw")
   2185        with open(pwfilePath, "w") as pwfile:
   2186            pwfile.write("\n")
   2187 
   2188        # Pre-create the certification database for the profile
   2189        env = self.environment(xrePath=options.xrePath)
   2190        env["LD_LIBRARY_PATH"] = options.xrePath
   2191        bin_suffix = mozinfo.info.get("bin_suffix", "")
   2192        certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix)
   2193        pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix)
   2194        toolsEnv = env
   2195        if "browser.newtabpage.trainhopAddon.version=any" in options.extraPrefs:
   2196            toolsEnv["LD_LIBRARY_PATH"] = os.path.join(os.path.dirname(here), "bin")
   2197        if mozinfo.info["asan"]:
   2198            # Disable leak checking when running these tools
   2199            toolsEnv["ASAN_OPTIONS"] = "detect_leaks=0"
   2200        if mozinfo.info["tsan"]:
   2201            # Disable race checking when running these tools
   2202            toolsEnv["TSAN_OPTIONS"] = "report_bugs=0"
   2203 
   2204        if self.certdbNew:
   2205            # android uses the new DB formats exclusively
   2206            certdbPath = "sql:" + options.profilePath
   2207        else:
   2208            # desktop seems to use the old
   2209            certdbPath = options.profilePath
   2210 
   2211        # certutil.exe depends on some DLLs in the app directory
   2212        # When running tests against an MSIX-installed Firefox, these DLLs
   2213        # cannot be used out of the install directory, they must be copied
   2214        # elsewhere first.
   2215        if "WindowsApps" in options.app:
   2216            install_dir = os.path.dirname(options.app)
   2217            for f in os.listdir(install_dir):
   2218                if f.endswith(".dll"):
   2219                    shutil.copy(os.path.join(install_dir, f), options.utilityPath)
   2220 
   2221        status = call(
   2222            [certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=toolsEnv
   2223        )
   2224        if status:
   2225            return status
   2226 
   2227        # Walk the cert directory and add custom CAs and client certs
   2228        files = os.listdir(options.certPath)
   2229        for item in files:
   2230            root, ext = os.path.splitext(item)
   2231            if ext == ".ca":
   2232                trustBits = "CT,,"
   2233                if root.endswith("-object"):
   2234                    trustBits = "CT,,CT"
   2235                call(
   2236                    [
   2237                        certutil,
   2238                        "-A",
   2239                        "-i",
   2240                        os.path.join(options.certPath, item),
   2241                        "-d",
   2242                        certdbPath,
   2243                        "-f",
   2244                        pwfilePath,
   2245                        "-n",
   2246                        root,
   2247                        "-t",
   2248                        trustBits,
   2249                    ],
   2250                    env=toolsEnv,
   2251                )
   2252            elif ext == ".client":
   2253                call(
   2254                    [
   2255                        pk12util,
   2256                        "-i",
   2257                        os.path.join(options.certPath, item),
   2258                        "-w",
   2259                        pwfilePath,
   2260                        "-d",
   2261                        certdbPath,
   2262                    ],
   2263                    env=toolsEnv,
   2264                )
   2265 
   2266        os.unlink(pwfilePath)
   2267        return 0
   2268 
   2269    def findFreePort(self, type):
   2270        with closing(socket.socket(socket.AF_INET, type)) as s:
   2271            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   2272            s.bind(("127.0.0.1", 0))
   2273            return s.getsockname()[1]
   2274 
   2275    def proxy(self, options):
   2276        # proxy
   2277        # use SSL port for legacy compatibility; see
   2278        # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
   2279        # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
   2280        # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
   2281        #             'ws': str(self.webSocketPort)
   2282        proxyOptions = {
   2283            "remote": options.webServer,
   2284            "http": options.httpPort,
   2285            "https": options.sslPort,
   2286            "ws": options.sslPort,
   2287        }
   2288 
   2289        if options.useHttp3Server:
   2290            options.dohServerPort = self.findFreePort(socket.SOCK_STREAM)
   2291            options.http3ServerPort = self.findFreePort(socket.SOCK_DGRAM)
   2292            proxyOptions["dohServerPort"] = options.dohServerPort
   2293            self.log.info("use doh server at port: %d" % options.dohServerPort)
   2294            self.log.info("use http3 server at port: %d" % options.http3ServerPort)
   2295        elif options.useHttp2Server:
   2296            options.dohServerPort = self.findFreePort(socket.SOCK_STREAM)
   2297            options.http2ServerPort = self.findFreePort(socket.SOCK_STREAM)
   2298            proxyOptions["dohServerPort"] = options.dohServerPort
   2299            self.log.info("use doh server at port: %d" % options.dohServerPort)
   2300            self.log.info("use http2 server at port: %d" % options.http2ServerPort)
   2301        return proxyOptions
   2302 
   2303    def merge_base_profiles(self, options, category):
   2304        """Merge extra profile data from testing/profiles."""
   2305 
   2306        # In test packages used in CI, the profile_data directory is installed
   2307        # in the SCRIPT_DIR.
   2308        profile_data_dir = os.path.join(SCRIPT_DIR, "profile_data")
   2309        # If possible, read profile data from topsrcdir. This prevents us from
   2310        # requiring a re-build to pick up newly added extensions in the
   2311        # <profile>/extensions directory.
   2312        if build_obj:
   2313            path = os.path.join(build_obj.topsrcdir, "testing", "profiles")
   2314            if os.path.isdir(path):
   2315                profile_data_dir = path
   2316        # Still not found? Look for testing/profiles relative to testing/mochitest.
   2317        if not os.path.isdir(profile_data_dir):
   2318            path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "profiles"))
   2319            if os.path.isdir(path):
   2320                profile_data_dir = path
   2321 
   2322        with open(os.path.join(profile_data_dir, "profiles.json")) as fh:
   2323            base_profiles = json.load(fh)[category]
   2324 
   2325        # values to use when interpolating preferences
   2326        interpolation = {
   2327            "server": "%s:%s" % (options.webServer, options.httpPort),
   2328        }
   2329 
   2330        for profile in base_profiles:
   2331            path = os.path.join(profile_data_dir, profile)
   2332            self.profile.merge(path, interpolation=interpolation)
   2333 
   2334    @property
   2335    def conditioned_profile_copy(self):
   2336        """Returns a copy of the original conditioned profile that was created."""
   2337 
   2338        condprof_copy = os.path.join(tempfile.mkdtemp(), "profile")
   2339        shutil.copytree(
   2340            self.conditioned_profile_dir,
   2341            condprof_copy,
   2342            ignore=shutil.ignore_patterns("lock"),
   2343        )
   2344        self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
   2345        return condprof_copy
   2346 
   2347    def downloadConditionedProfile(self, profile_scenario, app):
   2348        from condprof.client import get_profile
   2349        from condprof.util import get_current_platform, get_version
   2350 
   2351        if self.conditioned_profile_dir:
   2352            # We already have a directory, so provide a copy that
   2353            # will get deleted after it's done with
   2354            return self.conditioned_profile_copy
   2355 
   2356        temp_download_dir = tempfile.mkdtemp()
   2357 
   2358        # Call condprof's client API to yield our platform-specific
   2359        # conditioned-profile binary
   2360        platform = get_current_platform()
   2361 
   2362        if not profile_scenario:
   2363            profile_scenario = "settled"
   2364 
   2365        version = get_version(app)
   2366        try:
   2367            cond_prof_target_dir = get_profile(
   2368                temp_download_dir,
   2369                platform,
   2370                profile_scenario,
   2371                repo="mozilla-central",
   2372                version=version,
   2373                retries=2,  # quicker failure
   2374            )
   2375        except Exception:
   2376            if version is None:
   2377                # any other error is a showstopper
   2378                self.log.critical("Could not get the conditioned profile")
   2379                traceback.print_exc()
   2380                raise
   2381            version = None
   2382            try:
   2383                self.log.info("retrying a profile with no version specified")
   2384                cond_prof_target_dir = get_profile(
   2385                    temp_download_dir,
   2386                    platform,
   2387                    profile_scenario,
   2388                    repo="mozilla-central",
   2389                    version=version,
   2390                )
   2391            except Exception:
   2392                self.log.critical("Could not get the conditioned profile")
   2393                traceback.print_exc()
   2394                raise
   2395 
   2396        # Now get the full directory path to our fetched conditioned profile
   2397        self.conditioned_profile_dir = os.path.join(
   2398            temp_download_dir, cond_prof_target_dir
   2399        )
   2400        if not os.path.exists(cond_prof_target_dir):
   2401            self.log.critical(
   2402                f"Can't find target_dir {cond_prof_target_dir}, from get_profile()"
   2403                f"temp_download_dir {temp_download_dir}, platform {platform}, scenario {profile_scenario}"
   2404            )
   2405            raise OSError
   2406 
   2407        self.log.info(
   2408            f"Original self.conditioned_profile_dir is now set: {self.conditioned_profile_dir}"
   2409        )
   2410        return self.conditioned_profile_copy
   2411 
   2412    def buildProfile(self, options):
   2413        """create the profile and add optional chrome bits and files if requested"""
   2414        # get extensions to install
   2415        extensions = self.getExtensionsToInstall(options)
   2416 
   2417        # Whitelist the _tests directory (../..) so that TESTING_JS_MODULES work
   2418        tests_dir = os.path.dirname(os.path.dirname(SCRIPT_DIR))
   2419        sandbox_allowlist_paths = [tests_dir] + options.sandboxReadWhitelist
   2420        if platform.system() == "Linux" or platform.system() in (
   2421            "Windows",
   2422            "Microsoft",
   2423        ):
   2424            # Trailing slashes are needed to indicate directories on Linux and Windows
   2425            sandbox_allowlist_paths = [
   2426                os.path.join(p, "") for p in sandbox_allowlist_paths
   2427            ]
   2428 
   2429        if options.conditionedProfile:
   2430            if options.profilePath and os.path.exists(options.profilePath):
   2431                shutil.rmtree(options.profilePath, ignore_errors=True)
   2432            options.profilePath = self.downloadConditionedProfile("full", options.app)
   2433 
   2434            # This is causing `certutil -N -d -f`` to not use -f (pwd file)
   2435            try:
   2436                os.remove(os.path.join(options.profilePath, "key4.db"))
   2437            except Exception as e:
   2438                self.log.info(
   2439                    "Caught exception while removing key4.db"
   2440                    "during setup of conditioned profile: %s" % e
   2441                )
   2442 
   2443        # Create the profile
   2444        self.profile = Profile(
   2445            profile=options.profilePath,
   2446            addons=extensions,
   2447            locations=self.locations,
   2448            proxy=self.proxy(options),
   2449            allowlistpaths=sandbox_allowlist_paths,
   2450        )
   2451 
   2452        # Fix options.profilePath for legacy consumers.
   2453        options.profilePath = self.profile.profile
   2454 
   2455        manifest = self.addChromeToProfile(options)
   2456        self.copyExtraFilesToProfile(options)
   2457 
   2458        # create certificate database for the profile
   2459        # TODO: this should really be upstreamed somewhere, maybe mozprofile
   2460        certificateStatus = self.fillCertificateDB(options)
   2461        if certificateStatus:
   2462            self.log.error(
   2463                "TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed"
   2464            )
   2465            return None
   2466 
   2467        # Set preferences in the following order (latter overrides former):
   2468        # 1) Preferences from base profile (e.g from testing/profiles)
   2469        # 2) Prefs hardcoded in this function
   2470        # 3) Prefs from --setpref
   2471 
   2472        # Prefs from base profiles
   2473        self.merge_base_profiles(options, "mochitest")
   2474 
   2475        # Hardcoded prefs (TODO move these into a base profile)
   2476        prefs = {
   2477            # Enable tracing output for detailed failures in case of
   2478            # failing connection attempts, and hangs (bug 1397201)
   2479            "remote.log.level": "Trace",
   2480            # Disable async font fallback, because the unpredictable
   2481            # extra reflow it can trigger (potentially affecting a later
   2482            # test) results in spurious intermittent failures.
   2483            "gfx.font_rendering.fallback.async": False,
   2484        }
   2485 
   2486        test_timeout = None
   2487        if options.flavor == "browser":
   2488            if options.timeout:
   2489                test_timeout = options.timeout
   2490            elif mozinfo.info["asan"] or mozinfo.info["debug"]:
   2491                # browser-chrome tests use a fairly short default timeout of 45 seconds;
   2492                # this is sometimes too short on asan and debug, where we expect reduced
   2493                # performance.
   2494                self.log.info(
   2495                    "Increasing default timeout to 90 seconds (asan or debug)"
   2496                )
   2497                test_timeout = 90
   2498            elif mozinfo.info["tsan"]:
   2499                # tsan builds need even more time
   2500                self.log.info("Increasing default timeout to 120 seconds (tsan)")
   2501                test_timeout = 120
   2502            else:
   2503                test_timeout = 45
   2504        elif options.flavor in ("a11y", "chrome"):
   2505            test_timeout = 45
   2506 
   2507        if "MOZ_CHAOSMODE=0xfb" in options.environment and test_timeout:
   2508            test_timeout *= 2
   2509            self.log.info(
   2510                f"Increasing default timeout to {test_timeout} seconds (MOZ_CHAOSMODE)"
   2511            )
   2512 
   2513        if test_timeout:
   2514            prefs["testing.browserTestHarness.timeout"] = test_timeout
   2515 
   2516        if getattr(self, "testRootAbs", None):
   2517            prefs["mochitest.testRoot"] = self.testRootAbs
   2518 
   2519        # See if we should use fake media devices.
   2520        if options.useTestMediaDevices:
   2521            prefs["media.audio_loopback_dev"] = self.mediaDevices["audio"]["name"]
   2522            prefs["media.video_loopback_dev"] = self.mediaDevices["video"]["name"]
   2523            prefs["media.cubeb.output_device"] = self.mediaDevices["speaker"]["name"]
   2524            prefs["media.volume_scale"] = "1.0"
   2525            self.gstForV4l2loopbackProcess = self.mediaDevices["video"]["process"]
   2526 
   2527        self.profile.set_preferences(prefs)
   2528 
   2529        # Extra prefs from --setpref
   2530        self.profile.set_preferences(self.extraPrefs)
   2531        return manifest
   2532 
   2533    def getGMPPluginPath(self, options):
   2534        if options.gmp_path:
   2535            return options.gmp_path
   2536 
   2537        gmp_parentdirs = [
   2538            # For local builds, GMP plugins will be under dist/bin.
   2539            options.xrePath,
   2540            # For packaged builds, GMP plugins will get copied under
   2541            # $profile/plugins.
   2542            os.path.join(self.profile.profile, "plugins"),
   2543        ]
   2544 
   2545        gmp_subdirs = [
   2546            os.path.join("gmp-fake", "1.0"),
   2547            os.path.join("gmp-fakeopenh264", "1.0"),
   2548            os.path.join("gmp-clearkey", "0.1"),
   2549        ]
   2550 
   2551        gmp_paths = [
   2552            os.path.join(parent, sub)
   2553            for parent in gmp_parentdirs
   2554            for sub in gmp_subdirs
   2555            if os.path.isdir(os.path.join(parent, sub))
   2556        ]
   2557 
   2558        if not gmp_paths:
   2559            # This is fatal for desktop environments.
   2560            raise OSError("Could not find test gmp plugins")
   2561 
   2562        return os.pathsep.join(gmp_paths)
   2563 
   2564    def cleanup(self, options, final=False):
   2565        """remove temporary files, profile and virtual audio input device"""
   2566        if hasattr(self, "manifest") and self.manifest is not None:
   2567            if os.path.exists(self.manifest):
   2568                os.remove(self.manifest)
   2569        if hasattr(self, "profile"):
   2570            del self.profile
   2571        if hasattr(self, "extraTestsDirs"):
   2572            for d in self.extraTestsDirs:
   2573                if os.path.exists(d):
   2574                    shutil.rmtree(d)
   2575        if options.pidFile != "" and os.path.exists(options.pidFile):
   2576            try:
   2577                os.remove(options.pidFile)
   2578                if os.path.exists(options.pidFile + ".xpcshell.pid"):
   2579                    os.remove(options.pidFile + ".xpcshell.pid")
   2580            except Exception:
   2581                self.log.warning(
   2582                    "cleaning up pidfile '%s' was unsuccessful from the test harness"
   2583                    % options.pidFile
   2584                )
   2585        options.manifestFile = None
   2586 
   2587        if hasattr(self, "virtualDeviceIdList"):
   2588            pactl = which("pactl")
   2589 
   2590            if not pactl:
   2591                self.log.error("Could not find pactl on system")
   2592                return None
   2593 
   2594            for id in self.virtualDeviceIdList:
   2595                try:
   2596                    subprocess.check_call([pactl, "unload-module", str(id)])
   2597                except subprocess.CalledProcessError:
   2598                    self.log.error(f"Could not remove pulse module with id {id}")
   2599                    return None
   2600 
   2601            self.virtualDeviceIdList = []
   2602 
   2603        if hasattr(self, "virtualAudioNodeIdList"):
   2604            for id in self.virtualAudioNodeIdList:
   2605                subprocess.check_output(["pw-cli", "destroy", str(id)])
   2606            self.virtualAudioNodeIdList = []
   2607 
   2608    def dumpScreen(self, utilityPath):
   2609        if self.haveDumpedScreen:
   2610            self.log.info(
   2611                "Not taking screenshot here: see the one that was previously logged"
   2612            )
   2613            return
   2614        self.haveDumpedScreen = True
   2615        dump_screen(utilityPath, self.log)
   2616 
   2617    def killAndGetStack(self, processPID, utilityPath, debuggerInfo, dump_screen=False):
   2618        """
   2619        Kill the process, preferrably in a way that gets us a stack trace.
   2620        Also attempts to obtain a screenshot before killing the process
   2621        if specified.
   2622        """
   2623        self.log.info("Killing process: %s" % processPID)
   2624        if dump_screen:
   2625            self.dumpScreen(utilityPath)
   2626 
   2627        if mozinfo.info.get("crashreporter", True) and not debuggerInfo:
   2628            try:
   2629                minidump_path = os.path.join(self.profile.profile, "minidumps")
   2630                mozcrash.kill_and_get_minidump(processPID, minidump_path, utilityPath)
   2631            except OSError:
   2632                # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
   2633                self.log.info("Can't trigger Breakpad, process no longer exists")
   2634            return
   2635        self.log.info("Can't trigger Breakpad, just killing process")
   2636        killPid(processPID, self.log)
   2637 
   2638    def extract_child_pids(self, process_log, parent_pid=None):
   2639        """Parses the given log file for the pids of any processes launched by
   2640        the main process and returns them as a list.
   2641        If parent_pid is provided, and psutil is available, returns children of
   2642        parent_pid according to psutil.
   2643        """
   2644        rv = []
   2645        if parent_pid and HAVE_PSUTIL:
   2646            self.log.info("Determining child pids from psutil...")
   2647            try:
   2648                rv = [p.pid for p in psutil.Process(parent_pid).children()]
   2649                self.log.info(str(rv))
   2650            except psutil.NoSuchProcess:
   2651                self.log.warning("Failed to lookup children of pid %d" % parent_pid)
   2652 
   2653        rv = set(rv)
   2654        pid_re = re.compile(r"==> process \d+ launched child process (\d+)")
   2655        with open(process_log) as fd:
   2656            for line in fd:
   2657                self.log.info(line.rstrip())
   2658                m = pid_re.search(line)
   2659                if m:
   2660                    rv.add(int(m.group(1)))
   2661        return rv
   2662 
   2663    def checkForZombies(self, processLog, utilityPath, debuggerInfo):
   2664        """Look for hung processes"""
   2665 
   2666        if not os.path.exists(processLog):
   2667            self.log.info("Automation Error: PID log not found: %s" % processLog)
   2668            # Whilst no hung process was found, the run should still display as
   2669            # a failure
   2670            return True
   2671 
   2672        # scan processLog for zombies
   2673        self.log.info("zombiecheck | Reading PID log: %s" % processLog)
   2674        processList = self.extract_child_pids(processLog)
   2675        # kill zombies
   2676        foundZombie = False
   2677        for processPID in processList:
   2678            self.log.info(
   2679                "zombiecheck | Checking for orphan process with PID: %d" % processPID
   2680            )
   2681            if isPidAlive(processPID):
   2682                foundZombie = True
   2683                self.log.error(
   2684                    "TEST-UNEXPECTED-FAIL | zombiecheck | child process "
   2685                    "%d still alive after shutdown" % processPID
   2686                )
   2687                self.killAndGetStack(
   2688                    processPID, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
   2689                )
   2690 
   2691        return foundZombie
   2692 
   2693    def checkForRunningBrowsers(self):
   2694        firefoxes = ""
   2695        if HAVE_PSUTIL:
   2696            attrs = ["pid", "ppid", "name", "cmdline", "username"]
   2697            for proc in psutil.process_iter():
   2698                try:
   2699                    if "firefox" in proc.name():
   2700                        firefoxes = "%s%s\n" % (firefoxes, proc.as_dict(attrs=attrs))
   2701                except Exception:
   2702                    # may not be able to access process info for all processes
   2703                    continue
   2704        if len(firefoxes) > 0:
   2705            # In automation, this warning is unexpected and should be investigated.
   2706            # In local testing, this is probably okay, as long as the browser is not
   2707            # running a marionette server.
   2708            self.log.warning("Found 'firefox' running before starting test browser!")
   2709            self.log.warning(firefoxes)
   2710 
   2711    def runApp(
   2712        self,
   2713        testUrl,
   2714        env,
   2715        app,
   2716        profile,
   2717        extraArgs,
   2718        utilityPath,
   2719        debuggerInfo=None,
   2720        valgrindPath=None,
   2721        valgrindArgs=None,
   2722        valgrindSuppFiles=None,
   2723        symbolsPath=None,
   2724        timeout=-1,
   2725        detectShutdownLeaks=False,
   2726        screenshotOnFail=False,
   2727        bisectChunk=None,
   2728        restartAfterFailure=False,
   2729        marionette_args=None,
   2730        e10s=True,
   2731        runFailures=False,
   2732        crashAsPass=False,
   2733        currentManifest=None,
   2734    ):
   2735        """
   2736        Run the app, log the duration it took to execute, return the status code.
   2737        Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing
   2738        for |timeout| seconds.
   2739        """
   2740        # It can't be the case that both a with-debugger and an
   2741        # on-Valgrind run have been requested.  doTests() should have
   2742        # already excluded this possibility.
   2743        assert not (valgrindPath and debuggerInfo)
   2744 
   2745        # debugger information
   2746        interactive = False
   2747        debug_args = None
   2748        if debuggerInfo:
   2749            interactive = debuggerInfo.interactive
   2750            debug_args = [debuggerInfo.path] + debuggerInfo.args
   2751 
   2752        # Set up Valgrind arguments.
   2753        if valgrindPath:
   2754            interactive = False
   2755            valgrindArgs_split = (
   2756                [] if valgrindArgs is None else shlex.split(valgrindArgs)
   2757            )
   2758 
   2759            valgrindSuppFiles_final = []
   2760            if valgrindSuppFiles is not None:
   2761                valgrindSuppFiles_final = [
   2762                    "--suppressions=" + path for path in valgrindSuppFiles.split(",")
   2763                ]
   2764 
   2765            debug_args = (
   2766                [valgrindPath]
   2767                + mozdebug.get_default_valgrind_args()
   2768                + valgrindArgs_split
   2769                + valgrindSuppFiles_final
   2770            )
   2771 
   2772        # fix default timeout
   2773        if timeout == -1:
   2774            timeout = self.DEFAULT_TIMEOUT
   2775 
   2776        # Note in the log if running on Valgrind
   2777        if valgrindPath:
   2778            self.log.info(
   2779                "runtests.py | Running on Valgrind.  "
   2780                + "Using timeout of %d seconds." % timeout
   2781            )
   2782 
   2783        # copy env so we don't munge the caller's environment
   2784        env = env.copy()
   2785 
   2786        # Used to defer a possible IOError exception from Marionette
   2787        marionette_exception = None
   2788 
   2789        temp_file_paths = []
   2790 
   2791        # make sure we clean up after ourselves.
   2792        try:
   2793            # set process log environment variable
   2794            tmpfd, processLog = tempfile.mkstemp(suffix="pidlog")
   2795            os.close(tmpfd)
   2796            env["MOZ_PROCESS_LOG"] = processLog
   2797 
   2798            if debuggerInfo:
   2799                # If a debugger is attached, don't use timeouts, and don't
   2800                # capture ctrl-c.
   2801                timeout = None
   2802                signal.signal(signal.SIGINT, lambda sigid, frame: None)
   2803 
   2804            # build command line
   2805            cmd = os.path.abspath(app)
   2806            args = list(extraArgs)
   2807 
   2808            # Enable Marionette and allow system access to execute the mochitest
   2809            # init script in the chrome scope of the application
   2810            args.append("-marionette")
   2811            args.append("-remote-allow-system-access")
   2812 
   2813            # TODO: mozrunner should use -foreground at least for mac
   2814            # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
   2815            args.append("-foreground")
   2816            self.start_script_kwargs["testUrl"] = testUrl or "about:blank"
   2817 
   2818            # Log if slow events are used from chrome.
   2819            env["MOZ_LOG"] = (
   2820                env["MOZ_LOG"] + "," if env["MOZ_LOG"] else ""
   2821            ) + "SlowChromeEvent:3"
   2822 
   2823            if detectShutdownLeaks:
   2824                env["MOZ_LOG"] = (
   2825                    env["MOZ_LOG"] + "," if env["MOZ_LOG"] else ""
   2826                ) + "DocShellAndDOMWindowLeak:3"
   2827                shutdownLeaks = ShutdownLeaks(self.log)
   2828            else:
   2829                shutdownLeaks = None
   2830 
   2831            if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
   2832                lsanLeaks = LSANLeaks(self.log)
   2833            else:
   2834                lsanLeaks = None
   2835 
   2836            # create an instance to process the output
   2837            outputHandler = self.OutputHandler(
   2838                harness=self,
   2839                utilityPath=utilityPath,
   2840                symbolsPath=symbolsPath,
   2841                dump_screen_on_timeout=not debuggerInfo,
   2842                dump_screen_on_fail=screenshotOnFail,
   2843                shutdownLeaks=shutdownLeaks,
   2844                lsanLeaks=lsanLeaks,
   2845                bisectChunk=bisectChunk,
   2846                restartAfterFailure=restartAfterFailure,
   2847            )
   2848 
   2849            def timeoutHandler():
   2850                browserProcessId = outputHandler.browserProcessId
   2851                self.handleTimeout(
   2852                    timeout,
   2853                    proc,
   2854                    utilityPath,
   2855                    debuggerInfo,
   2856                    browserProcessId,
   2857                    processLog,
   2858                    symbolsPath,
   2859                )
   2860 
   2861            kp_kwargs = {
   2862                "kill_on_timeout": False,
   2863                "cwd": SCRIPT_DIR,
   2864                "onTimeout": [timeoutHandler],
   2865            }
   2866            kp_kwargs["processOutputLine"] = [outputHandler]
   2867 
   2868            self.checkForRunningBrowsers()
   2869 
   2870            # create mozrunner instance and start the system under test process
   2871            self.lastTestSeen = self.test_name
   2872            self.lastManifest = currentManifest
   2873            startTime = datetime.now()
   2874 
   2875            runner_cls = mozrunner.runners.get(
   2876                mozinfo.info.get("appname", "firefox"), mozrunner.Runner
   2877            )
   2878            runner = runner_cls(
   2879                profile=self.profile,
   2880                binary=cmd,
   2881                cmdargs=args,
   2882                env=env,
   2883                process_class=mozprocess.ProcessHandlerMixin,
   2884                process_args=kp_kwargs,
   2885            )
   2886 
   2887            # start the runner
   2888            try:
   2889                runner.start(
   2890                    debug_args=debug_args,
   2891                    interactive=interactive,
   2892                    outputTimeout=timeout,
   2893                )
   2894                proc = runner.process_handler
   2895                self.log.info("runtests.py | Application pid: %d" % proc.pid)
   2896 
   2897                gecko_id = "GECKO(%d)" % proc.pid
   2898                self.log.process_start(gecko_id)
   2899                self.message_logger.gecko_id = gecko_id
   2900            except PermissionError:
   2901                # treat machine as bad, return
   2902                return TBPL_RETRY, "Failure to launch browser"
   2903            except Exception as e:
   2904                raise e  # unknown error
   2905 
   2906            try:
   2907                # start marionette and kick off the tests
   2908                marionette_args = marionette_args or {}
   2909                self.marionette = Marionette(**marionette_args)
   2910                self.marionette.start_session()
   2911 
   2912                # install specialpowers and mochikit addons
   2913                addons = Addons(self.marionette)
   2914 
   2915                if self.staged_addons:
   2916                    for addon_path in self.staged_addons:
   2917                        if not os.path.isdir(addon_path):
   2918                            self.log.error(
   2919                                "TEST-UNEXPECTED-FAIL | invalid setup: missing extension at %s"
   2920                                % addon_path
   2921                            )
   2922                            return 1, self.lastTestSeen
   2923                        temp_addon_path = create_zip(addon_path)
   2924                        temp_file_paths.append(temp_addon_path)
   2925                        addons.install(temp_addon_path)
   2926 
   2927                self.execute_start_script()
   2928 
   2929                # an open marionette session interacts badly with mochitest,
   2930                # delete it until we figure out why.
   2931                self.marionette.delete_session()
   2932                del self.marionette
   2933 
   2934            except OSError as e:
   2935                # Any IOError as thrown by Marionette means that something is
   2936                # wrong with the process, like a crash or the socket is no
   2937                # longer open. We defer raising this specific error so that
   2938                # post-test checks for leaks and crashes are performed and
   2939                # reported first.
   2940                marionette_exception = e
   2941 
   2942            # wait until app is finished
   2943            # XXX copy functionality from
   2944            # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61
   2945            # until bug 913970 is fixed regarding mozrunner `wait` not returning status
   2946            # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970
   2947            self.log.info("runtests.py | Waiting for browser...")
   2948            status = proc.wait()
   2949            if status is None:
   2950                self.log.warning(
   2951                    "runtests.py | Failed to get app exit code - running/crashed?"
   2952                )
   2953                # must report an integer to process_exit()
   2954                status = 0
   2955            self.log.process_exit("Main app process", status)
   2956            runner.process_handler = None
   2957 
   2958            if not status and self.message_logger.is_test_running:
   2959                message = {
   2960                    "action": "test_end",
   2961                    "status": "FAIL",
   2962                    "expected": "PASS",
   2963                    "thread": None,
   2964                    "pid": None,
   2965                    "source": "mochitest",
   2966                    "time": int(time.time() * 1000),
   2967                    "test": self.lastTestSeen,
   2968                    "message": "Application shut down (without crashing) in the middle of a test!",
   2969                }
   2970                self.message_logger.process_message(message)
   2971 
   2972            # finalize output handler
   2973            outputHandler.finish()
   2974 
   2975            # record post-test information
   2976            if status:
   2977                # no need to keep return code 137, 245, etc.
   2978                status = 1
   2979                self.message_logger.dump_buffered()
   2980                msg = "application terminated with exit code %s" % status
   2981                # self.message_logger.is_test_running indicates we need to send a test_end
   2982                if crashAsPass and self.message_logger.is_test_running:
   2983                    # this works for browser-chrome, mochitest-plain has status=0
   2984                    message = {
   2985                        "action": "test_end",
   2986                        "status": "CRASH",
   2987                        "expected": "CRASH",
   2988                        "thread": None,
   2989                        "pid": None,
   2990                        "source": "mochitest",
   2991                        "time": int(time.time() * 1000),
   2992                        "test": self.lastTestSeen,
   2993                        "message": msg,
   2994                    }
   2995 
   2996                    # for looping scenarios (like --restartAfterFailure), document the test
   2997                    key = message["test"].split(" ")[0].split("/")[-1].strip()
   2998                    if key not in self.expectedError:
   2999                        self.expectedError[key] = message.get(
   3000                            "message", message["message"]
   3001                        ).strip()
   3002 
   3003                    # need to send a test_end in order to have mozharness process messages properly
   3004                    # this requires a custom message vs log.error/log.warning/etc.
   3005                    self.message_logger.process_message(message)
   3006            else:
   3007                self.lastTestSeen = (
   3008                    currentManifest or "Main app process exited normally"
   3009                )
   3010 
   3011            self.log.info(
   3012                "runtests.py | Application ran for: %s"
   3013                % str(datetime.now() - startTime)
   3014            )
   3015 
   3016            # Do a final check for zombie child processes.
   3017            zombieProcesses = self.checkForZombies(
   3018                processLog, utilityPath, debuggerInfo
   3019            )
   3020 
   3021            # check for crashes
   3022            quiet = False
   3023            if crashAsPass:
   3024                quiet = True
   3025 
   3026            minidump_path = os.path.join(self.profile.profile, "minidumps")
   3027            crash_count = mozcrash.log_crashes(
   3028                self.log,
   3029                minidump_path,
   3030                symbolsPath,
   3031                test=self.lastTestSeen,
   3032                quiet=quiet,
   3033            )
   3034 
   3035            expected = None
   3036            if crashAsPass or crash_count > 0:
   3037                # self.message_logger.is_test_running indicates we need a test_end message
   3038                if self.message_logger.is_test_running:
   3039                    # this works for browser-chrome, mochitest-plain has status=0
   3040                    expected = "CRASH"
   3041                if crashAsPass:
   3042                    status = 0
   3043            elif crash_count or zombieProcesses:
   3044                if self.message_logger.is_test_running:
   3045                    expected = "PASS"
   3046                status = 1
   3047 
   3048            if expected:
   3049                # send this out so we always wrap up the test-end message
   3050                message = {
   3051                    "action": "test_end",
   3052                    "status": "CRASH",
   3053                    "expected": expected,
   3054                    "thread": None,
   3055                    "pid": None,
   3056                    "source": "mochitest",
   3057                    "time": int(time.time() * 1000),
   3058                    "test": self.lastTestSeen,
   3059                    "message": "application terminated with exit code %s" % status,
   3060                }
   3061 
   3062                # for looping scenarios (like --restartAfterFailure), document the test
   3063                key = message["test"].split(" ")[0].split("/")[-1].strip()
   3064                if key not in self.expectedError:
   3065                    self.expectedError[key] = message.get(
   3066                        "message", message["message"]
   3067                    ).strip()
   3068 
   3069                # need to send a test_end in order to have mozharness process messages properly
   3070                # this requires a custom message vs log.error/log.warning/etc.
   3071                self.message_logger.process_message(message)
   3072        finally:
   3073            # cleanup
   3074            if os.path.exists(processLog):
   3075                os.remove(processLog)
   3076            for p in temp_file_paths:
   3077                os.remove(p)
   3078 
   3079        if marionette_exception is not None:
   3080            raise marionette_exception
   3081 
   3082        return status, self.lastTestSeen
   3083 
   3084    def initializeLooping(self, options):
   3085        """
   3086        This method is used to clear the contents before each run of for loop.
   3087        This method is used for --run-by-dir and --bisect-chunk.
   3088        """
   3089        if options.conditionedProfile:
   3090            if options.profilePath and os.path.exists(options.profilePath):
   3091                shutil.rmtree(options.profilePath, ignore_errors=True)
   3092                if options.manifestFile and os.path.exists(options.manifestFile):
   3093                    os.remove(options.manifestFile)
   3094 
   3095        self.expectedError.clear()
   3096        self.result.clear()
   3097        options.manifestFile = None
   3098        options.profilePath = None
   3099 
   3100    def initializeVirtualAudioDevices(self):
   3101        """
   3102        Configure the system to have a number of virtual audio devices:
   3103        2 output devices, and
   3104        4 input devices that each produce a tone at a particular frequency.
   3105 
   3106        This method is only currently implemented for Linux.
   3107        """
   3108        if not mozinfo.isLinux:
   3109            return
   3110 
   3111        INPUT_DEVICES_COUNT = 4
   3112        DEVICES_BASE_FREQUENCY = 110  # Hz
   3113 
   3114        output_devices = [
   3115            {"name": "null-44100", "description": "44100Hz Null Output", "rate": 44100},
   3116            {"name": "null-48000", "description": "48000Hz Null Output", "rate": 48000},
   3117        ]
   3118        # We want quite a number of input devices, each with a different tone
   3119        # frequency and device name so that we can recognize them easily during
   3120        # testing.
   3121        input_devices = []
   3122        for i in range(1, INPUT_DEVICES_COUNT + 1):
   3123            freq = i * DEVICES_BASE_FREQUENCY
   3124            input_devices.append({
   3125                "name": f"sine-{freq}",
   3126                "description": f"{freq}Hz Sine Source",
   3127                "frequency": freq,
   3128            })
   3129 
   3130        # Determine if this is running PulseAudio or PipeWire
   3131        # `pactl info` works on both systems, but when running on PipeWire it says
   3132        # something like:
   3133        # Server Name: PulseAudio (on PipeWire 1.0.5)
   3134        pactl = which("pactl")
   3135        if not pactl:
   3136            self.log.error("Could not find pactl on system")
   3137            return
   3138 
   3139        o = subprocess.check_output([pactl, "info"])
   3140        if b"PipeWire" in o:
   3141            self.initializeVirtualAudioDevicesPipeWire(input_devices, output_devices)
   3142        else:
   3143            self.initializeVirtualAudioDevicesPulseAudio(
   3144                pactl, input_devices, output_devices
   3145            )
   3146 
   3147    def initializeVirtualAudioDevicesPipeWire(self, input_devices, output_devices):
   3148        required_commands = ["pw-cli", "pw-dump"]
   3149        for command in required_commands:
   3150            cmd = which(command)
   3151            if not cmd:
   3152                self.log.error(f"Could not find required program {command} on system")
   3153                return
   3154 
   3155        # Create outputs
   3156        for device in output_devices:
   3157            cmd = ["pw-cli", "create-node", "adapter"]
   3158            device_spec = [
   3159                (
   3160                    "{{factory.name=support.null-audio-sink "
   3161                    'node.name="{}" '
   3162                    'node.description="{}" '
   3163                    "media.class=Audio/Sink "
   3164                    "object.linger=true "
   3165                    "audio.position=[FL FR] "
   3166                    "monitor.channel-volumes=true "
   3167                    "audio.rate={}}}".format(
   3168                        device["name"], device["description"], device["rate"]
   3169                    )
   3170                )
   3171            ]
   3172            subprocess.check_output(cmd + device_spec)
   3173 
   3174        # Create inputs
   3175        for device in input_devices:
   3176            cmd = ["pw-cli", "create-node", "adapter"]
   3177            # The frequency setting doesn't work for now
   3178            device_spec = [
   3179                (
   3180                    "{{factory.name=audiotestsrc "
   3181                    'node.name="{}" '
   3182                    'node.description="{}" '
   3183                    "media.class=Audio/Source "
   3184                    "object.linger=true "
   3185                    "node.param.Props={{frequency: {}}} }}".format(
   3186                        device["name"], device["description"], device["frequency"]
   3187                    )
   3188                )
   3189            ]
   3190            subprocess.check_output(cmd + device_spec)
   3191 
   3192        # Get the node ids for cleanup
   3193        virtual_node_ids = []
   3194        cmd = ["pw-dump", "Node"]
   3195        try:
   3196            nodes = json.loads(subprocess.check_output(cmd))
   3197        except json.JSONDecodeError as e:
   3198            # This can happen but I'm not sure why, leaving that in for now
   3199            print(e, str(cmd))
   3200            sys.exit(1)
   3201        for node in nodes:
   3202            name = node["info"]["props"]["node.name"]
   3203            if "null-" in name or "sine-" in name:
   3204                virtual_node_ids.append(node["info"]["props"]["object.id"])
   3205 
   3206        self.virtualAudioNodeIdList = virtual_node_ids
   3207 
   3208    def initializeVirtualAudioDevicesPulseAudio(
   3209        self, pactl, input_devices, output_devices
   3210    ):
   3211        def getModuleIds(moduleName):
   3212            o = subprocess.check_output([pactl, "list", "modules", "short"])
   3213            list = []
   3214            for input in o.splitlines():
   3215                device = input.decode().split("\t")
   3216                if device[1] == moduleName:
   3217                    list.append(int(device[0]))
   3218            return list
   3219 
   3220        # If the device are already present, find their id and return early
   3221        outputDeviceIdList = getModuleIds("module-null-sink")
   3222        inputDeviceIdList = getModuleIds("module-sine-source")
   3223 
   3224        if len(outputDeviceIdList) == len(output_devices) and len(
   3225            inputDeviceIdList
   3226        ) == len(input_devices):
   3227            self.virtualDeviceIdList = outputDeviceIdList + inputDeviceIdList
   3228            return
   3229        else:
   3230            # Remove any existing devices and reinitialize properly
   3231            for id in outputDeviceIdList + inputDeviceIdList:
   3232                try:
   3233                    subprocess.check_call([pactl, "unload-module", str(id)])
   3234                except subprocess.CalledProcessError:
   3235                    log.error(f"Could not remove pulse module with id {id}")
   3236                    return None
   3237 
   3238        idList = []
   3239        command = [pactl, "load-module", "module-null-sink"]
   3240        for device in output_devices:
   3241            try:
   3242                o = subprocess.check_output(
   3243                    command
   3244                    + [
   3245                        "rate={}".format(device["rate"]),
   3246                        "sink_name='\"{}\"'".format(device["name"]),
   3247                        "sink_properties='device.description=\"{}\"'".format(
   3248                            device["description"]
   3249                        ),
   3250                    ]
   3251                )
   3252                idList.append(int(o))
   3253            except subprocess.CalledProcessError:
   3254                self.log.error(
   3255                    "Could not load module-null-sink at rate={}".format(device["rate"])
   3256                )
   3257 
   3258        command = [pactl, "load-module", "module-sine-source", "rate=44100"]
   3259        for device in input_devices:
   3260            complete_command = command + [
   3261                'source_name="{}"'.format(device["name"]),
   3262                "frequency={}".format(device["frequency"]),
   3263            ]
   3264            try:
   3265                o = subprocess.check_output(complete_command)
   3266                idList.append(int(o))
   3267 
   3268            except subprocess.CalledProcessError:
   3269                self.log.error(
   3270                    "Could not create device with module-sine-source (freq={})".format(
   3271                        device["frequency"]
   3272                    )
   3273                )
   3274 
   3275        self.virtualDeviceIdList = idList
   3276 
   3277    def normalize_paths(self, paths):
   3278        # Normalize test paths so they are relative to test root
   3279        norm_paths = []
   3280        for p in paths:
   3281            abspath = os.path.abspath(os.path.join(self.oldcwd, p))
   3282            if abspath.startswith(self.testRootAbs):
   3283                norm_paths.append(os.path.relpath(abspath, self.testRootAbs))
   3284            else:
   3285                norm_paths.append(p)
   3286        return norm_paths
   3287 
   3288    def runMochitests(self, options, testsToRun, manifestToFilter=None):
   3289        "This is a base method for calling other methods in this class for --bisect-chunk."
   3290        # Making an instance of bisect class for --bisect-chunk option.
   3291        bisect = bisection.Bisect(self)
   3292        finished = False
   3293        status = 0
   3294        bisection_log = 0
   3295        while not finished:
   3296            if options.bisectChunk:
   3297                testsToRun = bisect.pre_test(options, testsToRun, status)
   3298                # To inform that we are in the process of bisection, and to
   3299                # look for bleedthrough
   3300                if options.bisectChunk != "default" and not bisection_log:
   3301                    self.log.error(
   3302                        "TEST-UNEXPECTED-FAIL | Bisection | Please ignore repeats "
   3303                        "and look for 'Bleedthrough' (if any) at the end of "
   3304                        "the failure list"
   3305                    )
   3306                    bisection_log = 1
   3307 
   3308            result = self.doTests(options, testsToRun, manifestToFilter)
   3309            if result == TBPL_RETRY:  # terminate task
   3310                return result
   3311 
   3312            if options.bisectChunk:
   3313                status = bisect.post_test(options, self.expectedError, self.result)
   3314            elif options.restartAfterFailure:
   3315                # NOTE: ideally browser will halt on first failure, then this will always be the last test
   3316                if not self.expectedError:
   3317                    status = -1
   3318                else:
   3319                    firstFail = len(testsToRun)
   3320                    for key in self.expectedError:
   3321                        full_key = [x for x in testsToRun if key in x]
   3322                        if full_key:
   3323                            firstFail = min(firstFail, testsToRun.index(full_key[0]))
   3324                    testsToRun = testsToRun[firstFail + 1 :]
   3325                    if testsToRun == []:
   3326                        status = -1
   3327            else:
   3328                status = -1
   3329 
   3330            if status == -1:
   3331                finished = True
   3332 
   3333        # We need to print the summary only if options.bisectChunk has a value.
   3334        # Also we need to make sure that we do not print the summary in between
   3335        # running tests via --run-by-dir.
   3336        if options.bisectChunk and options.bisectChunk in self.result:
   3337            bisect.print_summary()
   3338 
   3339        return result
   3340 
   3341    def groupTestsByScheme(self, tests):
   3342        """
   3343        split tests into groups by schemes. test is classified as http if
   3344        no scheme specified
   3345        """
   3346        httpTests = []
   3347        httpsTests = []
   3348        for test in tests:
   3349            if not test.get("scheme") or test.get("scheme") == "http":
   3350                httpTests.append(test)
   3351            elif test.get("scheme") == "https":
   3352                httpsTests.append(test)
   3353        return {"http": httpTests, "https": httpsTests}
   3354 
   3355    def verifyTests(self, options):
   3356        """
   3357        Support --verify mode: Run test(s) many times in a variety of
   3358        configurations/environments in an effort to find intermittent
   3359        failures.
   3360        """
   3361 
   3362        # Number of times to repeat test(s) when running with --repeat
   3363        VERIFY_REPEAT = 10
   3364        # Number of times to repeat test(s) when running test in
   3365        VERIFY_REPEAT_SINGLE_BROWSER = 5
   3366 
   3367        def step1():
   3368            options.repeat = VERIFY_REPEAT
   3369            options.keep_open = False
   3370            options.runUntilFailure = True
   3371            options.profilePath = None
   3372            result = self.runTests(options)
   3373            result = result or (-2 if self.countfail > 0 else 0)
   3374            self.message_logger.finish()
   3375            return result
   3376 
   3377        def step2():
   3378            options.repeat = 0
   3379            options.keep_open = False
   3380            options.runUntilFailure = False
   3381            for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
   3382                options.profilePath = None
   3383                result = self.runTests(options)
   3384                result = result or (-2 if self.countfail > 0 else 0)
   3385                self.message_logger.finish()
   3386                if result != 0:
   3387                    break
   3388            return result
   3389 
   3390        def step3():
   3391            options.repeat = VERIFY_REPEAT
   3392            options.keep_open = False
   3393            options.runUntilFailure = True
   3394            options.environment.append("MOZ_CHAOSMODE=0xfb")
   3395            options.profilePath = None
   3396            result = self.runTests(options)
   3397            options.environment.remove("MOZ_CHAOSMODE=0xfb")
   3398            result = result or (-2 if self.countfail > 0 else 0)
   3399            self.message_logger.finish()
   3400            return result
   3401 
   3402        def step4():
   3403            options.repeat = 0
   3404            options.keep_open = False
   3405            options.runUntilFailure = False
   3406            options.environment.append("MOZ_CHAOSMODE=0xfb")
   3407            for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
   3408                options.profilePath = None
   3409                result = self.runTests(options)
   3410                result = result or (-2 if self.countfail > 0 else 0)
   3411                self.message_logger.finish()
   3412                if result != 0:
   3413                    break
   3414            options.environment.remove("MOZ_CHAOSMODE=0xfb")
   3415            return result
   3416 
   3417        def fission_step(fission_pref):
   3418            if fission_pref not in options.extraPrefs:
   3419                options.extraPrefs.append(fission_pref)
   3420            options.keep_open = False
   3421            options.runUntilFailure = True
   3422            options.profilePath = None
   3423            result = self.runTests(options)
   3424            result = result or (-2 if self.countfail > 0 else 0)
   3425            self.message_logger.finish()
   3426            return result
   3427 
   3428        def fission_step1():
   3429            return fission_step("fission.autostart=false")
   3430 
   3431        def fission_step2():
   3432            return fission_step("fission.autostart=true")
   3433 
   3434        if options.verify_fission:
   3435            steps = [
   3436                ("1. Run each test without fission.", fission_step1),
   3437                ("2. Run each test with fission.", fission_step2),
   3438            ]
   3439        else:
   3440            steps = [
   3441                ("1. Run each test %d times in one browser." % VERIFY_REPEAT, step1),
   3442                (
   3443                    "2. Run each test %d times in a new browser each time."
   3444                    % VERIFY_REPEAT_SINGLE_BROWSER,
   3445                    step2,
   3446                ),
   3447                (
   3448                    "3. Run each test %d times in one browser, in chaos mode."
   3449                    % VERIFY_REPEAT,
   3450                    step3,
   3451                ),
   3452                (
   3453                    "4. Run each test %d times in a new browser each time, "
   3454                    "in chaos mode." % VERIFY_REPEAT_SINGLE_BROWSER,
   3455                    step4,
   3456                ),
   3457            ]
   3458 
   3459        stepResults = {}
   3460        for descr, step in steps:
   3461            stepResults[descr] = "not run / incomplete"
   3462 
   3463        startTime = datetime.now()
   3464        maxTime = timedelta(seconds=options.verify_max_time)
   3465        finalResult = "PASSED"
   3466        for descr, step in steps:
   3467            if (datetime.now() - startTime) > maxTime:
   3468                self.log.info("::: Test verification is taking too long: Giving up!")
   3469                self.log.info(
   3470                    "::: So far, all checks passed, but not all checks were run."
   3471                )
   3472                break
   3473            self.log.info(":::")
   3474            self.log.info('::: Running test verification step "%s"...' % descr)
   3475            self.log.info(":::")
   3476            result = step()
   3477            if result != 0:
   3478                stepResults[descr] = "FAIL"
   3479                finalResult = "FAILED!"
   3480                break
   3481            stepResults[descr] = "Pass"
   3482 
   3483        self.logPreamble([])
   3484 
   3485        self.log.info(":::")
   3486        self.log.info("::: Test verification summary for:")
   3487        self.log.info(":::")
   3488        tests = self.getActiveTests(options)
   3489        for test in tests:
   3490            self.log.info("::: " + test["path"])
   3491        self.log.info(":::")
   3492        for descr in sorted(stepResults.keys()):
   3493            self.log.info("::: %s : %s" % (descr, stepResults[descr]))
   3494        self.log.info(":::")
   3495        self.log.info("::: Test verification %s" % finalResult)
   3496        self.log.info(":::")
   3497 
   3498        return 0
   3499 
   3500    def runTests(self, options):
   3501        """Prepare, configure, run tests and cleanup"""
   3502        self.extraPrefs = parse_preferences(options.extraPrefs)
   3503        self.extraPrefs["fission.autostart"] = not options.disable_fission
   3504 
   3505        # for test manifest parsing.
   3506        mozinfo.update({
   3507            "a11y_checks": options.a11y_checks,
   3508            "e10s": options.e10s,
   3509            "fission": not options.disable_fission,
   3510            "headless": options.headless,
   3511            "http3": options.useHttp3Server,
   3512            "http2": options.useHttp2Server,
   3513            "inc_origin_init": os.environ.get("MOZ_ENABLE_INC_ORIGIN_INIT") == "1",
   3514            # Until the test harness can understand default pref values,
   3515            # (https://bugzilla.mozilla.org/show_bug.cgi?id=1577912) this value
   3516            # should by synchronized with the default pref value indicated in
   3517            # StaticPrefList.yaml.
   3518            #
   3519            # Currently for automation, the pref defaults to true (but can be
   3520            # overridden with --setpref).
   3521            "sessionHistoryInParent": not options.disable_fission
   3522            or not self.extraPrefs.get("fission.disableSessionHistoryInParent"),
   3523            "socketprocess_e10s": self.extraPrefs.get("network.process.enabled", False),
   3524            "socketprocess_networking": self.extraPrefs.get(
   3525                "network.http.network_access_on_socket_process.enabled", False
   3526            ),
   3527            "swgl": self.extraPrefs.get("gfx.webrender.software", False),
   3528            "verify": options.verify,
   3529            "verify_fission": options.verify_fission,
   3530            "vertical_tab": self.extraPrefs.get("sidebar.verticalTabs", False),
   3531            "webgl_ipc": self.extraPrefs.get("webgl.out-of-process", False),
   3532            "wmfme": (
   3533                self.extraPrefs.get("media.wmf.media-engine.enabled", 0)
   3534                and self.extraPrefs.get(
   3535                    "media.wmf.media-engine.channel-decoder.enabled", False
   3536                )
   3537            ),
   3538            "mda_gpu": self.extraPrefs.get(
   3539                "media.hardware-video-decoding.force-enabled", False
   3540            ),
   3541            "xorigin": options.xOriginTests,
   3542            "condprof": options.conditionedProfile,
   3543            "msix": "WindowsApps" in options.app,
   3544            "android": mozinfo.info.get("android", False),
   3545            "is_emulator": mozinfo.info.get("is_emulator", False),
   3546            "coverage": mozinfo.info.get("coverage", False),
   3547            "nogpu": mozinfo.info.get("nogpu", False),
   3548        })
   3549 
   3550        if not self.mozinfo_variables_shown:
   3551            self.mozinfo_variables_shown = True
   3552            self.log.info(
   3553                "These variables are available in the mozinfo environment and "
   3554                "can be used to skip tests conditionally:"
   3555            )
   3556            for info in sorted(mozinfo.info.items(), key=lambda item: item[0]):
   3557                self.log.info(f"    {info[0]}: {info[1]}")
   3558        self.setTestRoot(options)
   3559 
   3560        # Despite our efforts to clean up servers started by this script, in practice
   3561        # we still see infrequent cases where a process is orphaned and interferes
   3562        # with future tests, typically because the old server is keeping the port in use.
   3563        # Try to avoid those failures by checking for and killing servers before
   3564        # trying to start new ones.
   3565        self.killNamedProc("ssltunnel")
   3566        self.killNamedProc("xpcshell")
   3567 
   3568        if options.cleanupCrashes:
   3569            mozcrash.cleanup_pending_crash_reports()
   3570 
   3571        tests = self.getActiveTests(options)
   3572        self.logPreamble(tests)
   3573 
   3574        if mozinfo.info["fission"] and not mozinfo.info["e10s"]:
   3575            # Make sure this is logged *after* suite_start so it gets associated with the
   3576            # current suite in the summary formatters.
   3577            self.log.error("Fission is not supported without e10s.")
   3578            return 1
   3579 
   3580        tests = [t for t in tests if "disabled" not in t]
   3581 
   3582        # Until we have all green, this does not run on a11y (for perf reasons)
   3583        if not options.runByManifest:
   3584            result = self.runMochitests(options, [t["path"] for t in tests])
   3585            self.handleShutdownProfile(options)
   3586            return result
   3587 
   3588        # code for --run-by-manifest
   3589        manifests = set(t["manifest"].replace("\\", "/") for t in tests)
   3590        result = 0
   3591 
   3592        origPrefs = self.extraPrefs.copy()
   3593        for m in sorted(manifests):
   3594            self.log.group_start(name=m)
   3595            self.log.info(f"Running manifest: {m}")
   3596            self.message_logger.setManifest(m)
   3597 
   3598            args = list(self.args_by_manifest[m])[0]
   3599            self.extraArgs = []
   3600            if args:
   3601                for arg in args.strip().split():
   3602                    # Split off the argument value if available so that both
   3603                    # name and value will be set individually
   3604                    self.extraArgs.extend(arg.split("="))
   3605 
   3606                self.log.info(
   3607                    "The following arguments will be set:\n  {}".format(
   3608                        "\n  ".join(self.extraArgs)
   3609                    )
   3610                )
   3611 
   3612            prefs = list(self.prefs_by_manifest[m])[0]
   3613            self.extraPrefs = origPrefs.copy()
   3614            if prefs:
   3615                prefs = [p.strip() for p in prefs.strip().split("\n")]
   3616                self.log.info(
   3617                    "The following extra prefs will be set:\n  {}".format(
   3618                        "\n  ".join(prefs)
   3619                    )
   3620                )
   3621                self.extraPrefs.update(parse_preferences(prefs))
   3622 
   3623            envVars = list(self.env_vars_by_manifest[m])[0]
   3624            self.extraEnv = {}
   3625            if envVars:
   3626                self.extraEnv = envVars.strip().split()
   3627                self.log.info(
   3628                    "The following extra environment variables will be set:\n  {}".format(
   3629                        "\n  ".join(self.extraEnv)
   3630                    )
   3631                )
   3632 
   3633            self.parseAndCreateTestsDirs(m)
   3634 
   3635            # If we are using --run-by-manifest, we should not use the profile path (if) provided
   3636            # by the user, since we need to create a new directory for each run. We would face
   3637            # problems if we use the directory provided by the user.
   3638            tests_in_manifest = [t["path"] for t in tests if t["manifest"] == m]
   3639            res = self.runMochitests(options, tests_in_manifest, manifestToFilter=m)
   3640            if res == TBPL_RETRY:  # terminate task
   3641                return res
   3642            result = result or res
   3643 
   3644            # Dump the logging buffer
   3645            self.message_logger.dump_buffered()
   3646            self.log.group_end(name=m)
   3647 
   3648            if res == -1:
   3649                break
   3650 
   3651        if self.manifest is not None:
   3652            self.cleanup(options, True)
   3653 
   3654        e10s_mode = "e10s" if options.e10s else "non-e10s"
   3655 
   3656        # for failure mode: where browser window has crashed and we have no reported results
   3657        if (
   3658            self.countpass == self.countfail == self.counttodo == 0
   3659            and options.crashAsPass
   3660        ):
   3661            self.countpass = 1
   3662            self.result = 0
   3663 
   3664        # printing total number of tests
   3665        if options.flavor == "browser":
   3666            print("TEST-INFO | checking window state")
   3667            print("Browser Chrome Test Summary")
   3668            print("\tPassed: %s" % self.countpass)
   3669            print("\tFailed: %s" % self.countfail)
   3670            print("\tTodo: %s" % self.counttodo)
   3671            print("\tMode: %s" % e10s_mode)
   3672            print("*** End BrowserChrome Test Results ***")
   3673        else:
   3674            print("0 INFO TEST-START | Shutdown")
   3675            print("1 INFO Passed:  %s" % self.countpass)
   3676            print("2 INFO Failed:  %s" % self.countfail)
   3677            print("3 INFO Todo:    %s" % self.counttodo)
   3678            print("4 INFO Mode:    %s" % e10s_mode)
   3679            print("5 INFO SimpleTest FINISHED")
   3680 
   3681        if os.getenv("MOZ_AUTOMATION") and self.perfherder_data:
   3682            upload_dir = Path(os.getenv("MOZ_UPLOAD_DIR"))
   3683            for i, data in enumerate(self.perfherder_data):
   3684                out_path = upload_dir / f"perfherder-data-mochitest-{i}.json"
   3685                with out_path.open("w", encoding="utf-8") as f:
   3686                    f.write(json.dumps(data))
   3687 
   3688        self.handleShutdownProfile(options)
   3689 
   3690        if not result:
   3691            if self.countfail or not (self.countpass or self.counttodo):
   3692                # at least one test failed, or
   3693                # no tests passed, and no tests failed (possibly a crash)
   3694                result = 1
   3695 
   3696        return result
   3697 
   3698    def handleShutdownProfile(self, options):
   3699        # If shutdown profiling was enabled, then the user will want to access the
   3700        # performance profile. The following code will display helpful log messages
   3701        # and automatically open the profile if it is requested.
   3702        if self.browserEnv and "MOZ_PROFILER_SHUTDOWN" in self.browserEnv:
   3703            profile_path = self.browserEnv["MOZ_PROFILER_SHUTDOWN"]
   3704 
   3705            profiler_logger = get_proxy_logger("profiler")
   3706            profiler_logger.info("Shutdown performance profiling was enabled")
   3707            profiler_logger.info("Profile saved locally to: %s" % profile_path)
   3708 
   3709            if options.profilerSaveOnly or options.profiler:
   3710                # Only do the extra work of symbolicating and viewing the profile if
   3711                # officially requested through a command line flag. The MOZ_PROFILER_*
   3712                # flags can be set by a user.
   3713                symbolicate_profile_json(profile_path, options.symbolsPath)
   3714                view_gecko_profile_from_mochitest(
   3715                    profile_path, options, profiler_logger
   3716                )
   3717            else:
   3718                profiler_logger.info(
   3719                    "The profiler was enabled outside of the mochitests. "
   3720                    "Use --profiler instead of MOZ_PROFILER_SHUTDOWN to "
   3721                    "symbolicate and open the profile automatically."
   3722                )
   3723 
   3724            # Clean up the temporary file if it exists.
   3725            if self.profiler_tempdir:
   3726                shutil.rmtree(self.profiler_tempdir)
   3727 
   3728    def doTests(self, options, testsToFilter=None, manifestToFilter=None):
   3729        # A call to initializeLooping method is required in case of --run-by-dir or --bisect-chunk
   3730        # since we need to initialize variables for each loop.
   3731        if options.bisectChunk or options.runByManifest:
   3732            self.initializeLooping(options)
   3733 
   3734        # get debugger info, a dict of:
   3735        # {'path': path to the debugger (string),
   3736        #  'interactive': whether the debugger is interactive or not (bool)
   3737        #  'args': arguments to the debugger (list)
   3738        # TODO: use mozrunner.local.debugger_arguments:
   3739        # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
   3740 
   3741        debuggerInfo = None
   3742        if options.debugger:
   3743            debuggerInfo = mozdebug.get_debugger_info(
   3744                options.debugger, options.debuggerArgs, options.debuggerInteractive
   3745            )
   3746 
   3747        if options.useTestMediaDevices:
   3748            self.initializeVirtualAudioDevices()
   3749            devices = findTestMediaDevices(self.log)
   3750            if not devices:
   3751                self.log.error("Could not find test media devices to use")
   3752                return 1
   3753            self.mediaDevices = devices
   3754 
   3755        # See if we were asked to run on Valgrind
   3756        valgrindPath = None
   3757        valgrindArgs = None
   3758        valgrindSuppFiles = None
   3759        if options.valgrind:
   3760            valgrindPath = options.valgrind
   3761        if options.valgrindArgs:
   3762            valgrindArgs = options.valgrindArgs
   3763        if options.valgrindSuppFiles:
   3764            valgrindSuppFiles = options.valgrindSuppFiles
   3765 
   3766        if (valgrindArgs or valgrindSuppFiles) and not valgrindPath:
   3767            self.log.error(
   3768                "Specified --valgrind-args or --valgrind-supp-files, but not --valgrind"
   3769            )
   3770            return 1
   3771 
   3772        if valgrindPath and debuggerInfo:
   3773            self.log.error("Can't use both --debugger and --valgrind together")
   3774            return 1
   3775 
   3776        if valgrindPath and not valgrindSuppFiles:
   3777            valgrindSuppFiles = ",".join(get_default_valgrind_suppression_files())
   3778 
   3779        # buildProfile sets self.profile .
   3780        # This relies on sideeffects and isn't very stateful:
   3781        # https://bugzilla.mozilla.org/show_bug.cgi?id=919300
   3782        self.manifest = self.buildProfile(options)
   3783        if self.manifest is None:
   3784            return 1
   3785 
   3786        self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
   3787 
   3788        self.browserEnv = self.buildBrowserEnv(options, debuggerInfo is not None)
   3789 
   3790        if self.browserEnv is None:
   3791            return 1
   3792 
   3793        if self.mozLogs:
   3794            self.browserEnv["MOZ_LOG_FILE"] = "{}/moz-pid=%PID-uid={}.log".format(
   3795                self.browserEnv["MOZ_UPLOAD_DIR"], str(uuid.uuid4())
   3796            )
   3797 
   3798        status = 0
   3799        try:
   3800            self.startServers(options, debuggerInfo)
   3801 
   3802            if options.jsconsole:
   3803                options.browserArgs.extend(["--jsconsole"])
   3804 
   3805            if options.jsdebugger:
   3806                options.browserArgs.extend(["-wait-for-jsdebugger", "-jsdebugger"])
   3807 
   3808            # -jsdebugger takes a binary path as an optional argument.
   3809            # Append jsdebuggerPath right after `-jsdebugger`.
   3810            if options.jsdebuggerPath:
   3811                options.browserArgs.extend([options.jsdebuggerPath])
   3812 
   3813            # Remove the leak detection file so it can't "leak" to the tests run.
   3814            # The file is not there if leak logging was not enabled in the
   3815            # application build.
   3816            if os.path.exists(self.leak_report_file):
   3817                os.remove(self.leak_report_file)
   3818 
   3819            # then again to actually run mochitest
   3820            if options.timeout:
   3821                timeout = options.timeout + 30
   3822            elif options.debugger or options.jsdebugger or not options.autorun:
   3823                timeout = None
   3824            else:
   3825                # We generally want the JS harness or marionette to handle
   3826                # timeouts if they can.
   3827                # The default JS harness timeout is currently 300 seconds.
   3828                # The default Marionette socket timeout is currently 360 seconds.
   3829                # Wait a little (10 seconds) more before timing out here.
   3830                # See bug 479518 and bug 1414063.
   3831                timeout = 370.0
   3832 
   3833            if "MOZ_CHAOSMODE=0xfb" in options.environment and timeout:
   3834                timeout *= 2
   3835 
   3836            # Detect shutdown leaks for m-bc runs if
   3837            # code coverage is not enabled.
   3838            detectShutdownLeaks = False
   3839            if options.jscov_dir_prefix is None:
   3840                detectShutdownLeaks = (
   3841                    mozinfo.info["debug"]
   3842                    and options.flavor == "browser"
   3843                    and options.subsuite != "thunderbird"
   3844                    and not options.crashAsPass
   3845                )
   3846 
   3847            self.start_script_kwargs["flavor"] = self.normflavor(options.flavor)
   3848            marionette_args = {
   3849                "symbols_path": options.symbolsPath,
   3850                "socket_timeout": options.marionette_socket_timeout,
   3851                "startup_timeout": options.marionette_startup_timeout,
   3852            }
   3853 
   3854            if options.marionette:
   3855                host, port = options.marionette.split(":")
   3856                marionette_args["host"] = host
   3857                marionette_args["port"] = int(port)
   3858 
   3859            # testsToFilter parameter is used to filter out the test list that
   3860            # is sent to getTestsByScheme
   3861            for scheme, tests in self.getTestsByScheme(
   3862                options, testsToFilter, True, manifestToFilter
   3863            ):
   3864                # read the number of tests here, if we are not going to run any,
   3865                # terminate early
   3866                if not tests:
   3867                    continue
   3868 
   3869                self.currentTests = [t["path"] for t in tests]
   3870                testURL = self.buildTestURL(options, scheme=scheme)
   3871 
   3872                self.buildURLOptions(options, self.browserEnv)
   3873                if self.urlOpts:
   3874                    testURL += "?" + "&".join(self.urlOpts)
   3875 
   3876                if options.runFailures:
   3877                    testURL += "&runFailures=true"
   3878 
   3879                if options.timeoutAsPass:
   3880                    testURL += "&timeoutAsPass=true"
   3881 
   3882                if options.conditionedProfile:
   3883                    testURL += "&conditionedProfile=true"
   3884 
   3885                self.log.info(f"runtests.py | Running with scheme: {scheme}")
   3886                self.log.info(f"runtests.py | Running with e10s: {options.e10s}")
   3887                self.log.info(
   3888                    "runtests.py | Running with fission: {}".format(
   3889                        mozinfo.info.get("fission", True)
   3890                    )
   3891                )
   3892                self.log.info(
   3893                    "runtests.py | Running with cross-origin iframes: {}".format(
   3894                        mozinfo.info.get("xorigin", False)
   3895                    )
   3896                )
   3897                self.log.info(
   3898                    "runtests.py | Running with socketprocess_e10s: {}".format(
   3899                        mozinfo.info.get("socketprocess_e10s", False)
   3900                    )
   3901                )
   3902                self.log.info(f"runtests.py | Running {scheme} tests: start.\n")
   3903                ret, _ = self.runApp(
   3904                    testURL,
   3905                    self.browserEnv,
   3906                    options.app,
   3907                    profile=self.profile,
   3908                    extraArgs=options.browserArgs + self.extraArgs,
   3909                    utilityPath=options.utilityPath,
   3910                    debuggerInfo=debuggerInfo,
   3911                    valgrindPath=valgrindPath,
   3912                    valgrindArgs=valgrindArgs,
   3913                    valgrindSuppFiles=valgrindSuppFiles,
   3914                    symbolsPath=options.symbolsPath,
   3915                    timeout=timeout,
   3916                    detectShutdownLeaks=detectShutdownLeaks,
   3917                    screenshotOnFail=options.screenshotOnFail,
   3918                    bisectChunk=options.bisectChunk,
   3919                    restartAfterFailure=options.restartAfterFailure,
   3920                    marionette_args=marionette_args,
   3921                    e10s=options.e10s,
   3922                    runFailures=options.runFailures,
   3923                    crashAsPass=options.crashAsPass,
   3924                    currentManifest=manifestToFilter,
   3925                )
   3926                self.log.info(
   3927                    f"runtests.py | Running {scheme} tests: end. status: {ret}"
   3928                )
   3929                status = ret or status
   3930        except KeyboardInterrupt:
   3931            self.log.info("runtests.py | Received keyboard interrupt.\n")
   3932            status = -1
   3933        except Exception as e:
   3934            traceback.print_exc()
   3935            self.log.error(
   3936                "Automation Error: Received unexpected exception while running application\n"
   3937            )
   3938            if "ADBTimeoutError" in repr(e):
   3939                self.log.info("runtests.py | Device disconnected. Aborting test.\n")
   3940                raise
   3941            status = 1
   3942        finally:
   3943            self.stopServers()
   3944 
   3945        ignoreMissingLeaks = options.ignoreMissingLeaks
   3946        leakThresholds = options.leakThresholds
   3947 
   3948        if options.crashAsPass:
   3949            ignoreMissingLeaks.append("tab")
   3950            ignoreMissingLeaks.append("socket")
   3951 
   3952        # Provide a floor for Windows chrome leak detection, because we know
   3953        # we have some Windows-specific shutdown hangs that we avoid by timing
   3954        # out and leaking memory.
   3955        if options.flavor == "chrome" and mozinfo.isWin:
   3956            leakThresholds["default"] += 1296
   3957 
   3958        # Stop leak detection if m-bc code coverage is enabled
   3959        # by maxing out the leak threshold for all processes.
   3960        if options.jscov_dir_prefix:
   3961            for processType in leakThresholds:
   3962                ignoreMissingLeaks.append(processType)
   3963                leakThresholds[processType] = sys.maxsize
   3964 
   3965        utilityPath = options.utilityPath or options.xrePath
   3966        if status == 0:
   3967            # ignore leak checks for crashes
   3968            mozleak.process_leak_log(
   3969                self.leak_report_file,
   3970                leak_thresholds=leakThresholds,
   3971                ignore_missing_leaks=ignoreMissingLeaks,
   3972                log=self.log,
   3973                stack_fixer=get_stack_fixer_function(utilityPath, options.symbolsPath),
   3974                scope=manifestToFilter,
   3975            )
   3976 
   3977        if self.manifest is not None:
   3978            self.cleanup(options, False)
   3979 
   3980        return status
   3981 
   3982    def handleTimeout(
   3983        self,
   3984        timeout,
   3985        proc,
   3986        utilityPath,
   3987        debuggerInfo,
   3988        browser_pid,
   3989        processLog,
   3990        symbolsPath,
   3991    ):
   3992        """handle process output timeout"""
   3993        # TODO: bug 913975 : _processOutput should call self.processOutputLine
   3994        # one more time one timeout (I think)
   3995        message = {
   3996            "action": "test_end",
   3997            "status": "TIMEOUT",
   3998            "expected": "PASS",
   3999            "thread": None,
   4000            "pid": None,
   4001            "source": "mochitest",
   4002            "time": int(time.time() * 1000),
   4003            "test": self.lastTestSeen,
   4004            "message": "application timed out after %d seconds with no output"
   4005            % int(timeout),
   4006        }
   4007 
   4008        # for looping scenarios (like --restartAfterFailure), document the test
   4009        key = message["test"].split(" ")[0].split("/")[-1].strip()
   4010        if key not in self.expectedError:
   4011            self.expectedError[key] = message.get("message", message["message"]).strip()
   4012 
   4013        # need to send a test_end in order to have mozharness process messages properly
   4014        # this requires a custom message vs log.error/log.warning/etc.
   4015        self.message_logger.process_message(message)
   4016        self.message_logger.dump_buffered()
   4017        self.message_logger.buffering = False
   4018        self.log.warning("Force-terminating active process(es).")
   4019 
   4020        browser_pid = browser_pid or proc.pid
   4021 
   4022        # Send a signal to start the profiler - if we're running on Linux or MacOS
   4023        profiler_logger = get_proxy_logger("profiler")
   4024        if mozinfo.isLinux or mozinfo.isMac:
   4025            profiler_logger.warning(
   4026                "Attempting to start the profiler to help with diagnosing the hang."
   4027            )
   4028            profiler_logger.info(
   4029                "Sending SIGUSR1 to pid %d start the profiler." % browser_pid
   4030            )
   4031            os.kill(browser_pid, signal.SIGUSR1)
   4032            profiler_logger.info("Waiting 10s to capture a profile...")
   4033            time.sleep(10)
   4034            profiler_logger.info(
   4035                "Sending SIGUSR2 to pid %d stop the profiler." % browser_pid
   4036            )
   4037            os.kill(browser_pid, signal.SIGUSR2)
   4038            # We trigger `killPid` further down in this function, which will
   4039            # stop the profiler writing to disk. As we might still be writing a
   4040            # profile when we run `killPid`, we might end up with a truncated
   4041            # profile. Instead, we give the profiler ten seconds to write a
   4042            # profile (which should be plenty of time!) See Bug 1906151 for more
   4043            # details, and Bug 1905929 for an intermediate solution that would
   4044            # allow this test to watch for the profile file being completed.
   4045            profiler_logger.info("Wait 10s for Firefox to write the profile to disk.")
   4046            time.sleep(10)
   4047 
   4048            # Symbolicate the profile generated above using signals. The profile
   4049            # file will be named something like:
   4050            #  `$MOZ_UPLOAD_DIR/profile_${tid}_${pid}.json
   4051            # where `tid` is /currently/ always 0 (we can only write our profile
   4052            # from the main thread). This may change if we end up writing from
   4053            # other threads in firefox. See `profiler_find_dump_path()` in
   4054            # `platform.cpp` for more details on how we name signal-generated
   4055            # profiles.
   4056            # Sanity check that we actually have a MOZ_UPLOAD_DIR
   4057            if "MOZ_UPLOAD_DIR" in os.environ:
   4058                profiler_logger.info(
   4059                    "Symbolicating profile in %s" % os.environ["MOZ_UPLOAD_DIR"]
   4060                )
   4061                profile_path = "{}/profile_0_{}.json".format(
   4062                    os.environ["MOZ_UPLOAD_DIR"], browser_pid
   4063                )
   4064                profiler_logger.info("Looking inside symbols dir: %s)" % symbolsPath)
   4065                profiler_logger.info("Symbolicating profile: %s" % profile_path)
   4066                symbolicate_profile_json(profile_path, symbolsPath)
   4067        else:
   4068            profiler_logger.info(
   4069                "Not sending a signal to start the profiler - not on MacOS or Linux. See Bug 1823370."
   4070            )
   4071 
   4072        child_pids = self.extract_child_pids(processLog, browser_pid)
   4073        self.log.info("Found child pids: %s" % child_pids)
   4074 
   4075        if HAVE_PSUTIL:
   4076            try:
   4077                browser_proc = [psutil.Process(browser_pid)]
   4078            except Exception:
   4079                self.log.info("Failed to get proc for pid %d" % browser_pid)
   4080                browser_proc = []
   4081            try:
   4082                child_procs = [psutil.Process(pid) for pid in child_pids]
   4083            except Exception:
   4084                self.log.info("Failed to get child procs")
   4085                child_procs = []
   4086            for pid in child_pids:
   4087                self.killAndGetStack(
   4088                    pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
   4089                )
   4090            gone, alive = psutil.wait_procs(child_procs, timeout=30)
   4091            for p in gone:
   4092                self.log.info("psutil found pid %s dead" % p.pid)
   4093            for p in alive:
   4094                self.log.warning("failed to kill pid %d after 30s" % p.pid)
   4095            self.killAndGetStack(
   4096                browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
   4097            )
   4098            gone, alive = psutil.wait_procs(browser_proc, timeout=30)
   4099            for p in gone:
   4100                self.log.info("psutil found pid %s dead" % p.pid)
   4101            for p in alive:
   4102                self.log.warning("failed to kill pid %d after 30s" % p.pid)
   4103        else:
   4104            self.log.error(
   4105                "psutil not available! Will wait 30s before "
   4106                "attempting to kill parent process. This should "
   4107                "not occur in mozilla automation. See bug 1143547."
   4108            )
   4109            for pid in child_pids:
   4110                self.killAndGetStack(
   4111                    pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
   4112                )
   4113            if child_pids:
   4114                time.sleep(30)
   4115 
   4116            self.killAndGetStack(
   4117                browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
   4118            )
   4119 
   4120    def archiveMozLogs(self):
   4121        if self.mozLogs:
   4122            with zipfile.ZipFile(
   4123                "{}/mozLogs.zip".format(os.environ["MOZ_UPLOAD_DIR"]),
   4124                "w",
   4125                zipfile.ZIP_DEFLATED,
   4126            ) as logzip:
   4127                for logfile in glob.glob(
   4128                    "{}/moz*.log*".format(os.environ["MOZ_UPLOAD_DIR"])
   4129                ):
   4130                    logzip.write(logfile, os.path.basename(logfile))
   4131                    os.remove(logfile)
   4132                logzip.close()
   4133 
   4134    class OutputHandler:
   4135        """line output handler for mozrunner"""
   4136 
   4137        def __init__(
   4138            self,
   4139            harness,
   4140            utilityPath,
   4141            symbolsPath=None,
   4142            dump_screen_on_timeout=True,
   4143            dump_screen_on_fail=False,
   4144            shutdownLeaks=None,
   4145            lsanLeaks=None,
   4146            bisectChunk=None,
   4147            restartAfterFailure=None,
   4148        ):
   4149            """
   4150            harness -- harness instance
   4151            dump_screen_on_timeout -- whether to dump the screen on timeout
   4152            """
   4153            self.harness = harness
   4154            self.utilityPath = utilityPath
   4155            self.symbolsPath = symbolsPath
   4156            self.dump_screen_on_timeout = dump_screen_on_timeout
   4157            self.dump_screen_on_fail = dump_screen_on_fail
   4158            self.shutdownLeaks = shutdownLeaks
   4159            self.lsanLeaks = lsanLeaks
   4160            self.bisectChunk = bisectChunk
   4161            self.restartAfterFailure = restartAfterFailure
   4162            self.browserProcessId = None
   4163            self.stackFixerFunction = self.stackFixer()
   4164 
   4165        def processOutputLine(self, line):
   4166            """per line handler of output for mozprocess"""
   4167            # Parsing the line (by the structured messages logger).
   4168            messages = self.harness.message_logger.parse_line(line)
   4169 
   4170            for message in messages:
   4171                # Passing the message to the handlers
   4172                msg = message
   4173                for handler in self.outputHandlers():
   4174                    msg = handler(msg)
   4175 
   4176                # Processing the message by the logger
   4177                self.harness.message_logger.process_message(msg)
   4178                self.parse_perfherder_data(msg)
   4179 
   4180        __call__ = processOutputLine
   4181 
   4182        def outputHandlers(self):
   4183            """returns ordered list of output handlers"""
   4184            handlers = [
   4185                self.fix_stack,
   4186                self.record_last_test,
   4187                self.dumpScreenOnTimeout,
   4188                self.dumpScreenOnFail,
   4189                self.trackShutdownLeaks,
   4190                self.trackLSANLeaks,
   4191                self.countline,
   4192            ]
   4193            if self.bisectChunk or self.restartAfterFailure:
   4194                handlers.append(self.record_result)
   4195                handlers.append(self.first_error)
   4196 
   4197            return handlers
   4198 
   4199        def stackFixer(self):
   4200            """
   4201            return get_stack_fixer_function, if any, to use on the output lines
   4202            """
   4203            return get_stack_fixer_function(self.utilityPath, self.symbolsPath)
   4204 
   4205        def finish(self):
   4206            if self.shutdownLeaks:
   4207                numFailures, errorMessages = self.shutdownLeaks.process()
   4208                self.harness.countfail += numFailures
   4209                for message in errorMessages:
   4210                    msg = {
   4211                        "action": "test_end",
   4212                        "status": "FAIL",
   4213                        "expected": "PASS",
   4214                        "thread": None,
   4215                        "pid": None,
   4216                        "source": "mochitest",
   4217                        "time": int(time.time() * 1000),
   4218                        "test": message["test"],
   4219                        "message": message["msg"],
   4220                    }
   4221                    self.harness.message_logger.process_message(msg)
   4222 
   4223            if self.lsanLeaks:
   4224                self.harness.countfail += self.lsanLeaks.process()
   4225 
   4226        # output message handlers:
   4227        # these take a message and return a message
   4228 
   4229        def record_result(self, message):
   4230            # by default make the result key equal to pass.
   4231            if message["action"] == "test_start":
   4232                key = message["test"].split("/")[-1].strip()
   4233                self.harness.result[key] = "PASS"
   4234            elif message["action"] == "test_status":
   4235                if "expected" in message:
   4236                    key = message["test"].split("/")[-1].strip()
   4237                    self.harness.result[key] = "FAIL"
   4238                elif message["status"] == "FAIL":
   4239                    key = message["test"].split("/")[-1].strip()
   4240                    self.harness.result[key] = "TODO"
   4241            return message
   4242 
   4243        def first_error(self, message):
   4244            if (
   4245                message["action"] == "test_status"
   4246                and "expected" in message
   4247                and message["status"] == "FAIL"
   4248            ):
   4249                key = message["test"].split("/")[-1].strip()
   4250                if key not in self.harness.expectedError:
   4251                    error_msg = message.get("message") or message.get("subtest") or ""
   4252                    self.harness.expectedError[key] = error_msg.strip()
   4253            return message
   4254 
   4255        def countline(self, message):
   4256            if message["action"] == "log":
   4257                line = message.get("message", "")
   4258            elif message["action"] == "process_output":
   4259                line = message.get("data", "")
   4260            else:
   4261                return message
   4262            val = 0
   4263            try:
   4264                val = int(line.split(":")[-1].strip())
   4265            except (AttributeError, ValueError):
   4266                return message
   4267 
   4268            if "Passed:" in line:
   4269                self.harness.countpass += val
   4270            elif "Failed:" in line:
   4271                self.harness.countfail += val
   4272            elif "Todo:" in line:
   4273                self.harness.counttodo += val
   4274            return message
   4275 
   4276        def fix_stack(self, message):
   4277            if self.stackFixerFunction:
   4278                if message["action"] == "log":
   4279                    message["message"] = self.stackFixerFunction(message["message"])
   4280                elif message["action"] == "process_output":
   4281                    message["data"] = self.stackFixerFunction(message["data"])
   4282            return message
   4283 
   4284        def record_last_test(self, message):
   4285            """record last test on harness"""
   4286            if message["action"] == "test_start":
   4287                self.harness.lastTestSeen = message["test"]
   4288            elif message["action"] == "test_end":
   4289                self.harness.lastTestSeen = "{} (finished)".format(message["test"])
   4290            return message
   4291 
   4292        def dumpScreenOnTimeout(self, message):
   4293            if (
   4294                not self.dump_screen_on_fail
   4295                and self.dump_screen_on_timeout
   4296                and message["action"] == "test_status"
   4297                and "expected" in message
   4298                and message["subtest"] is not None
   4299                and "Test timed out" in message["subtest"]
   4300            ):
   4301                self.harness.dumpScreen(self.utilityPath)
   4302            return message
   4303 
   4304        def dumpScreenOnFail(self, message):
   4305            if (
   4306                self.dump_screen_on_fail
   4307                and "expected" in message
   4308                and message["status"] == "FAIL"
   4309            ):
   4310                self.harness.dumpScreen(self.utilityPath)
   4311            return message
   4312 
   4313        def trackLSANLeaks(self, message):
   4314            if self.lsanLeaks and message["action"] in ("log", "process_output"):
   4315                line = (
   4316                    message.get("message", "")
   4317                    if message["action"] == "log"
   4318                    else message["data"]
   4319                )
   4320                if "(finished)" in self.harness.lastTestSeen:
   4321                    self.lsanLeaks.log(line, self.harness.lastManifest)
   4322                else:
   4323                    self.lsanLeaks.log(line, self.harness.lastTestSeen)
   4324            return message
   4325 
   4326        def trackShutdownLeaks(self, message):
   4327            if self.shutdownLeaks:
   4328                self.shutdownLeaks.log(message)
   4329            return message
   4330 
   4331        def parse_perfherder_data(self, message):
   4332            PERFHERDER_MATCHER = re.compile(r"PERFHERDER_DATA:\s*(\{.*\})\s*$")
   4333            match = PERFHERDER_MATCHER.search(message.get("message", ""))
   4334            if match:
   4335                data = json.loads(match.group(1))
   4336                self.harness.perfherder_data.append(data)
   4337 
   4338 
   4339 def view_gecko_profile_from_mochitest(profile_path, options, profiler_logger):
   4340    """Getting shutdown performance profiles from just the command line arguments is
   4341    difficult. This function makes the developer ergonomics a bit easier by taking the
   4342    generated Gecko profile, and automatically serving it to profiler.firefox.com. The
   4343    Gecko profile during shutdown is dumped to disk at:
   4344 
   4345    {objdir}/_tests/testing/mochitest/{profilename}
   4346 
   4347    This function takes that file, and launches a local webserver, and then points
   4348    a browser to profiler.firefox.com to view it. From there it's easy to publish
   4349    or save the profile.
   4350    """
   4351 
   4352    if options.profilerSaveOnly:
   4353        # The user did not want this to automatically open, only share the location.
   4354        return
   4355 
   4356    if not os.path.exists(profile_path):
   4357        profiler_logger.error(
   4358            "No profile was found at the profile path, cannot "
   4359            "launch profiler.firefox.com."
   4360        )
   4361        return
   4362 
   4363    profiler_logger.info("Loading this profile in the Firefox Profiler")
   4364 
   4365    view_gecko_profile(profile_path)
   4366 
   4367 
   4368 def run_test_harness(parser, options):
   4369    parser.validate(options)
   4370 
   4371    logger_options = {
   4372        key: value
   4373        for key, value in vars(options).items()
   4374        if key.startswith("log") or key == "valgrind"
   4375    }
   4376 
   4377    runner = MochitestDesktop(
   4378        options.flavor, logger_options, options.stagedAddons, quiet=options.quiet
   4379    )
   4380 
   4381    options.runByManifest = False
   4382    if options.flavor in ("plain", "a11y", "browser", "chrome"):
   4383        options.runByManifest = True
   4384 
   4385    # run until failure, then loop until all tests have ran
   4386    # using looping similar to bisection code
   4387    if options.restartAfterFailure:
   4388        options.runUntilFailure = True
   4389 
   4390    if options.verify or options.verify_fission:
   4391        result = runner.verifyTests(options)
   4392    else:
   4393        result = runner.runTests(options)
   4394 
   4395    runner.archiveMozLogs()
   4396    runner.message_logger.finish()
   4397    return result
   4398 
   4399 
   4400 def cli(args=sys.argv[1:]):
   4401    # parse command line options
   4402    parser = MochitestArgumentParser(app="generic")
   4403    options = parser.parse_args(args)
   4404    if options is None:
   4405        # parsing error
   4406        sys.exit(1)
   4407 
   4408    return run_test_harness(parser, options)
   4409 
   4410 
   4411 if __name__ == "__main__":
   4412    sys.exit(cli())