diff options
Diffstat (limited to 'layout/tools/reftest/runreftest.py')
-rw-r--r-- | layout/tools/reftest/runreftest.py | 1198 |
1 files changed, 1198 insertions, 0 deletions
diff --git a/layout/tools/reftest/runreftest.py b/layout/tools/reftest/runreftest.py new file mode 100644 index 0000000000..763acc105d --- /dev/null +++ b/layout/tools/reftest/runreftest.py @@ -0,0 +1,1198 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +Runs the reftest test harness. +""" +import json +import multiprocessing +import os +import platform +import posixpath +import re +import shutil +import signal +import subprocess +import sys +import tempfile +import threading +from collections import defaultdict +from datetime import datetime, timedelta + +SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) +if SCRIPT_DIRECTORY not in sys.path: + sys.path.insert(0, SCRIPT_DIRECTORY) + +import mozcrash +import mozdebug +import mozfile +import mozinfo +import mozleak +import mozlog +import mozprocess +import mozprofile +import mozrunner +from manifestparser import TestManifest +from manifestparser import filters as mpf +from mozrunner.utils import get_stack_fixer_function, test_environment +from mozscreenshot import dump_screen, printstatus +from six import reraise, string_types +from six.moves import range + +try: + from marionette_driver.addons import Addons + from marionette_driver.marionette import Marionette +except ImportError as e: # noqa + # Defer ImportError until attempt to use Marionette. + # Python 3 deletes the exception once the except block + # is exited. Save a version to raise later. + e_save = ImportError(str(e)) + + def reraise_(*args, **kwargs): + raise (e_save) # noqa + + Marionette = reraise_ + +import reftestcommandline +from output import OutputHandler, ReftestFormatter + +here = os.path.abspath(os.path.dirname(__file__)) + +try: + from mozbuild.base import MozbuildObject + + build_obj = MozbuildObject.from_environment(cwd=here) +except ImportError: + build_obj = None + + +def categoriesToRegex(categoryList): + return "\\(" + ", ".join(["(?P<%s>\\d+) %s" % c for c in categoryList]) + "\\)" + + +summaryLines = [ + ("Successful", [("pass", "pass"), ("loadOnly", "load only")]), + ( + "Unexpected", + [ + ("fail", "unexpected fail"), + ("pass", "unexpected pass"), + ("asserts", "unexpected asserts"), + ("fixedAsserts", "unexpected fixed asserts"), + ("failedLoad", "failed load"), + ("exception", "exception"), + ], + ), + ( + "Known problems", + [ + ("knownFail", "known fail"), + ("knownAsserts", "known asserts"), + ("random", "random"), + ("skipped", "skipped"), + ("slow", "slow"), + ], + ), +] + + +if sys.version_info[0] == 3: + + def reraise_(tp_, value_, tb_=None): + if value_ is None: + value_ = tp_() + if value_.__traceback__ is not tb_: + raise value_.with_traceback(tb_) + raise value_ + + +else: + exec("def reraise_(tp_, value_, tb_=None):\n raise tp_, value_, tb_\n") + + +def update_mozinfo(): + """walk up directories to find mozinfo.json update the info""" + # TODO: This should go in a more generic place, e.g. mozinfo + + path = SCRIPT_DIRECTORY + dirs = set() + while path != os.path.expanduser("~"): + if path in dirs: + break + dirs.add(path) + path = os.path.split(path)[0] + mozinfo.find_and_update_from_json(*dirs) + + +# Python's print is not threadsafe. +printLock = threading.Lock() + + +class ReftestThread(threading.Thread): + def __init__(self, cmdargs): + threading.Thread.__init__(self) + self.cmdargs = cmdargs + self.summaryMatches = {} + self.retcode = -1 + for text, _ in summaryLines: + self.summaryMatches[text] = None + + def run(self): + with printLock: + print("Starting thread with", self.cmdargs) + sys.stdout.flush() + process = subprocess.Popen(self.cmdargs, stdout=subprocess.PIPE) + for chunk in self.chunkForMergedOutput(process.stdout): + with printLock: + print(chunk, end=" ") + sys.stdout.flush() + self.retcode = process.wait() + + def chunkForMergedOutput(self, logsource): + """Gather lines together that should be printed as one atomic unit. + Individual test results--anything between 'REFTEST TEST-START' and + 'REFTEST TEST-END' lines--are an atomic unit. Lines with data from + summaries are parsed and the data stored for later aggregation. + Other lines are considered their own atomic units and are permitted + to intermix freely.""" + testStartRegex = re.compile("^REFTEST TEST-START") + testEndRegex = re.compile("^REFTEST TEST-END") + summaryHeadRegex = re.compile("^REFTEST INFO \\| Result summary:") + summaryRegexFormatString = ( + "^REFTEST INFO \\| (?P<message>{text}): (?P<total>\\d+) {regex}" + ) + summaryRegexStrings = [ + summaryRegexFormatString.format( + text=text, regex=categoriesToRegex(categories) + ) + for (text, categories) in summaryLines + ] + summaryRegexes = [re.compile(regex) for regex in summaryRegexStrings] + + for line in logsource: + if testStartRegex.search(line) is not None: + chunkedLines = [line] + for lineToBeChunked in logsource: + chunkedLines.append(lineToBeChunked) + if testEndRegex.search(lineToBeChunked) is not None: + break + yield "".join(chunkedLines) + continue + + haveSuppressedSummaryLine = False + for regex in summaryRegexes: + match = regex.search(line) + if match is not None: + self.summaryMatches[match.group("message")] = match + haveSuppressedSummaryLine = True + break + if haveSuppressedSummaryLine: + continue + + if summaryHeadRegex.search(line) is None: + yield line + + +class ReftestResolver(object): + def defaultManifest(self, suite): + return { + "reftest": "reftest.list", + "crashtest": "crashtests.list", + "jstestbrowser": "jstests.list", + }[suite] + + def directoryManifest(self, suite, path): + return os.path.join(path, self.defaultManifest(suite)) + + def findManifest(self, suite, test_file, subdirs=True): + """Return a tuple of (manifest-path, filter-string) for running test_file. + + test_file is a path to a test or a manifest file + """ + rv = [] + default_manifest = self.defaultManifest(suite) + relative_path = None + if not os.path.isabs(test_file): + relative_path = test_file + test_file = self.absManifestPath(test_file) + + if os.path.isdir(test_file): + for dirpath, dirnames, filenames in os.walk(test_file): + if default_manifest in filenames: + rv.append((os.path.join(dirpath, default_manifest), None)) + # We keep recursing into subdirectories which means that in the case + # of include directives we get the same manifest multiple times. + # However reftest.js will only read each manifest once + + if ( + len(rv) == 0 + and relative_path + and suite == "jstestbrowser" + and build_obj + ): + # The relative path can be from staging area. + staged_js_dir = os.path.join( + build_obj.topobjdir, "dist", "test-stage", "jsreftest" + ) + staged_file = os.path.join(staged_js_dir, "tests", relative_path) + return self.findManifest(suite, staged_file, subdirs) + elif test_file.endswith(".list"): + if os.path.exists(test_file): + rv = [(test_file, None)] + else: + dirname, pathname = os.path.split(test_file) + found = True + while not os.path.exists(os.path.join(dirname, default_manifest)): + dirname, suffix = os.path.split(dirname) + pathname = posixpath.join(suffix, pathname) + if os.path.dirname(dirname) == dirname: + found = False + break + if found: + rv = [ + ( + os.path.join(dirname, default_manifest), + r".*%s(?:[#?].*)?$" % pathname.replace("?", "\?"), + ) + ] + + return rv + + def absManifestPath(self, path): + return os.path.normpath(os.path.abspath(path)) + + def manifestURL(self, options, path): + return "file://%s" % path + + def resolveManifests(self, options, tests): + suite = options.suite + manifests = {} + for testPath in tests: + for manifest, filter_str in self.findManifest(suite, testPath): + if manifest not in manifests: + manifests[manifest] = set() + manifests[manifest].add(filter_str) + manifests_by_url = {} + for key in manifests.keys(): + id = os.path.relpath( + os.path.abspath(os.path.dirname(key)), options.topsrcdir + ) + id = id.replace(os.sep, posixpath.sep) + if None in manifests[key]: + manifests[key] = (None, id) + else: + manifests[key] = ("|".join(list(manifests[key])), id) + url = self.manifestURL(options, key) + manifests_by_url[url] = manifests[key] + return manifests_by_url + + +class RefTest(object): + oldcwd = os.getcwd() + resolver_cls = ReftestResolver + use_marionette = True + + def __init__(self, suite): + update_mozinfo() + self.lastTestSeen = None + self.lastTest = None + self.haveDumpedScreen = False + self.resolver = self.resolver_cls() + self.log = None + self.outputHandler = None + self.testDumpFile = os.path.join(tempfile.gettempdir(), "reftests.json") + self.currentManifest = "No test started" + + self.run_by_manifest = True + if suite in ("crashtest", "jstestbrowser"): + self.run_by_manifest = False + + def _populate_logger(self, options): + if self.log: + return + + self.log = getattr(options, "log", None) + if self.log: + return + + mozlog.commandline.log_formatters["tbpl"] = ( + ReftestFormatter, + "Reftest specific formatter for the" + "benefit of legacy log parsers and" + "tools such as the reftest analyzer", + ) + fmt_options = {} + if not options.log_tbpl_level and os.environ.get("MOZ_REFTEST_VERBOSE"): + options.log_tbpl_level = fmt_options["level"] = "debug" + self.log = mozlog.commandline.setup_logging( + "reftest harness", options, {"tbpl": sys.stdout}, fmt_options + ) + + def getFullPath(self, path): + "Get an absolute path relative to self.oldcwd." + return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path))) + + def createReftestProfile( + self, + options, + tests=None, + manifests=None, + server="localhost", + port=0, + profile_to_clone=None, + prefs=None, + ): + """Sets up a profile for reftest. + + :param options: Object containing command line options + :param tests: List of test objects to run + :param manifests: List of manifest files to parse (only takes effect + if tests were not passed in) + :param server: Server name to use for http tests + :param profile_to_clone: Path to a profile to use as the basis for the + test profile + :param prefs: Extra preferences to set in the profile + """ + locations = mozprofile.permissions.ServerLocations() + locations.add_host(server, scheme="http", port=port) + locations.add_host(server, scheme="https", port=port) + + sandbox_whitelist_paths = options.sandboxReadWhitelist + if platform.system() == "Linux" or platform.system() in ( + "Windows", + "Microsoft", + ): + # Trailing slashes are needed to indicate directories on Linux and Windows + sandbox_whitelist_paths = map( + lambda p: os.path.join(p, ""), sandbox_whitelist_paths + ) + + addons = [] + if not self.use_marionette: + addons.append(options.reftestExtensionPath) + + if options.specialPowersExtensionPath is not None: + if not self.use_marionette: + addons.append(options.specialPowersExtensionPath) + + # Install distributed extensions, if application has any. + distExtDir = os.path.join( + options.app[: options.app.rfind(os.sep)], "distribution", "extensions" + ) + if os.path.isdir(distExtDir): + for f in os.listdir(distExtDir): + addons.append(os.path.join(distExtDir, f)) + + # Install custom extensions. + for f in options.extensionsToInstall: + addons.append(self.getFullPath(f)) + + kwargs = { + "addons": addons, + "locations": locations, + "whitelistpaths": sandbox_whitelist_paths, + } + if profile_to_clone: + profile = mozprofile.Profile.clone(profile_to_clone, **kwargs) + else: + profile = mozprofile.Profile(**kwargs) + + # First set prefs from the base profiles under testing/profiles. + + # In test packages used in CI, the profile_data directory is installed + # in the SCRIPT_DIRECTORY. + profile_data_dir = os.path.join(SCRIPT_DIRECTORY, "profile_data") + # If possible, read profile data from topsrcdir. This prevents us from + # requiring a re-build to pick up newly added extensions in the + # <profile>/extensions directory. + if build_obj: + path = os.path.join(build_obj.topsrcdir, "testing", "profiles") + if os.path.isdir(path): + profile_data_dir = path + # Still not found? Look for testing/profiles relative to layout/tools/reftest. + if not os.path.isdir(profile_data_dir): + path = os.path.abspath( + os.path.join(SCRIPT_DIRECTORY, "..", "..", "..", "testing", "profiles") + ) + if os.path.isdir(path): + profile_data_dir = path + + with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh: + base_profiles = json.load(fh)["reftest"] + + for name in base_profiles: + path = os.path.join(profile_data_dir, name) + profile.merge(path) + + # Second set preferences for communication between our command line + # arguments and the reftest harness. Preferences that are required for + # reftest to work should instead be set under srcdir/testing/profiles. + prefs = prefs or {} + prefs["reftest.timeout"] = options.timeout * 1000 + if options.logFile: + prefs["reftest.logFile"] = options.logFile + if options.ignoreWindowSize: + prefs["reftest.ignoreWindowSize"] = True + if options.shuffle: + prefs["reftest.shuffle"] = True + if options.repeat: + prefs["reftest.repeat"] = options.repeat + if options.runUntilFailure: + prefs["reftest.runUntilFailure"] = True + if not options.repeat: + prefs["reftest.repeat"] = 30 + if options.verify: + prefs["reftest.verify"] = True + if options.cleanupCrashes: + prefs["reftest.cleanupPendingCrashes"] = True + prefs["reftest.focusFilterMode"] = options.focusFilterMode + prefs["reftest.logLevel"] = options.log_tbpl_level or "info" + prefs["reftest.suite"] = options.suite + prefs["gfx.font_rendering.ahem_antialias_none"] = True + # Run the "deferred" font-loader immediately, because if it finishes + # mid-test, the extra reflow that is triggered can disrupt the test. + prefs["gfx.font_loader.delay"] = 0 + # Ensure bundled fonts are activated, even if not enabled by default + # on the platform, so that tests can rely on them. + prefs["gfx.bundled-fonts.activate"] = 1 + # Disable dark scrollbars because it's semi-transparent. + prefs["widget.disable-dark-scrollbar"] = True + prefs["reftest.isCoverageBuild"] = mozinfo.info.get("ccov", False) + + # config specific flags + prefs["sandbox.apple_silicon"] = mozinfo.info.get("apple_silicon", False) + + # Set tests to run or manifests to parse. + if tests: + testlist = os.path.join(profile.profile, "reftests.json") + with open(testlist, "w") as fh: + json.dump(tests, fh) + prefs["reftest.tests"] = testlist + elif manifests: + prefs["reftest.manifests"] = json.dumps(manifests) + + # Unconditionally update the e10s pref, default True + prefs["browser.tabs.remote.autostart"] = True + if not options.e10s: + prefs["browser.tabs.remote.autostart"] = False + + # default fission to True + prefs["fission.autostart"] = True + if options.disableFission: + prefs["fission.autostart"] = False + + if not self.run_by_manifest: + if options.totalChunks: + prefs["reftest.totalChunks"] = options.totalChunks + if options.thisChunk: + prefs["reftest.thisChunk"] = options.thisChunk + + # Bug 1262954: For winXP + e10s disable acceleration + if ( + platform.system() in ("Windows", "Microsoft") + and "5.1" in platform.version() + and options.e10s + ): + prefs["layers.acceleration.disabled"] = True + + # Bug 1300355: Disable canvas cache for win7 as it uses + # too much memory and causes OOMs. + if ( + platform.system() in ("Windows", "Microsoft") + and "6.1" in platform.version() + ): + prefs["reftest.nocache"] = True + + if options.marionette: + # options.marionette can specify host:port + port = options.marionette.split(":")[1] + prefs["marionette.port"] = int(port) + + # Enable tracing output for detailed failures in case of + # failing connection attempts, and hangs (bug 1397201) + prefs["remote.log.level"] = "Trace" + + # Third, set preferences passed in via the command line. + for v in options.extraPrefs: + thispref = v.split("=") + if len(thispref) < 2: + print("Error: syntax error in --setpref=" + v) + sys.exit(1) + prefs[thispref[0]] = thispref[1].strip() + + for pref in prefs: + prefs[pref] = mozprofile.Preferences.cast(prefs[pref]) + profile.set_preferences(prefs) + + if os.path.join(here, "chrome") not in options.extraProfileFiles: + options.extraProfileFiles.append(os.path.join(here, "chrome")) + + self.copyExtraFilesToProfile(options, profile) + + self.log.info( + "Running with e10s: {}".format(prefs["browser.tabs.remote.autostart"]) + ) + self.log.info("Running with fission: {}".format(prefs["fission.autostart"])) + + return profile + + def environment(self, **kwargs): + kwargs["log"] = self.log + return test_environment(**kwargs) + + def buildBrowserEnv(self, options, profileDir): + browserEnv = self.environment( + xrePath=options.xrePath, debugger=options.debugger + ) + browserEnv["XPCOM_DEBUG_BREAK"] = "stack" + if options.topsrcdir: + browserEnv["MOZ_DEVELOPER_REPO_DIR"] = options.topsrcdir + if hasattr(options, "topobjdir"): + browserEnv["MOZ_DEVELOPER_OBJ_DIR"] = options.topobjdir + + if mozinfo.info["asan"]: + # Disable leak checking for reftests for now + if "ASAN_OPTIONS" in browserEnv: + browserEnv["ASAN_OPTIONS"] += ":detect_leaks=0" + else: + browserEnv["ASAN_OPTIONS"] = "detect_leaks=0" + + # Set environment defaults for jstestbrowser. Keep in sync with the + # defaults used in js/src/tests/lib/tests.py. + if options.suite == "jstestbrowser": + browserEnv["TZ"] = "PST8PDT" + browserEnv["LC_ALL"] = "en_US.UTF-8" + + for v in options.environment: + ix = v.find("=") + if ix <= 0: + print("Error: syntax error in --setenv=" + v) + return None + browserEnv[v[:ix]] = v[ix + 1 :] + + # Enable leaks detection to its own log file. + self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log") + browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile + + # TODO: this is always defined (as part of --enable-webrender which is default) + # can we make this default in the browser? + browserEnv["MOZ_ACCELERATED"] = "1" + + if options.headless: + browserEnv["MOZ_HEADLESS"] = "1" + + return browserEnv + + def cleanup(self, profileDir): + if profileDir: + shutil.rmtree(profileDir, True) + + def verifyTests(self, tests, options): + """ + Support --verify mode: Run test(s) many times in a variety of + configurations/environments in an effort to find intermittent + failures. + """ + + self._populate_logger(options) + + # Number of times to repeat test(s) when running with --repeat + VERIFY_REPEAT = 10 + # Number of times to repeat test(s) when running test in separate browser + VERIFY_REPEAT_SINGLE_BROWSER = 5 + + def step1(): + options.repeat = VERIFY_REPEAT + options.runUntilFailure = True + result = self.runTests(tests, options) + return result + + def step2(): + options.repeat = 0 + options.runUntilFailure = False + for i in range(VERIFY_REPEAT_SINGLE_BROWSER): + result = self.runTests(tests, options) + if result != 0: + break + return result + + def step3(): + options.repeat = VERIFY_REPEAT + options.runUntilFailure = True + options.environment.append("MOZ_CHAOSMODE=0xfb") + result = self.runTests(tests, options) + options.environment.remove("MOZ_CHAOSMODE=0xfb") + return result + + def step4(): + options.repeat = 0 + options.runUntilFailure = False + options.environment.append("MOZ_CHAOSMODE=0xfb") + for i in range(VERIFY_REPEAT_SINGLE_BROWSER): + result = self.runTests(tests, options) + if result != 0: + break + options.environment.remove("MOZ_CHAOSMODE=0xfb") + return result + + steps = [ + ("1. Run each test %d times in one browser." % VERIFY_REPEAT, step1), + ( + "2. Run each test %d times in a new browser each time." + % VERIFY_REPEAT_SINGLE_BROWSER, + step2, + ), + ( + "3. Run each test %d times in one browser, in chaos mode." + % VERIFY_REPEAT, + step3, + ), + ( + "4. Run each test %d times in a new browser each time, in chaos mode." + % VERIFY_REPEAT_SINGLE_BROWSER, + step4, + ), + ] + + stepResults = {} + for (descr, step) in steps: + stepResults[descr] = "not run / incomplete" + + startTime = datetime.now() + maxTime = timedelta(seconds=options.verify_max_time) + finalResult = "PASSED" + for (descr, step) in steps: + if (datetime.now() - startTime) > maxTime: + self.log.info("::: Test verification is taking too long: Giving up!") + self.log.info( + "::: So far, all checks passed, but not all checks were run." + ) + break + self.log.info(":::") + self.log.info('::: Running test verification step "%s"...' % descr) + self.log.info(":::") + result = step() + if result != 0: + stepResults[descr] = "FAIL" + finalResult = "FAILED!" + break + stepResults[descr] = "Pass" + + self.log.info(":::") + self.log.info("::: Test verification summary for:") + self.log.info(":::") + for test in tests: + self.log.info("::: " + test) + self.log.info(":::") + for descr in sorted(stepResults.keys()): + self.log.info("::: %s : %s" % (descr, stepResults[descr])) + self.log.info(":::") + self.log.info("::: Test verification %s" % finalResult) + self.log.info(":::") + + return result + + def runTests(self, tests, options, cmdargs=None): + cmdargs = cmdargs or [] + self._populate_logger(options) + self.outputHandler = OutputHandler( + self.log, options.utilityPath, options.symbolsPath + ) + + if options.cleanupCrashes: + mozcrash.cleanup_pending_crash_reports() + + manifests = self.resolver.resolveManifests(options, tests) + if options.filter: + manifests[""] = (options.filter, None) + + if not getattr(options, "runTestsInParallel", False): + return self.runSerialTests(manifests, options, cmdargs) + + cpuCount = multiprocessing.cpu_count() + + # We have the directive, technology, and machine to run multiple test instances. + # Experimentation says that reftests are not overly CPU-intensive, so we can run + # multiple jobs per CPU core. + # + # Our Windows machines in automation seem to get upset when we run a lot of + # simultaneous tests on them, so tone things down there. + if sys.platform == "win32": + jobsWithoutFocus = cpuCount + else: + jobsWithoutFocus = 2 * cpuCount + + totalJobs = jobsWithoutFocus + 1 + perProcessArgs = [sys.argv[:] for i in range(0, totalJobs)] + + host = "localhost" + port = 2828 + if options.marionette: + host, port = options.marionette.split(":") + + # First job is only needs-focus tests. Remaining jobs are + # non-needs-focus and chunked. + perProcessArgs[0].insert(-1, "--focus-filter-mode=needs-focus") + for (chunkNumber, jobArgs) in enumerate(perProcessArgs[1:], start=1): + jobArgs[-1:-1] = [ + "--focus-filter-mode=non-needs-focus", + "--total-chunks=%d" % jobsWithoutFocus, + "--this-chunk=%d" % chunkNumber, + "--marionette=%s:%d" % (host, port), + ] + port += 1 + + for jobArgs in perProcessArgs: + try: + jobArgs.remove("--run-tests-in-parallel") + except Exception: + pass + jobArgs[0:0] = [sys.executable, "-u"] + + threads = [ReftestThread(args) for args in perProcessArgs[1:]] + for t in threads: + t.start() + + while True: + # The test harness in each individual thread will be doing timeout + # handling on its own, so we shouldn't need to worry about any of + # the threads hanging for arbitrarily long. + for t in threads: + t.join(10) + if not any(t.is_alive() for t in threads): + break + + # Run the needs-focus tests serially after the other ones, so we don't + # have to worry about races between the needs-focus tests *actually* + # needing focus and the dummy windows in the non-needs-focus tests + # trying to focus themselves. + focusThread = ReftestThread(perProcessArgs[0]) + focusThread.start() + focusThread.join() + + # Output the summaries that the ReftestThread filters suppressed. + summaryObjects = [defaultdict(int) for s in summaryLines] + for t in threads: + for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines): + threadMatches = t.summaryMatches[text] + for (attribute, description) in categories: + amount = int(threadMatches.group(attribute) if threadMatches else 0) + summaryObj[attribute] += amount + amount = int(threadMatches.group("total") if threadMatches else 0) + summaryObj["total"] += amount + + print("REFTEST INFO | Result summary:") + for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines): + details = ", ".join( + [ + "%d %s" % (summaryObj[attribute], description) + for (attribute, description) in categories + ] + ) + print( + "REFTEST INFO | " + + text + + ": " + + str(summaryObj["total"]) + + " (" + + details + + ")" + ) + + return int(any(t.retcode != 0 for t in threads)) + + def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo): + """handle process output timeout""" + # TODO: bug 913975 : _processOutput should call self.processOutputLine + # one more time one timeout (I think) + self.log.error( + "%s | application timed out after %d seconds with no output" + % (self.lastTestSeen, int(timeout)) + ) + self.log.warning("Force-terminating active process(es).") + self.killAndGetStack( + proc, utilityPath, debuggerInfo, dump_screen=not debuggerInfo + ) + + def dumpScreen(self, utilityPath): + if self.haveDumpedScreen: + self.log.info( + "Not taking screenshot here: see the one that was previously logged" + ) + return + self.haveDumpedScreen = True + dump_screen(utilityPath, self.log) + + def killAndGetStack(self, process, utilityPath, debuggerInfo, dump_screen=False): + """ + Kill the process, preferrably in a way that gets us a stack trace. + Also attempts to obtain a screenshot before killing the process + if specified. + """ + + if dump_screen: + self.dumpScreen(utilityPath) + + if mozinfo.info.get("crashreporter", True) and not debuggerInfo: + if mozinfo.isWin: + # We should have a "crashinject" program in our utility path + crashinject = os.path.normpath( + os.path.join(utilityPath, "crashinject.exe") + ) + if os.path.exists(crashinject): + status = subprocess.Popen([crashinject, str(process.pid)]).wait() + printstatus("crashinject", status) + if status == 0: + return + else: + try: + process.kill(sig=signal.SIGABRT) + except OSError: + # https://bugzilla.mozilla.org/show_bug.cgi?id=921509 + self.log.info("Can't trigger Breakpad, process no longer exists") + return + self.log.info("Can't trigger Breakpad, just killing process") + process.kill() + + def runApp( + self, + options, + cmdargs=None, + timeout=None, + debuggerInfo=None, + symbolsPath=None, + valgrindPath=None, + valgrindArgs=None, + valgrindSuppFiles=None, + **profileArgs + ): + + if cmdargs is None: + cmdargs = [] + cmdargs = cmdargs[:] + + if self.use_marionette: + cmdargs.append("-marionette") + + binary = options.app + profile = self.createReftestProfile(options, **profileArgs) + + # browser environment + env = self.buildBrowserEnv(options, profile.profile) + + def timeoutHandler(): + self.handleTimeout(timeout, proc, options.utilityPath, debuggerInfo) + + interactive = False + debug_args = None + if debuggerInfo: + interactive = debuggerInfo.interactive + debug_args = [debuggerInfo.path] + debuggerInfo.args + + def record_last_test(message): + """Records the last test seen by this harness for the benefit of crash logging.""" + + def testid(test): + if " " in test: + return test.split(" ")[0] + return test + + if message["action"] == "test_start": + self.lastTestSeen = testid(message["test"]) + elif message["action"] == "test_end": + if self.lastTest and message["test"] == self.lastTest: + self.lastTestSeen = self.currentManifest + else: + self.lastTestSeen = "{} (finished)".format(testid(message["test"])) + + self.log.add_handler(record_last_test) + + kp_kwargs = { + "kill_on_timeout": False, + "cwd": SCRIPT_DIRECTORY, + "onTimeout": [timeoutHandler], + "processOutputLine": [self.outputHandler], + } + + if mozinfo.isWin or mozinfo.isMac: + # Prevents log interleaving on Windows at the expense of losing + # true log order. See bug 798300 and bug 1324961 for more details. + kp_kwargs["processStderrLine"] = [self.outputHandler] + + if interactive: + # If an interactive debugger is attached, + # don't use timeouts, and don't capture ctrl-c. + timeout = None + signal.signal(signal.SIGINT, lambda sigid, frame: None) + + runner_cls = mozrunner.runners.get( + mozinfo.info.get("appname", "firefox"), mozrunner.Runner + ) + runner = runner_cls( + profile=profile, + binary=binary, + process_class=mozprocess.ProcessHandlerMixin, + cmdargs=cmdargs, + env=env, + process_args=kp_kwargs, + ) + runner.start( + debug_args=debug_args, interactive=interactive, outputTimeout=timeout + ) + proc = runner.process_handler + self.outputHandler.proc_name = "GECKO({})".format(proc.pid) + + # Used to defer a possible IOError exception from Marionette + marionette_exception = None + + if self.use_marionette: + marionette_args = { + "socket_timeout": options.marionette_socket_timeout, + "startup_timeout": options.marionette_startup_timeout, + "symbols_path": options.symbolsPath, + } + if options.marionette: + host, port = options.marionette.split(":") + marionette_args["host"] = host + marionette_args["port"] = int(port) + + try: + marionette = Marionette(**marionette_args) + marionette.start_session() + + addons = Addons(marionette) + if options.specialPowersExtensionPath: + addons.install(options.specialPowersExtensionPath, temp=True) + + addons.install(options.reftestExtensionPath, temp=True) + + marionette.delete_session() + except IOError: + # Any IOError as thrown by Marionette means that something is + # wrong with the process, like a crash or the socket is no + # longer open. We defer raising this specific error so that + # post-test checks for leaks and crashes are performed and + # reported first. + marionette_exception = sys.exc_info() + + status = runner.wait() + runner.process_handler = None + self.outputHandler.proc_name = None + + crashed = mozcrash.log_crashes( + self.log, + os.path.join(profile.profile, "minidumps"), + options.symbolsPath, + test=self.lastTestSeen, + ) + + if crashed: + # log suite_end to wrap up, this is usually done with in in-browser harness + if not self.outputHandler.results: + # TODO: while .results is a defaultdict(int), it is proxied via log_actions as data, not type + self.outputHandler.results = { + "Pass": 0, + "LoadOnly": 0, + "Exception": 0, + "FailedLoad": 0, + "UnexpectedFail": 1, + "UnexpectedPass": 0, + "AssertionUnexpected": 0, + "AssertionUnexpectedFixed": 0, + "KnownFail": 0, + "AssertionKnown": 0, + "Random": 0, + "Skip": 0, + "Slow": 0, + } + self.log.suite_end(extra={"results": self.outputHandler.results}) + + if not status and crashed: + status = 1 + + if status and not crashed: + msg = ( + "TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s" + % (self.lastTestSeen, status) + ) + # use process_output so message is logged verbatim + self.log.process_output(None, msg) + + runner.cleanup() + self.cleanup(profile.profile) + + if marionette_exception is not None: + exc, value, tb = marionette_exception + raise reraise(exc, value, tb) + + self.log.info("Process mode: {}".format("e10s" if options.e10s else "non-e10s")) + return status + + def getActiveTests(self, manifests, options, testDumpFile=None): + # These prefs will cause reftest.jsm to parse the manifests, + # dump the resulting tests to a file, and exit. + prefs = { + "reftest.manifests": json.dumps(manifests), + "reftest.manifests.dumpTests": testDumpFile or self.testDumpFile, + } + cmdargs = [] + self.runApp(options, cmdargs=cmdargs, prefs=prefs) + + if not os.path.isfile(self.testDumpFile): + print("Error: parsing manifests failed!") + sys.exit(1) + + with open(self.testDumpFile, "r") as fh: + tests = json.load(fh) + + if os.path.isfile(self.testDumpFile): + mozfile.remove(self.testDumpFile) + + for test in tests: + # Name and path are expected by manifestparser, but not used in reftest. + test["name"] = test["path"] = test["url1"] + + mp = TestManifest(strict=False) + mp.tests = tests + + filters = [] + if options.totalChunks: + filters.append( + mpf.chunk_by_manifest(options.thisChunk, options.totalChunks) + ) + + tests = mp.active_tests(exists=False, filters=filters) + return tests + + def runSerialTests(self, manifests, options, cmdargs=None): + debuggerInfo = None + if options.debugger: + debuggerInfo = mozdebug.get_debugger_info( + options.debugger, options.debuggerArgs, options.debuggerInteractive + ) + + def run(**kwargs): + if kwargs.get("tests"): + self.lastTest = kwargs["tests"][-1]["identifier"] + if not isinstance(self.lastTest, string_types): + self.lastTest = " ".join(self.lastTest) + + status = self.runApp( + options, + manifests=manifests, + cmdargs=cmdargs, + # We generally want the JS harness or marionette + # to handle timeouts if they can. + # The default JS harness timeout is currently + # 300 seconds (default options.timeout). + # The default Marionette socket timeout is + # currently 360 seconds. + # Give the JS harness extra time to deal with + # its own timeouts and try to usually exceed + # the 360 second marionette socket timeout. + # See bug 479518 and bug 1414063. + timeout=options.timeout + 70.0, + debuggerInfo=debuggerInfo, + symbolsPath=options.symbolsPath, + **kwargs + ) + + # do not process leak log when we crash/assert + if status == 0: + mozleak.process_leak_log( + self.leakLogFile, + leak_thresholds=options.leakThresholds, + stack_fixer=get_stack_fixer_function( + options.utilityPath, options.symbolsPath + ), + ) + return status + + if not self.run_by_manifest: + return run() + + tests = self.getActiveTests(manifests, options) + tests_by_manifest = defaultdict(list) + ids_by_manifest = defaultdict(list) + for t in tests: + tests_by_manifest[t["manifest"]].append(t) + test_id = t["identifier"] + if not isinstance(test_id, string_types): + test_id = " ".join(test_id) + ids_by_manifest[t["manifestID"]].append(test_id) + + self.log.suite_start(ids_by_manifest, name=options.suite) + + overall = 0 + status = -1 + for manifest, tests in tests_by_manifest.items(): + self.log.info("Running tests in {}".format(manifest)) + self.currentManifest = manifest + status = run(tests=tests) + overall = overall or status + if status == -1: + # we didn't run anything + overall = 1 + + self.log.suite_end(extra={"results": self.outputHandler.results}) + return overall + + def copyExtraFilesToProfile(self, options, profile): + "Copy extra files or dirs specified on the command line to the testing profile." + profileDir = profile.profile + for f in options.extraProfileFiles: + abspath = self.getFullPath(f) + if os.path.isfile(abspath): + if os.path.basename(abspath) == "user.js": + extra_prefs = mozprofile.Preferences.read_prefs(abspath) + profile.set_preferences(extra_prefs) + elif os.path.basename(abspath).endswith(".dic"): + hyphDir = os.path.join(profileDir, "hyphenation") + if not os.path.exists(hyphDir): + os.makedirs(hyphDir) + shutil.copy2(abspath, hyphDir) + else: + shutil.copy2(abspath, profileDir) + elif os.path.isdir(abspath): + dest = os.path.join(profileDir, os.path.basename(abspath)) + shutil.copytree(abspath, dest) + else: + self.log.warning( + "runreftest.py | Failed to copy %s to profile" % abspath + ) + continue + + +def run_test_harness(parser, options): + reftest = RefTest(options.suite) + parser.validate(options, reftest) + + # We have to validate options.app here for the case when the mach + # command is able to find it after argument parsing. This can happen + # when running from a tests archive. + if not options.app: + parser.error("could not find the application path, --appname must be specified") + + options.app = reftest.getFullPath(options.app) + if not os.path.exists(options.app): + parser.error( + "Error: Path %(app)s doesn't exist. Are you executing " + "$objdir/_tests/reftest/runreftest.py?" % {"app": options.app} + ) + + if options.xrePath is None: + options.xrePath = os.path.dirname(options.app) + + if options.verify: + result = reftest.verifyTests(options.tests, options) + else: + result = reftest.runTests(options.tests, options) + + return result + + +if __name__ == "__main__": + parser = reftestcommandline.DesktopArgumentsParser() + options = parser.parse_args() + sys.exit(run_test_harness(parser, options)) |