tor-browser

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

runreftest.py (43159B)


      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 reftest test harness.
      7 """
      8 
      9 import json
     10 import os
     11 import platform
     12 import posixpath
     13 import re
     14 import shutil
     15 import signal
     16 import subprocess
     17 import sys
     18 import tempfile
     19 import threading
     20 from collections import defaultdict
     21 from datetime import datetime, timedelta
     22 
     23 SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
     24 if SCRIPT_DIRECTORY not in sys.path:
     25    sys.path.insert(0, SCRIPT_DIRECTORY)
     26 
     27 import mozcrash
     28 import mozdebug
     29 import mozfile
     30 import mozinfo
     31 import mozleak
     32 import mozlog
     33 import mozprocess
     34 import mozprofile
     35 import mozrunner
     36 from manifestparser import TestManifest
     37 from manifestparser import filters as mpf
     38 from mozrunner.utils import get_stack_fixer_function, test_environment
     39 from mozscreenshot import dump_screen, printstatus
     40 
     41 try:
     42    from marionette_driver.addons import Addons
     43    from marionette_driver.marionette import Marionette
     44 except ImportError as e:  # noqa
     45    # Defer ImportError until attempt to use Marionette.
     46    # Python 3 deletes the exception once the except block
     47    # is exited. Save a version to raise later.
     48    e_save = ImportError(str(e))
     49 
     50    def reraise_(*args, **kwargs):
     51        raise (e_save)  # noqa
     52 
     53    Marionette = reraise_
     54 
     55 import reftestcommandline
     56 from output import OutputHandler, ReftestFormatter
     57 
     58 here = os.path.abspath(os.path.dirname(__file__))
     59 
     60 try:
     61    from mozbuild.base import MozbuildObject
     62    from mozbuild.util import cpu_count
     63 
     64    build_obj = MozbuildObject.from_environment(cwd=here)
     65 except ImportError:
     66    build_obj = None
     67    from multiprocessing import cpu_count
     68 
     69 
     70 def categoriesToRegex(categoryList):
     71    return "\\(" + ", ".join(["(?P<%s>\\d+) %s" % c for c in categoryList]) + "\\)"
     72 
     73 
     74 summaryLines = [
     75    ("Successful", [("pass", "pass"), ("loadOnly", "load only")]),
     76    (
     77        "Unexpected",
     78        [
     79            ("fail", "unexpected fail"),
     80            ("pass", "unexpected pass"),
     81            ("asserts", "unexpected asserts"),
     82            ("fixedAsserts", "unexpected fixed asserts"),
     83            ("failedLoad", "failed load"),
     84            ("exception", "exception"),
     85        ],
     86    ),
     87    (
     88        "Known problems",
     89        [
     90            ("knownFail", "known fail"),
     91            ("knownAsserts", "known asserts"),
     92            ("random", "random"),
     93            ("skipped", "skipped"),
     94            ("slow", "slow"),
     95        ],
     96    ),
     97 ]
     98 
     99 
    100 def reraise_(tp_, value_, tb_=None):
    101    if value_ is None:
    102        value_ = tp_()
    103    if value_.__traceback__ is not tb_:
    104        raise value_.with_traceback(tb_)
    105    raise value_
    106 
    107 
    108 def update_mozinfo():
    109    """walk up directories to find mozinfo.json update the info"""
    110    # TODO: This should go in a more generic place, e.g. mozinfo
    111 
    112    path = SCRIPT_DIRECTORY
    113    dirs = set()
    114    while path != os.path.expanduser("~"):
    115        if path in dirs:
    116            break
    117        dirs.add(path)
    118        path = os.path.split(path)[0]
    119    mozinfo.find_and_update_from_json(*dirs)
    120 
    121 
    122 # Python's print is not threadsafe.
    123 printLock = threading.Lock()
    124 
    125 
    126 class ReftestThread(threading.Thread):
    127    def __init__(self, cmdargs):
    128        threading.Thread.__init__(self)
    129        self.cmdargs = cmdargs
    130        self.summaryMatches = {}
    131        self.retcode = -1
    132        for text, _ in summaryLines:
    133            self.summaryMatches[text] = None
    134 
    135    def run(self):
    136        with printLock:
    137            print("Starting thread with", self.cmdargs)
    138            sys.stdout.flush()
    139        process = subprocess.Popen(self.cmdargs, stdout=subprocess.PIPE)
    140        for chunk in self.chunkForMergedOutput(process.stdout):
    141            with printLock:
    142                print(chunk, end=" ")
    143                sys.stdout.flush()
    144        self.retcode = process.wait()
    145 
    146    def chunkForMergedOutput(self, logsource):
    147        """Gather lines together that should be printed as one atomic unit.
    148        Individual test results--anything between 'REFTEST TEST-START' and
    149        'REFTEST TEST-END' lines--are an atomic unit.  Lines with data from
    150        summaries are parsed and the data stored for later aggregation.
    151        Other lines are considered their own atomic units and are permitted
    152        to intermix freely."""
    153        testStartRegex = re.compile("^REFTEST TEST-START")
    154        testEndRegex = re.compile("^REFTEST TEST-END")
    155        summaryHeadRegex = re.compile("^REFTEST INFO \\| Result summary:")
    156        summaryRegexFormatString = (
    157            "^REFTEST INFO \\| (?P<message>{text}): (?P<total>\\d+) {regex}"
    158        )
    159        summaryRegexStrings = [
    160            summaryRegexFormatString.format(
    161                text=text, regex=categoriesToRegex(categories)
    162            )
    163            for (text, categories) in summaryLines
    164        ]
    165        summaryRegexes = [re.compile(regex) for regex in summaryRegexStrings]
    166 
    167        for line in logsource:
    168            if testStartRegex.search(line) is not None:
    169                chunkedLines = [line]
    170                for lineToBeChunked in logsource:
    171                    chunkedLines.append(lineToBeChunked)
    172                    if testEndRegex.search(lineToBeChunked) is not None:
    173                        break
    174                yield "".join(chunkedLines)
    175                continue
    176 
    177            haveSuppressedSummaryLine = False
    178            for regex in summaryRegexes:
    179                match = regex.search(line)
    180                if match is not None:
    181                    self.summaryMatches[match.group("message")] = match
    182                    haveSuppressedSummaryLine = True
    183                    break
    184            if haveSuppressedSummaryLine:
    185                continue
    186 
    187            if summaryHeadRegex.search(line) is None:
    188                yield line
    189 
    190 
    191 class ReftestResolver:
    192    def defaultManifest(self, suite):
    193        return {
    194            "reftest": "reftest.list",
    195            "crashtest": "crashtests.list",
    196            "jstestbrowser": "jstests.list",
    197        }[suite]
    198 
    199    def directoryManifest(self, suite, path):
    200        return os.path.join(path, self.defaultManifest(suite))
    201 
    202    def findManifest(self, suite, test_file, subdirs=True):
    203        """Return a tuple of (manifest-path, filter-string) for running test_file.
    204 
    205        test_file is a path to a test or a manifest file
    206        """
    207        rv = []
    208        default_manifest = self.defaultManifest(suite)
    209        relative_path = None
    210        if not os.path.isabs(test_file):
    211            relative_path = test_file
    212            test_file = self.absManifestPath(test_file)
    213 
    214        if os.path.isdir(test_file):
    215            for dirpath, dirnames, filenames in os.walk(test_file):
    216                if default_manifest in filenames:
    217                    rv.append((os.path.join(dirpath, default_manifest), None))
    218                    # We keep recursing into subdirectories which means that in the case
    219                    # of include directives we get the same manifest multiple times.
    220                    # However reftest.js will only read each manifest once
    221 
    222            if (
    223                len(rv) == 0
    224                and relative_path
    225                and suite == "jstestbrowser"
    226                and build_obj
    227            ):
    228                # The relative path can be from staging area.
    229                staged_js_dir = os.path.join(
    230                    build_obj.topobjdir, "dist", "test-stage", "jsreftest"
    231                )
    232                staged_file = os.path.join(staged_js_dir, "tests", relative_path)
    233                return self.findManifest(suite, staged_file, subdirs)
    234        elif test_file.endswith(".list"):
    235            if os.path.exists(test_file):
    236                rv = [(test_file, None)]
    237        else:
    238            dirname, pathname = os.path.split(test_file)
    239            found = True
    240            while not os.path.exists(os.path.join(dirname, default_manifest)):
    241                dirname, suffix = os.path.split(dirname)
    242                pathname = posixpath.join(suffix, pathname)
    243                if os.path.dirname(dirname) == dirname:
    244                    found = False
    245                    break
    246            if found:
    247                rv = [
    248                    (
    249                        os.path.join(dirname, default_manifest),
    250                        r".*%s(?:[#?].*)?$" % pathname.replace("?", r"\?"),
    251                    )
    252                ]
    253 
    254        return rv
    255 
    256    def absManifestPath(self, path):
    257        return os.path.normpath(os.path.abspath(path))
    258 
    259    def manifestURL(self, options, path):
    260        return "file://%s" % path
    261 
    262    def resolveManifests(self, options, tests):
    263        suite = options.suite
    264        manifests = {}
    265        for testPath in tests:
    266            for manifest, filter_str in self.findManifest(suite, testPath):
    267                if manifest not in manifests:
    268                    manifests[manifest] = set()
    269                manifests[manifest].add(filter_str)
    270        manifests_by_url = {}
    271        for key in manifests.keys():
    272            id = os.path.relpath(
    273                os.path.abspath(os.path.dirname(key)), options.topsrcdir
    274            )
    275            id = id.replace(os.sep, posixpath.sep)
    276            if None in manifests[key]:
    277                manifests[key] = (None, id)
    278            else:
    279                manifests[key] = ("|".join(list(manifests[key])), id)
    280            url = self.manifestURL(options, key)
    281            manifests_by_url[url] = manifests[key]
    282        return manifests_by_url
    283 
    284 
    285 class RefTest:
    286    oldcwd = os.getcwd()
    287    resolver_cls = ReftestResolver
    288    use_marionette = True
    289 
    290    def __init__(self, suite):
    291        update_mozinfo()
    292        self.lastTestSeen = None
    293        self.lastTest = None
    294        self.haveDumpedScreen = False
    295        self.resolver = self.resolver_cls()
    296        self.log = None
    297        self.outputHandler = None
    298        self.testDumpFile = os.path.join(tempfile.gettempdir(), "reftests.json")
    299        self.currentManifest = "No test started"
    300        self.gtkTheme = self.getGtkTheme()
    301 
    302        self.run_by_manifest = True
    303        if suite in ("crashtest", "jstestbrowser"):
    304            self.run_by_manifest = False
    305 
    306    def _populate_logger(self, options):
    307        if self.log:
    308            return
    309 
    310        self.log = getattr(options, "log", None)
    311        if self.log:
    312            return
    313 
    314        mozlog.commandline.log_formatters["tbpl"] = (
    315            ReftestFormatter,
    316            "Reftest specific formatter for the"
    317            "benefit of legacy log parsers and"
    318            "tools such as the reftest analyzer",
    319        )
    320        fmt_options = {}
    321        if not options.log_tbpl_level and os.environ.get("MOZ_REFTEST_VERBOSE"):
    322            options.log_tbpl_level = fmt_options["level"] = "debug"
    323        self.log = mozlog.commandline.setup_logging(
    324            "reftest harness", options, {"tbpl": sys.stdout}, fmt_options
    325        )
    326 
    327    def getGtkTheme(self):
    328        if not platform.system() == "Linux":
    329            return ""
    330 
    331        try:
    332            theme_cmd = "gsettings get org.gnome.desktop.interface gtk-theme"
    333            theme = subprocess.check_output(
    334                theme_cmd, shell=True, universal_newlines=True
    335            )
    336            if theme:
    337                theme = theme.strip("\n")
    338                theme = theme.strip("'")
    339            return theme.strip()
    340        except subprocess.CalledProcessError:
    341            return ""
    342 
    343    def getFullPath(self, path):
    344        "Get an absolute path relative to self.oldcwd."
    345        return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
    346 
    347    def createReftestProfile(
    348        self,
    349        options,
    350        tests=None,
    351        manifests=None,
    352        server="localhost",
    353        port=0,
    354        profile_to_clone=None,
    355        prefs=None,
    356    ):
    357        """Sets up a profile for reftest.
    358 
    359        :param options: Object containing command line options
    360        :param tests: List of test objects to run
    361        :param manifests: List of manifest files to parse (only takes effect
    362                          if tests were not passed in)
    363        :param server: Server name to use for http tests
    364        :param profile_to_clone: Path to a profile to use as the basis for the
    365                                 test profile
    366        :param prefs: Extra preferences to set in the profile
    367        """
    368        locations = mozprofile.permissions.ServerLocations()
    369        locations.add_host(server, scheme="http", port=port)
    370        locations.add_host(server, scheme="https", port=port)
    371 
    372        sandbox_allowlist_paths = options.sandboxReadWhitelist
    373        if platform.system() == "Linux" or platform.system() in (
    374            "Windows",
    375            "Microsoft",
    376        ):
    377            # Trailing slashes are needed to indicate directories on Linux and Windows
    378            sandbox_allowlist_paths = map(
    379                lambda p: os.path.join(p, ""), sandbox_allowlist_paths
    380            )
    381 
    382        addons = []
    383        if not self.use_marionette:
    384            addons.append(options.reftestExtensionPath)
    385 
    386        if options.specialPowersExtensionPath is not None:
    387            if not self.use_marionette:
    388                addons.append(options.specialPowersExtensionPath)
    389 
    390        # Install distributed extensions, if application has any.
    391        distExtDir = os.path.join(
    392            options.app[: options.app.rfind(os.sep)], "distribution", "extensions"
    393        )
    394        if os.path.isdir(distExtDir):
    395            for f in os.listdir(distExtDir):
    396                addons.append(os.path.join(distExtDir, f))
    397 
    398        # Install custom extensions.
    399        for f in options.extensionsToInstall:
    400            addons.append(self.getFullPath(f))
    401 
    402        kwargs = {
    403            "addons": addons,
    404            "locations": locations,
    405            "allowlistpaths": sandbox_allowlist_paths,
    406        }
    407        if profile_to_clone:
    408            profile = mozprofile.Profile.clone(profile_to_clone, **kwargs)
    409        else:
    410            profile = mozprofile.Profile(**kwargs)
    411 
    412        # First set prefs from the base profiles under testing/profiles.
    413 
    414        # In test packages used in CI, the profile_data directory is installed
    415        # in the SCRIPT_DIRECTORY.
    416        profile_data_dir = os.path.join(SCRIPT_DIRECTORY, "profile_data")
    417        # If possible, read profile data from topsrcdir. This prevents us from
    418        # requiring a re-build to pick up newly added extensions in the
    419        # <profile>/extensions directory.
    420        if build_obj:
    421            path = os.path.join(build_obj.topsrcdir, "testing", "profiles")
    422            if os.path.isdir(path):
    423                profile_data_dir = path
    424        # Still not found? Look for testing/profiles relative to layout/tools/reftest.
    425        if not os.path.isdir(profile_data_dir):
    426            path = os.path.abspath(
    427                os.path.join(SCRIPT_DIRECTORY, "..", "..", "..", "testing", "profiles")
    428            )
    429            if os.path.isdir(path):
    430                profile_data_dir = path
    431 
    432        with open(os.path.join(profile_data_dir, "profiles.json")) as fh:
    433            base_profiles = json.load(fh)["reftest"]
    434 
    435        for name in base_profiles:
    436            path = os.path.join(profile_data_dir, name)
    437            profile.merge(path)
    438 
    439        # Second set preferences for communication between our command line
    440        # arguments and the reftest harness. Preferences that are required for
    441        # reftest to work should instead be set under srcdir/testing/profiles.
    442        prefs = prefs or {}
    443        prefs["reftest.timeout"] = options.timeout * 1000
    444        if options.logFile:
    445            prefs["reftest.logFile"] = options.logFile
    446        if options.ignoreWindowSize:
    447            prefs["reftest.ignoreWindowSize"] = True
    448        if options.shuffle:
    449            prefs["reftest.shuffle"] = True
    450        if options.repeat:
    451            prefs["reftest.repeat"] = options.repeat
    452        if options.runUntilFailure:
    453            prefs["reftest.runUntilFailure"] = True
    454            if not options.repeat:
    455                prefs["reftest.repeat"] = 30
    456        if options.verify:
    457            prefs["reftest.verify"] = True
    458        if options.cleanupCrashes:
    459            prefs["reftest.cleanupPendingCrashes"] = True
    460        prefs["reftest.focusFilterMode"] = options.focusFilterMode
    461        prefs["reftest.logLevel"] = options.log_tbpl_level or "info"
    462        prefs["reftest.suite"] = options.suite
    463        prefs["sandbox.mozinfo"] = json.dumps(mozinfo.info)
    464 
    465        # Set tests to run or manifests to parse.
    466        if tests:
    467            testlist = os.path.join(profile.profile, "reftests.json")
    468            with open(testlist, "w") as fh:
    469                json.dump(tests, fh)
    470            prefs["reftest.tests"] = testlist
    471        elif manifests:
    472            prefs["reftest.manifests"] = json.dumps(manifests)
    473 
    474        # Avoid unncessary recursion when MOZHARNESS_TEST_PATHS is set
    475        prefs["reftest.mozharness_test_paths"] = (
    476            len(os.environ.get("MOZHARNESS_TEST_PATHS", "")) > 0
    477        )
    478 
    479        # default fission to True
    480        prefs["fission.autostart"] = True
    481        if options.disableFission:
    482            prefs["fission.autostart"] = False
    483 
    484        if not self.run_by_manifest:
    485            if options.totalChunks:
    486                prefs["reftest.totalChunks"] = options.totalChunks
    487            if options.thisChunk:
    488                prefs["reftest.thisChunk"] = options.thisChunk
    489 
    490        if options.marionette:
    491            # options.marionette can specify host:port
    492            port = options.marionette.split(":")[1]
    493            prefs["marionette.port"] = int(port)
    494 
    495        # Enable tracing output for detailed failures in case of
    496        # failing connection attempts, and hangs (bug 1397201)
    497        prefs["remote.log.level"] = "Trace"
    498 
    499        # Third, set preferences passed in via the command line.
    500        for v in options.extraPrefs:
    501            thispref = v.split("=")
    502            if len(thispref) < 2:
    503                print("Error: syntax error in --setpref=" + v)
    504                sys.exit(1)
    505            prefs[thispref[0]] = thispref[1].strip()
    506 
    507        for pref in prefs:
    508            prefs[pref] = mozprofile.Preferences.cast(prefs[pref])
    509        profile.set_preferences(prefs)
    510 
    511        if os.path.join(here, "chrome") not in options.extraProfileFiles:
    512            options.extraProfileFiles.append(os.path.join(here, "chrome"))
    513 
    514        self.copyExtraFilesToProfile(options, profile)
    515 
    516        self.log.info(f"Running with e10s: {options.e10s}")
    517        self.log.info("Running with fission: {}".format(prefs["fission.autostart"]))
    518 
    519        return profile
    520 
    521    def environment(self, **kwargs):
    522        kwargs["log"] = self.log
    523        return test_environment(**kwargs)
    524 
    525    def buildBrowserEnv(self, options, profileDir):
    526        browserEnv = self.environment(
    527            xrePath=options.xrePath, debugger=options.debugger
    528        )
    529        browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
    530 
    531        if mozinfo.info["asan"]:
    532            # Disable leak checking for reftests for now
    533            if "ASAN_OPTIONS" in browserEnv:
    534                browserEnv["ASAN_OPTIONS"] += ":detect_leaks=0"
    535            else:
    536                browserEnv["ASAN_OPTIONS"] = "detect_leaks=0"
    537 
    538        # Set environment defaults for jstestbrowser. Keep in sync with the
    539        # defaults used in js/src/tests/lib/tests.py.
    540        if options.suite == "jstestbrowser":
    541            browserEnv["TZ"] = "PST8PDT"
    542            browserEnv["LC_ALL"] = "en_US.UTF-8"
    543 
    544        for v in options.environment:
    545            ix = v.find("=")
    546            if ix <= 0:
    547                print("Error: syntax error in --setenv=" + v)
    548                return None
    549            browserEnv[v[:ix]] = v[ix + 1 :]
    550 
    551        # Enable leaks detection to its own log file.
    552        self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log")
    553        browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile
    554 
    555        # TODO: this is always defined (as part of --enable-webrender which is default)
    556        #       can we make this default in the browser?
    557        browserEnv["MOZ_ACCELERATED"] = "1"
    558 
    559        if options.headless:
    560            browserEnv["MOZ_HEADLESS"] = "1"
    561 
    562        if not options.e10s:
    563            browserEnv["MOZ_FORCE_DISABLE_E10S"] = "1"
    564 
    565        return browserEnv
    566 
    567    def cleanup(self, profileDir):
    568        if profileDir:
    569            shutil.rmtree(profileDir, True)
    570 
    571    def verifyTests(self, tests, options):
    572        """
    573        Support --verify mode: Run test(s) many times in a variety of
    574        configurations/environments in an effort to find intermittent
    575        failures.
    576        """
    577 
    578        self._populate_logger(options)
    579 
    580        # Number of times to repeat test(s) when running with --repeat
    581        VERIFY_REPEAT = 10
    582        # Number of times to repeat test(s) when running test in separate browser
    583        VERIFY_REPEAT_SINGLE_BROWSER = 5
    584 
    585        def step1():
    586            options.repeat = VERIFY_REPEAT
    587            options.runUntilFailure = True
    588            result = self.runTests(tests, options)
    589            return result
    590 
    591        def step2():
    592            options.repeat = 0
    593            options.runUntilFailure = False
    594            for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
    595                result = self.runTests(tests, options)
    596                if result != 0:
    597                    break
    598            return result
    599 
    600        def step3():
    601            options.repeat = VERIFY_REPEAT
    602            options.runUntilFailure = True
    603            options.environment.append("MOZ_CHAOSMODE=0xfb")
    604            result = self.runTests(tests, options)
    605            options.environment.remove("MOZ_CHAOSMODE=0xfb")
    606            return result
    607 
    608        def step4():
    609            options.repeat = 0
    610            options.runUntilFailure = False
    611            options.environment.append("MOZ_CHAOSMODE=0xfb")
    612            for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
    613                result = self.runTests(tests, options)
    614                if result != 0:
    615                    break
    616            options.environment.remove("MOZ_CHAOSMODE=0xfb")
    617            return result
    618 
    619        steps = [
    620            ("1. Run each test %d times in one browser." % VERIFY_REPEAT, step1),
    621            (
    622                "2. Run each test %d times in a new browser each time."
    623                % VERIFY_REPEAT_SINGLE_BROWSER,
    624                step2,
    625            ),
    626            (
    627                "3. Run each test %d times in one browser, in chaos mode."
    628                % VERIFY_REPEAT,
    629                step3,
    630            ),
    631            (
    632                "4. Run each test %d times in a new browser each time, in chaos mode."
    633                % VERIFY_REPEAT_SINGLE_BROWSER,
    634                step4,
    635            ),
    636        ]
    637 
    638        stepResults = {}
    639        for descr, step in steps:
    640            stepResults[descr] = "not run / incomplete"
    641 
    642        startTime = datetime.now()
    643        maxTime = timedelta(seconds=options.verify_max_time)
    644        finalResult = "PASSED"
    645        for descr, step in steps:
    646            if (datetime.now() - startTime) > maxTime:
    647                self.log.info("::: Test verification is taking too long: Giving up!")
    648                self.log.info(
    649                    "::: So far, all checks passed, but not all checks were run."
    650                )
    651                break
    652            self.log.info(":::")
    653            self.log.info('::: Running test verification step "%s"...' % descr)
    654            self.log.info(":::")
    655            result = step()
    656            if result != 0:
    657                stepResults[descr] = "FAIL"
    658                finalResult = "FAILED!"
    659                break
    660            stepResults[descr] = "Pass"
    661 
    662        self.log.info(":::")
    663        self.log.info("::: Test verification summary for:")
    664        self.log.info(":::")
    665        for test in tests:
    666            self.log.info("::: " + test)
    667        self.log.info(":::")
    668        for descr in sorted(stepResults.keys()):
    669            self.log.info("::: %s : %s" % (descr, stepResults[descr]))
    670        self.log.info(":::")
    671        self.log.info("::: Test verification %s" % finalResult)
    672        self.log.info(":::")
    673 
    674        return result
    675 
    676    def runTests(self, tests, options, cmdargs=None):
    677        cmdargs = cmdargs or []
    678        self._populate_logger(options)
    679        self.outputHandler = OutputHandler(
    680            self.log, options.utilityPath, options.symbolsPath
    681        )
    682 
    683        if options.cleanupCrashes:
    684            mozcrash.cleanup_pending_crash_reports()
    685 
    686        manifests = self.resolver.resolveManifests(options, tests)
    687        if options.filter:
    688            manifests[""] = (options.filter, None)
    689 
    690        if not getattr(options, "runTestsInParallel", False):
    691            return self.runSerialTests(manifests, options, cmdargs)
    692 
    693        cpuCount = cpu_count()
    694 
    695        # We have the directive, technology, and machine to run multiple test instances.
    696        # Experimentation says that reftests are not overly CPU-intensive, so we can run
    697        # multiple jobs per CPU core.
    698        #
    699        # Our Windows machines in automation seem to get upset when we run a lot of
    700        # simultaneous tests on them, so tone things down there.
    701        if sys.platform == "win32":
    702            jobsWithoutFocus = cpuCount
    703        else:
    704            jobsWithoutFocus = 2 * cpuCount
    705 
    706        totalJobs = jobsWithoutFocus + 1
    707        perProcessArgs = [sys.argv[:] for i in range(0, totalJobs)]
    708 
    709        host = "localhost"
    710        port = 2828
    711        if options.marionette:
    712            host, port = options.marionette.split(":")
    713 
    714        # First job is only needs-focus tests.  Remaining jobs are
    715        # non-needs-focus and chunked.
    716        perProcessArgs[0].insert(-1, "--focus-filter-mode=needs-focus")
    717        for chunkNumber, jobArgs in enumerate(perProcessArgs[1:], start=1):
    718            jobArgs[-1:-1] = [
    719                "--focus-filter-mode=non-needs-focus",
    720                "--total-chunks=%d" % jobsWithoutFocus,
    721                "--this-chunk=%d" % chunkNumber,
    722                "--marionette=%s:%d" % (host, port),
    723            ]
    724            port += 1
    725 
    726        for jobArgs in perProcessArgs:
    727            try:
    728                jobArgs.remove("--run-tests-in-parallel")
    729            except Exception:
    730                pass
    731            jobArgs[0:0] = [sys.executable, "-u"]
    732 
    733        threads = [ReftestThread(args) for args in perProcessArgs[1:]]
    734        for t in threads:
    735            t.start()
    736 
    737        while True:
    738            # The test harness in each individual thread will be doing timeout
    739            # handling on its own, so we shouldn't need to worry about any of
    740            # the threads hanging for arbitrarily long.
    741            for t in threads:
    742                t.join(10)
    743            if not any(t.is_alive() for t in threads):
    744                break
    745 
    746        # Run the needs-focus tests serially after the other ones, so we don't
    747        # have to worry about races between the needs-focus tests *actually*
    748        # needing focus and the dummy windows in the non-needs-focus tests
    749        # trying to focus themselves.
    750        focusThread = ReftestThread(perProcessArgs[0])
    751        focusThread.start()
    752        focusThread.join()
    753 
    754        # Output the summaries that the ReftestThread filters suppressed.
    755        summaryObjects = [defaultdict(int) for s in summaryLines]
    756        for t in threads:
    757            for summaryObj, (text, categories) in zip(summaryObjects, summaryLines):
    758                threadMatches = t.summaryMatches[text]
    759                for attribute, description in categories:
    760                    amount = int(threadMatches.group(attribute) if threadMatches else 0)
    761                    summaryObj[attribute] += amount
    762                amount = int(threadMatches.group("total") if threadMatches else 0)
    763                summaryObj["total"] += amount
    764 
    765        print("REFTEST INFO | Result summary:")
    766        for summaryObj, (text, categories) in zip(summaryObjects, summaryLines):
    767            details = ", ".join([
    768                "%d %s" % (summaryObj[attribute], description)
    769                for (attribute, description) in categories
    770            ])
    771            print(
    772                "REFTEST INFO | "
    773                + text
    774                + ": "
    775                + str(summaryObj["total"])
    776                + " ("
    777                + details
    778                + ")"
    779            )
    780 
    781        return int(any(t.retcode != 0 for t in threads))
    782 
    783    def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo):
    784        """handle process output timeout"""
    785        # TODO: bug 913975 : _processOutput should call self.processOutputLine
    786        # one more time one timeout (I think)
    787        self.log.error(
    788            "%s | application timed out after %d seconds with no output"
    789            % (self.lastTestSeen, int(timeout))
    790        )
    791        self.log.warning("Force-terminating active process(es).")
    792        self.killAndGetStack(
    793            proc, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
    794        )
    795 
    796    def dumpScreen(self, utilityPath):
    797        if self.haveDumpedScreen:
    798            self.log.info(
    799                "Not taking screenshot here: see the one that was previously logged"
    800            )
    801            return
    802        self.haveDumpedScreen = True
    803        dump_screen(utilityPath, self.log)
    804 
    805    def killAndGetStack(self, process, utilityPath, debuggerInfo, dump_screen=False):
    806        """
    807        Kill the process, preferrably in a way that gets us a stack trace.
    808        Also attempts to obtain a screenshot before killing the process
    809        if specified.
    810        """
    811 
    812        if dump_screen:
    813            self.dumpScreen(utilityPath)
    814 
    815        if mozinfo.info.get("crashreporter", True) and not debuggerInfo:
    816            if mozinfo.isWin:
    817                # We should have a "crashinject" program in our utility path
    818                crashinject = os.path.normpath(
    819                    os.path.join(utilityPath, "crashinject.exe")
    820                )
    821                if os.path.exists(crashinject):
    822                    status = subprocess.Popen([crashinject, str(process.pid)]).wait()
    823                    printstatus("crashinject", status)
    824                    if status == 0:
    825                        return
    826            else:
    827                try:
    828                    process.kill(sig=signal.SIGABRT)
    829                except OSError:
    830                    # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
    831                    self.log.info("Can't trigger Breakpad, process no longer exists")
    832                return
    833        self.log.info("Can't trigger Breakpad, just killing process")
    834        process.kill()
    835 
    836    def runApp(
    837        self,
    838        options,
    839        cmdargs=None,
    840        timeout=None,
    841        debuggerInfo=None,
    842        symbolsPath=None,
    843        valgrindPath=None,
    844        valgrindArgs=None,
    845        valgrindSuppFiles=None,
    846        **profileArgs,
    847    ):
    848        if cmdargs is None:
    849            cmdargs = []
    850        cmdargs = cmdargs[:]
    851 
    852        if self.use_marionette:
    853            cmdargs.append("-marionette")
    854 
    855        binary = options.app
    856        profile = self.createReftestProfile(options, **profileArgs)
    857 
    858        # browser environment
    859        env = self.buildBrowserEnv(options, profile.profile)
    860 
    861        def timeoutHandler():
    862            self.handleTimeout(timeout, proc, options.utilityPath, debuggerInfo)
    863 
    864        interactive = False
    865        debug_args = None
    866        if debuggerInfo:
    867            interactive = debuggerInfo.interactive
    868            debug_args = [debuggerInfo.path] + debuggerInfo.args
    869 
    870        def record_last_test(message):
    871            """Records the last test seen by this harness for the benefit of crash logging."""
    872 
    873            def testid(test):
    874                if " " in test:
    875                    return test.split(" ")[0]
    876                return test
    877 
    878            if message["action"] == "test_start":
    879                self.lastTestSeen = testid(message["test"])
    880            elif message["action"] == "test_end":
    881                if self.lastTest and message["test"] == self.lastTest:
    882                    self.lastTestSeen = self.currentManifest
    883                else:
    884                    self.lastTestSeen = "{} (finished)".format(testid(message["test"]))
    885 
    886        self.log.add_handler(record_last_test)
    887 
    888        kp_kwargs = {
    889            "kill_on_timeout": False,
    890            "cwd": SCRIPT_DIRECTORY,
    891            "onTimeout": [timeoutHandler],
    892            "processOutputLine": [self.outputHandler],
    893        }
    894 
    895        if mozinfo.isWin or mozinfo.isMac:
    896            # Prevents log interleaving on Windows at the expense of losing
    897            # true log order. See bug 798300 and bug 1324961 for more details.
    898            kp_kwargs["processStderrLine"] = [self.outputHandler]
    899 
    900        if interactive:
    901            # If an interactive debugger is attached,
    902            # don't use timeouts, and don't capture ctrl-c.
    903            timeout = None
    904            signal.signal(signal.SIGINT, lambda sigid, frame: None)
    905 
    906        runner_cls = mozrunner.runners.get(
    907            mozinfo.info.get("appname", "firefox"), mozrunner.Runner
    908        )
    909        runner = runner_cls(
    910            profile=profile,
    911            binary=binary,
    912            process_class=mozprocess.ProcessHandlerMixin,
    913            cmdargs=cmdargs,
    914            env=env,
    915            process_args=kp_kwargs,
    916        )
    917        runner.start(
    918            debug_args=debug_args, interactive=interactive, outputTimeout=timeout
    919        )
    920        proc = runner.process_handler
    921        self.outputHandler.proc_name = f"GECKO({proc.pid})"
    922 
    923        # Used to defer a possible IOError exception from Marionette
    924        marionette_exception = None
    925 
    926        if self.use_marionette:
    927            marionette_args = {
    928                "socket_timeout": options.marionette_socket_timeout,
    929                "startup_timeout": options.marionette_startup_timeout,
    930                "symbols_path": options.symbolsPath,
    931            }
    932            if options.marionette:
    933                host, port = options.marionette.split(":")
    934                marionette_args["host"] = host
    935                marionette_args["port"] = int(port)
    936 
    937            try:
    938                marionette = Marionette(**marionette_args)
    939                marionette.start_session()
    940 
    941                addons = Addons(marionette)
    942                if options.specialPowersExtensionPath:
    943                    addons.install(options.specialPowersExtensionPath, temp=True)
    944 
    945                addons.install(options.reftestExtensionPath, temp=True)
    946 
    947                marionette.delete_session()
    948            except OSError as e:
    949                # Any IOError as thrown by Marionette means that something is
    950                # wrong with the process, like a crash or the socket is no
    951                # longer open. We defer raising this specific error so that
    952                # post-test checks for leaks and crashes are performed and
    953                # reported first.
    954                marionette_exception = e
    955 
    956        status = runner.wait()
    957        runner.process_handler = None
    958        self.outputHandler.proc_name = None
    959 
    960        crashed = mozcrash.log_crashes(
    961            self.log,
    962            os.path.join(profile.profile, "minidumps"),
    963            options.symbolsPath,
    964            test=self.lastTestSeen,
    965        )
    966 
    967        if crashed:
    968            # log suite_end to wrap up, this is usually done with in in-browser harness
    969            if not self.outputHandler.results:
    970                # TODO: while .results is a defaultdict(int), it is proxied via log_actions as data, not type
    971                self.outputHandler.results = {
    972                    "Pass": 0,
    973                    "LoadOnly": 0,
    974                    "Exception": 0,
    975                    "FailedLoad": 0,
    976                    "UnexpectedFail": 1,
    977                    "UnexpectedPass": 0,
    978                    "AssertionUnexpected": 0,
    979                    "AssertionUnexpectedFixed": 0,
    980                    "KnownFail": 0,
    981                    "AssertionKnown": 0,
    982                    "Random": 0,
    983                    "Skip": 0,
    984                    "Slow": 0,
    985                }
    986            self.log.suite_end(extra={"results": self.outputHandler.results})
    987 
    988        if not status and crashed:
    989            status = 1
    990 
    991        if status and not crashed:
    992            msg = "application terminated with exit code %s" % (status)
    993            self.log.shutdown_failure(group=self.lastTestSeen, message=msg)
    994 
    995        runner.cleanup()
    996        self.cleanup(profile.profile)
    997 
    998        if marionette_exception is not None:
    999            raise marionette_exception
   1000 
   1001        self.log.info("Process mode: {}".format("e10s" if options.e10s else "non-e10s"))
   1002        return status
   1003 
   1004    def getActiveTests(self, manifests, options, testDumpFile=None):
   1005        # These prefs will cause reftest.sys.mjs to parse the manifests,
   1006        # dump the resulting tests to a file, and exit.
   1007        prefs = {
   1008            "reftest.manifests": json.dumps(manifests),
   1009            "reftest.manifests.dumpTests": testDumpFile or self.testDumpFile,
   1010        }
   1011        cmdargs = []
   1012        self.runApp(options, cmdargs=cmdargs, prefs=prefs)
   1013 
   1014        if not os.path.isfile(self.testDumpFile):
   1015            print("Error: parsing manifests failed!")
   1016            sys.exit(1)
   1017 
   1018        with open(self.testDumpFile) as fh:
   1019            tests = json.load(fh)
   1020 
   1021        if os.path.isfile(self.testDumpFile):
   1022            mozfile.remove(self.testDumpFile)
   1023 
   1024        for test in tests:
   1025            # Name and path are expected by manifestparser, but not used in reftest.
   1026            test["name"] = test["path"] = test["url1"]
   1027 
   1028        mp = TestManifest(strict=False)
   1029        mp.tests = tests
   1030 
   1031        filters = []
   1032        if options.totalChunks:
   1033            filters.append(
   1034                mpf.chunk_by_manifest(options.thisChunk, options.totalChunks)
   1035            )
   1036 
   1037        tests = mp.active_tests(exists=False, filters=filters)
   1038        return tests
   1039 
   1040    def runSerialTests(self, manifests, options, cmdargs=None):
   1041        debuggerInfo = None
   1042        if options.debugger:
   1043            debuggerInfo = mozdebug.get_debugger_info(
   1044                options.debugger, options.debuggerArgs, options.debuggerInteractive
   1045            )
   1046 
   1047        def run(**kwargs):
   1048            if kwargs.get("tests"):
   1049                self.lastTest = kwargs["tests"][-1]["identifier"]
   1050                if not isinstance(self.lastTest, str):
   1051                    self.lastTest = " ".join(self.lastTest)
   1052 
   1053            status = self.runApp(
   1054                options,
   1055                manifests=manifests,
   1056                cmdargs=cmdargs,
   1057                # We generally want the JS harness or marionette
   1058                # to handle timeouts if they can.
   1059                # The default JS harness timeout is currently
   1060                # 300 seconds (default options.timeout).
   1061                # The default Marionette socket timeout is
   1062                # currently 360 seconds.
   1063                # Give the JS harness extra time to deal with
   1064                # its own timeouts and try to usually exceed
   1065                # the 360 second marionette socket timeout.
   1066                # See bug 479518 and bug 1414063.
   1067                timeout=options.timeout + 70.0,
   1068                debuggerInfo=debuggerInfo,
   1069                symbolsPath=options.symbolsPath,
   1070                **kwargs,
   1071            )
   1072 
   1073            # do not process leak log when we crash/assert
   1074            if status == 0:
   1075                mozleak.process_leak_log(
   1076                    self.leakLogFile,
   1077                    leak_thresholds=options.leakThresholds,
   1078                    stack_fixer=get_stack_fixer_function(
   1079                        options.utilityPath, options.symbolsPath
   1080                    ),
   1081                )
   1082            return status
   1083 
   1084        if not self.run_by_manifest:
   1085            return run()
   1086 
   1087        tests = self.getActiveTests(manifests, options)
   1088        tests_by_manifest = defaultdict(list)
   1089        ids_by_manifest = defaultdict(list)
   1090        for t in tests:
   1091            tests_by_manifest[t["manifest"]].append(t)
   1092            test_id = t["identifier"]
   1093            if not isinstance(test_id, str):
   1094                test_id = " ".join(test_id)
   1095            ids_by_manifest[t["manifestID"]].append(test_id)
   1096 
   1097        self.log.suite_start(ids_by_manifest, name=options.suite)
   1098 
   1099        overall = 0
   1100        status = -1
   1101        for manifest, tests in tests_by_manifest.items():
   1102            if self.getGtkTheme() != self.gtkTheme:
   1103                self.log.error(
   1104                    "Theme (%s) has changed to (%s), terminating job as this is unstable"
   1105                    % (self.gtkTheme, self.getGtkTheme())
   1106                )
   1107                return 1
   1108 
   1109            self.log.info(f"Running tests in {manifest}")
   1110            self.currentManifest = manifest
   1111            status = run(tests=tests)
   1112            overall = overall or status
   1113        if status == -1:
   1114            # we didn't run anything
   1115            overall = 1
   1116 
   1117        self.log.suite_end(extra={"results": self.outputHandler.results})
   1118        return overall
   1119 
   1120    def copyExtraFilesToProfile(self, options, profile):
   1121        "Copy extra files or dirs specified on the command line to the testing profile."
   1122        profileDir = profile.profile
   1123        for f in options.extraProfileFiles:
   1124            abspath = self.getFullPath(f)
   1125            if os.path.isfile(abspath):
   1126                if os.path.basename(abspath) == "user.js":
   1127                    extra_prefs = mozprofile.Preferences.read_prefs(abspath)
   1128                    profile.set_preferences(extra_prefs)
   1129                elif os.path.basename(abspath).endswith(".dic"):
   1130                    hyphDir = os.path.join(profileDir, "hyphenation")
   1131                    if not os.path.exists(hyphDir):
   1132                        os.makedirs(hyphDir)
   1133                    shutil.copy2(abspath, hyphDir)
   1134                else:
   1135                    shutil.copy2(abspath, profileDir)
   1136            elif os.path.isdir(abspath):
   1137                dest = os.path.join(profileDir, os.path.basename(abspath))
   1138                shutil.copytree(abspath, dest)
   1139            else:
   1140                self.log.warning(
   1141                    "runreftest.py | Failed to copy %s to profile" % abspath
   1142                )
   1143                continue
   1144 
   1145 
   1146 def run_test_harness(parser, options):
   1147    reftest = RefTest(options.suite)
   1148    parser.validate(options, reftest)
   1149 
   1150    # We have to validate options.app here for the case when the mach
   1151    # command is able to find it after argument parsing. This can happen
   1152    # when running from a tests archive.
   1153    if not options.app:
   1154        parser.error("could not find the application path, --appname must be specified")
   1155 
   1156    options.app = reftest.getFullPath(options.app)
   1157    if not os.path.exists(options.app):
   1158        parser.error(
   1159            "Error: Path %(app)s doesn't exist. Are you executing "
   1160            "$objdir/_tests/reftest/runreftest.py?" % {"app": options.app}
   1161        )
   1162 
   1163    if options.xrePath is None:
   1164        options.xrePath = os.path.dirname(options.app)
   1165 
   1166    if options.verify:
   1167        result = reftest.verifyTests(options.tests, options)
   1168    else:
   1169        result = reftest.runTests(options.tests, options)
   1170 
   1171    return result
   1172 
   1173 
   1174 if __name__ == "__main__":
   1175    parser = reftestcommandline.DesktopArgumentsParser()
   1176    options = parser.parse_args()
   1177    sys.exit(run_test_harness(parser, options))