michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: """ michael@0: Runs the reftest test harness. michael@0: """ michael@0: michael@0: from optparse import OptionParser michael@0: import collections michael@0: import json michael@0: import multiprocessing michael@0: import os michael@0: import re michael@0: import shutil michael@0: import subprocess michael@0: import sys michael@0: import threading michael@0: michael@0: SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) michael@0: sys.path.insert(0, SCRIPT_DIRECTORY) michael@0: michael@0: from automation import Automation michael@0: from automationutils import ( michael@0: addCommonOptions, michael@0: getDebuggerInfo, michael@0: isURL, michael@0: processLeakLog michael@0: ) michael@0: import mozprofile michael@0: michael@0: def categoriesToRegex(categoryList): michael@0: return "\\(" + ', '.join(["(?P<%s>\\d+) %s" % c for c in categoryList]) + "\\)" michael@0: summaryLines = [('Successful', [('pass', 'pass'), ('loadOnly', 'load only')]), michael@0: ('Unexpected', [('fail', 'unexpected fail'), michael@0: ('pass', 'unexpected pass'), michael@0: ('asserts', 'unexpected asserts'), michael@0: ('fixedAsserts', 'unexpected fixed asserts'), michael@0: ('failedLoad', 'failed load'), michael@0: ('exception', 'exception')]), michael@0: ('Known problems', [('knownFail', 'known fail'), michael@0: ('knownAsserts', 'known asserts'), michael@0: ('random', 'random'), michael@0: ('skipped', 'skipped'), michael@0: ('slow', 'slow')])] michael@0: michael@0: # Python's print is not threadsafe. michael@0: printLock = threading.Lock() michael@0: michael@0: class ReftestThread(threading.Thread): michael@0: def __init__(self, cmdlineArgs): michael@0: threading.Thread.__init__(self) michael@0: self.cmdlineArgs = cmdlineArgs michael@0: self.summaryMatches = {} michael@0: self.retcode = -1 michael@0: for text, _ in summaryLines: michael@0: self.summaryMatches[text] = None michael@0: michael@0: def run(self): michael@0: with printLock: michael@0: print "Starting thread with", self.cmdlineArgs michael@0: sys.stdout.flush() michael@0: process = subprocess.Popen(self.cmdlineArgs, stdout=subprocess.PIPE) michael@0: for chunk in self.chunkForMergedOutput(process.stdout): michael@0: with printLock: michael@0: print chunk, michael@0: sys.stdout.flush() michael@0: self.retcode = process.wait() michael@0: michael@0: def chunkForMergedOutput(self, logsource): michael@0: """Gather lines together that should be printed as one atomic unit. michael@0: Individual test results--anything between 'REFTEST TEST-START' and michael@0: 'REFTEST TEST-END' lines--are an atomic unit. Lines with data from michael@0: summaries are parsed and the data stored for later aggregation. michael@0: Other lines are considered their own atomic units and are permitted michael@0: to intermix freely.""" michael@0: testStartRegex = re.compile("^REFTEST TEST-START") michael@0: testEndRegex = re.compile("^REFTEST TEST-END") michael@0: summaryHeadRegex = re.compile("^REFTEST INFO \\| Result summary:") michael@0: summaryRegexFormatString = "^REFTEST INFO \\| (?P{text}): (?P\\d+) {regex}" michael@0: summaryRegexStrings = [summaryRegexFormatString.format(text=text, michael@0: regex=categoriesToRegex(categories)) michael@0: for (text, categories) in summaryLines] michael@0: summaryRegexes = [re.compile(regex) for regex in summaryRegexStrings] michael@0: michael@0: for line in logsource: michael@0: if testStartRegex.search(line) is not None: michael@0: chunkedLines = [line] michael@0: for lineToBeChunked in logsource: michael@0: chunkedLines.append(lineToBeChunked) michael@0: if testEndRegex.search(lineToBeChunked) is not None: michael@0: break michael@0: yield ''.join(chunkedLines) michael@0: continue michael@0: michael@0: haveSuppressedSummaryLine = False michael@0: for regex in summaryRegexes: michael@0: match = regex.search(line) michael@0: if match is not None: michael@0: self.summaryMatches[match.group('message')] = match michael@0: haveSuppressedSummaryLine = True michael@0: break michael@0: if haveSuppressedSummaryLine: michael@0: continue michael@0: michael@0: if summaryHeadRegex.search(line) is None: michael@0: yield line michael@0: michael@0: class RefTest(object): michael@0: michael@0: oldcwd = os.getcwd() michael@0: michael@0: def __init__(self, automation=None): michael@0: self.automation = automation or Automation() michael@0: michael@0: def getFullPath(self, path): michael@0: "Get an absolute path relative to self.oldcwd." michael@0: return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path))) michael@0: michael@0: def getManifestPath(self, path): michael@0: "Get the path of the manifest, and for remote testing this function is subclassed to point to remote manifest" michael@0: path = self.getFullPath(path) michael@0: if os.path.isdir(path): michael@0: defaultManifestPath = os.path.join(path, 'reftest.list') michael@0: if os.path.exists(defaultManifestPath): michael@0: path = defaultManifestPath michael@0: else: michael@0: defaultManifestPath = os.path.join(path, 'crashtests.list') michael@0: if os.path.exists(defaultManifestPath): michael@0: path = defaultManifestPath michael@0: return path michael@0: michael@0: def makeJSString(self, s): michael@0: return '"%s"' % re.sub(r'([\\"])', r'\\\1', s) michael@0: michael@0: def createReftestProfile(self, options, manifest, server='localhost', michael@0: special_powers=True, profile_to_clone=None): michael@0: """ michael@0: Sets up a profile for reftest. michael@0: 'manifest' is the path to the reftest.list file we want to test with. This is used in michael@0: the remote subclass in remotereftest.py so we can write it to a preference for the michael@0: bootstrap extension. michael@0: """ michael@0: michael@0: locations = mozprofile.permissions.ServerLocations() michael@0: locations.add_host(server, port=0) michael@0: locations.add_host('', port=0) michael@0: michael@0: # Set preferences for communication between our command line arguments michael@0: # and the reftest harness. Preferences that are required for reftest michael@0: # to work should instead be set in reftest-cmdline.js . michael@0: prefs = {} michael@0: prefs['reftest.timeout'] = options.timeout * 1000 michael@0: if options.totalChunks: michael@0: prefs['reftest.totalChunks'] = options.totalChunks michael@0: if options.thisChunk: michael@0: prefs['reftest.thisChunk'] = options.thisChunk michael@0: if options.logFile: michael@0: prefs['reftest.logFile'] = options.logFile michael@0: if options.ignoreWindowSize: michael@0: prefs['reftest.ignoreWindowSize'] = True michael@0: if options.filter: michael@0: prefs['reftest.filter'] = options.filter michael@0: if options.shuffle: michael@0: prefs['reftest.shuffle'] = True michael@0: prefs['reftest.focusFilterMode'] = options.focusFilterMode michael@0: michael@0: # Ensure that telemetry is disabled, so we don't connect to the telemetry michael@0: # server in the middle of the tests. michael@0: prefs['toolkit.telemetry.enabled'] = False michael@0: # Likewise for safebrowsing. michael@0: prefs['browser.safebrowsing.enabled'] = False michael@0: prefs['browser.safebrowsing.malware.enabled'] = False michael@0: # And for snippets. michael@0: prefs['browser.snippets.enabled'] = False michael@0: prefs['browser.snippets.syncPromo.enabled'] = False michael@0: # And for useragent updates. michael@0: prefs['general.useragent.updates.enabled'] = False michael@0: # And for webapp updates. Yes, it is supposed to be an integer. michael@0: prefs['browser.webapps.checkForUpdates'] = 0 michael@0: michael@0: if options.e10s: michael@0: prefs['browser.tabs.remote.autostart'] = True michael@0: michael@0: for v in options.extraPrefs: michael@0: thispref = v.split('=') michael@0: if len(thispref) < 2: michael@0: print "Error: syntax error in --setpref=" + v michael@0: sys.exit(1) michael@0: prefs[thispref[0]] = mozprofile.Preferences.cast(thispref[1].strip()) michael@0: michael@0: # install the reftest extension bits into the profile michael@0: addons = [] michael@0: addons.append(os.path.join(SCRIPT_DIRECTORY, "reftest")) michael@0: michael@0: # I would prefer to use "--install-extension reftest/specialpowers", but that requires tight coordination with michael@0: # release engineering and landing on multiple branches at once. michael@0: if special_powers and (manifest.endswith('crashtests.list') or manifest.endswith('jstests.list')): michael@0: addons.append(os.path.join(SCRIPT_DIRECTORY, 'specialpowers')) michael@0: michael@0: # Install distributed extensions, if application has any. michael@0: distExtDir = os.path.join(options.app[ : options.app.rfind(os.sep)], "distribution", "extensions") michael@0: if os.path.isdir(distExtDir): michael@0: for f in os.listdir(distExtDir): michael@0: addons.append(os.path.join(distExtDir, f)) michael@0: michael@0: # Install custom extensions. michael@0: for f in options.extensionsToInstall: michael@0: addons.append(self.getFullPath(f)) michael@0: michael@0: kwargs = { 'addons': addons, michael@0: 'preferences': prefs, michael@0: 'locations': locations } michael@0: if profile_to_clone: michael@0: profile = mozprofile.Profile.clone(profile_to_clone, **kwargs) michael@0: else: michael@0: profile = mozprofile.Profile(**kwargs) michael@0: michael@0: self.copyExtraFilesToProfile(options, profile) michael@0: return profile michael@0: michael@0: def buildBrowserEnv(self, options, profileDir): michael@0: browserEnv = self.automation.environment(xrePath = options.xrePath, debugger=options.debugger) michael@0: browserEnv["XPCOM_DEBUG_BREAK"] = "stack" michael@0: michael@0: for v in options.environment: michael@0: ix = v.find("=") michael@0: if ix <= 0: michael@0: print "Error: syntax error in --setenv=" + v michael@0: return None michael@0: browserEnv[v[:ix]] = v[ix + 1:] michael@0: michael@0: # Enable leaks detection to its own log file. michael@0: self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log") michael@0: browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile michael@0: return browserEnv michael@0: michael@0: def cleanup(self, profileDir): michael@0: if profileDir: michael@0: shutil.rmtree(profileDir, True) michael@0: michael@0: def runTests(self, testPath, options, cmdlineArgs = None): michael@0: if not options.runTestsInParallel: michael@0: return self.runSerialTests(testPath, options, cmdlineArgs) michael@0: michael@0: cpuCount = multiprocessing.cpu_count() michael@0: michael@0: # We have the directive, technology, and machine to run multiple test instances. michael@0: # Experimentation says that reftests are not overly CPU-intensive, so we can run michael@0: # multiple jobs per CPU core. michael@0: # michael@0: # Our Windows machines in automation seem to get upset when we run a lot of michael@0: # simultaneous tests on them, so tone things down there. michael@0: if sys.platform == 'win32': michael@0: jobsWithoutFocus = cpuCount michael@0: else: michael@0: jobsWithoutFocus = 2 * cpuCount michael@0: michael@0: totalJobs = jobsWithoutFocus + 1 michael@0: perProcessArgs = [sys.argv[:] for i in range(0, totalJobs)] michael@0: michael@0: # First job is only needs-focus tests. Remaining jobs are non-needs-focus and chunked. michael@0: perProcessArgs[0].insert(-1, "--focus-filter-mode=needs-focus") michael@0: for (chunkNumber, jobArgs) in enumerate(perProcessArgs[1:], start=1): michael@0: jobArgs[-1:-1] = ["--focus-filter-mode=non-needs-focus", michael@0: "--total-chunks=%d" % jobsWithoutFocus, michael@0: "--this-chunk=%d" % chunkNumber] michael@0: michael@0: for jobArgs in perProcessArgs: michael@0: try: michael@0: jobArgs.remove("--run-tests-in-parallel") michael@0: except: michael@0: pass michael@0: jobArgs.insert(-1, "--no-run-tests-in-parallel") michael@0: jobArgs[0:0] = [sys.executable, "-u"] michael@0: michael@0: threads = [ReftestThread(args) for args in perProcessArgs[1:]] michael@0: for t in threads: michael@0: t.start() michael@0: michael@0: while True: michael@0: # The test harness in each individual thread will be doing timeout michael@0: # handling on its own, so we shouldn't need to worry about any of michael@0: # the threads hanging for arbitrarily long. michael@0: for t in threads: michael@0: t.join(10) michael@0: if not any(t.is_alive() for t in threads): michael@0: break michael@0: michael@0: # Run the needs-focus tests serially after the other ones, so we don't michael@0: # have to worry about races between the needs-focus tests *actually* michael@0: # needing focus and the dummy windows in the non-needs-focus tests michael@0: # trying to focus themselves. michael@0: focusThread = ReftestThread(perProcessArgs[0]) michael@0: focusThread.start() michael@0: focusThread.join() michael@0: michael@0: # Output the summaries that the ReftestThread filters suppressed. michael@0: summaryObjects = [collections.defaultdict(int) for s in summaryLines] michael@0: for t in threads: michael@0: for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines): michael@0: threadMatches = t.summaryMatches[text] michael@0: for (attribute, description) in categories: michael@0: amount = int(threadMatches.group(attribute) if threadMatches else 0) michael@0: summaryObj[attribute] += amount michael@0: amount = int(threadMatches.group('total') if threadMatches else 0) michael@0: summaryObj['total'] += amount michael@0: michael@0: print 'REFTEST INFO | Result summary:' michael@0: for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines): michael@0: details = ', '.join(["%d %s" % (summaryObj[attribute], description) for (attribute, description) in categories]) michael@0: print 'REFTEST INFO | ' + text + ': ' + str(summaryObj['total']) + ' (' + details + ')' michael@0: michael@0: return int(any(t.retcode != 0 for t in threads)) michael@0: michael@0: def runSerialTests(self, testPath, options, cmdlineArgs = None): michael@0: debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs, michael@0: options.debuggerInteractive); michael@0: michael@0: profileDir = None michael@0: try: michael@0: reftestlist = self.getManifestPath(testPath) michael@0: if cmdlineArgs == None: michael@0: cmdlineArgs = ['-reftest', reftestlist] michael@0: profile = self.createReftestProfile(options, reftestlist) michael@0: profileDir = profile.profile # name makes more sense michael@0: michael@0: # browser environment michael@0: browserEnv = self.buildBrowserEnv(options, profileDir) michael@0: michael@0: self.automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n") michael@0: status = self.automation.runApp(None, browserEnv, options.app, profileDir, michael@0: cmdlineArgs, michael@0: utilityPath = options.utilityPath, michael@0: xrePath=options.xrePath, michael@0: debuggerInfo=debuggerInfo, michael@0: symbolsPath=options.symbolsPath, michael@0: # give the JS harness 30 seconds to deal michael@0: # with its own timeouts michael@0: timeout=options.timeout + 30.0) michael@0: processLeakLog(self.leakLogFile, options.leakThreshold) michael@0: self.automation.log.info("\nREFTEST INFO | runreftest.py | Running tests: end.") michael@0: finally: michael@0: self.cleanup(profileDir) michael@0: return status michael@0: michael@0: def copyExtraFilesToProfile(self, options, profile): michael@0: "Copy extra files or dirs specified on the command line to the testing profile." michael@0: profileDir = profile.profile michael@0: for f in options.extraProfileFiles: michael@0: abspath = self.getFullPath(f) michael@0: if os.path.isfile(abspath): michael@0: if os.path.basename(abspath) == 'user.js': michael@0: extra_prefs = mozprofile.Preferences.read_prefs(abspath) michael@0: profile.set_preferences(extra_prefs) michael@0: else: michael@0: shutil.copy2(abspath, profileDir) michael@0: elif os.path.isdir(abspath): michael@0: dest = os.path.join(profileDir, os.path.basename(abspath)) michael@0: shutil.copytree(abspath, dest) michael@0: else: michael@0: self.automation.log.warning("WARNING | runreftest.py | Failed to copy %s to profile", abspath) michael@0: continue michael@0: michael@0: michael@0: class ReftestOptions(OptionParser): michael@0: michael@0: def __init__(self, automation=None): michael@0: self.automation = automation or Automation() michael@0: OptionParser.__init__(self) michael@0: defaults = {} michael@0: michael@0: # we want to pass down everything from automation.__all__ michael@0: addCommonOptions(self, michael@0: defaults=dict(zip(self.automation.__all__, michael@0: [getattr(self.automation, x) for x in self.automation.__all__]))) michael@0: self.automation.addCommonOptions(self) michael@0: self.add_option("--appname", michael@0: action = "store", type = "string", dest = "app", michael@0: default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP), michael@0: help = "absolute path to application, overriding default") michael@0: self.add_option("--extra-profile-file", michael@0: action = "append", dest = "extraProfileFiles", michael@0: default = [], michael@0: help = "copy specified files/dirs to testing profile") michael@0: self.add_option("--timeout", michael@0: action = "store", dest = "timeout", type = "int", michael@0: default = 5 * 60, # 5 minutes per bug 479518 michael@0: help = "reftest will timeout in specified number of seconds. [default %default s].") michael@0: self.add_option("--leak-threshold", michael@0: action = "store", type = "int", dest = "leakThreshold", michael@0: default = 0, michael@0: help = "fail if the number of bytes leaked through " michael@0: "refcounted objects (or bytes in classes with " michael@0: "MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) is greater " michael@0: "than the given number") michael@0: self.add_option("--utility-path", michael@0: action = "store", type = "string", dest = "utilityPath", michael@0: default = self.automation.DIST_BIN, michael@0: help = "absolute path to directory containing utility " michael@0: "programs (xpcshell, ssltunnel, certutil)") michael@0: defaults["utilityPath"] = self.automation.DIST_BIN michael@0: michael@0: self.add_option("--total-chunks", michael@0: type = "int", dest = "totalChunks", michael@0: help = "how many chunks to split the tests up into") michael@0: defaults["totalChunks"] = None michael@0: michael@0: self.add_option("--this-chunk", michael@0: type = "int", dest = "thisChunk", michael@0: help = "which chunk to run between 1 and --total-chunks") michael@0: defaults["thisChunk"] = None michael@0: michael@0: self.add_option("--log-file", michael@0: action = "store", type = "string", dest = "logFile", michael@0: default = None, michael@0: help = "file to log output to in addition to stdout") michael@0: defaults["logFile"] = None michael@0: michael@0: self.add_option("--skip-slow-tests", michael@0: dest = "skipSlowTests", action = "store_true", michael@0: help = "skip tests marked as slow when running") michael@0: defaults["skipSlowTests"] = False michael@0: michael@0: self.add_option("--ignore-window-size", michael@0: dest = "ignoreWindowSize", action = "store_true", michael@0: help = "ignore the window size, which may cause spurious failures and passes") michael@0: defaults["ignoreWindowSize"] = False michael@0: michael@0: self.add_option("--install-extension", michael@0: action = "append", dest = "extensionsToInstall", michael@0: help = "install the specified extension in the testing profile. " michael@0: "The extension file's name should be .xpi where is " michael@0: "the extension's id as indicated in its install.rdf. " michael@0: "An optional path can be specified too.") michael@0: defaults["extensionsToInstall"] = [] michael@0: michael@0: self.add_option("--run-tests-in-parallel", michael@0: action = "store_true", dest = "runTestsInParallel", michael@0: help = "run tests in parallel if possible") michael@0: self.add_option("--no-run-tests-in-parallel", michael@0: action = "store_false", dest = "runTestsInParallel", michael@0: help = "do not run tests in parallel") michael@0: defaults["runTestsInParallel"] = False michael@0: michael@0: self.add_option("--setenv", michael@0: action = "append", type = "string", michael@0: dest = "environment", metavar = "NAME=VALUE", michael@0: help = "sets the given variable in the application's " michael@0: "environment") michael@0: defaults["environment"] = [] michael@0: michael@0: self.add_option("--filter", michael@0: action = "store", type="string", dest = "filter", michael@0: help = "specifies a regular expression (as could be passed to the JS " michael@0: "RegExp constructor) to test against URLs in the reftest manifest; " michael@0: "only test items that have a matching test URL will be run.") michael@0: defaults["filter"] = None michael@0: michael@0: self.add_option("--shuffle", michael@0: action = "store_true", dest = "shuffle", michael@0: help = "run reftests in random order") michael@0: defaults["shuffle"] = False michael@0: michael@0: self.add_option("--focus-filter-mode", michael@0: action = "store", type = "string", dest = "focusFilterMode", michael@0: help = "filters tests to run by whether they require focus. " michael@0: "Valid values are `all', `needs-focus', or `non-needs-focus'. " michael@0: "Defaults to `all'.") michael@0: defaults["focusFilterMode"] = "all" michael@0: michael@0: self.add_option("--e10s", michael@0: action = "store_true", michael@0: dest = "e10s", michael@0: help = "enables content processes") michael@0: defaults["e10s"] = False michael@0: michael@0: self.set_defaults(**defaults) michael@0: michael@0: def verifyCommonOptions(self, options, reftest): michael@0: if options.totalChunks is not None and options.thisChunk is None: michael@0: self.error("thisChunk must be specified when totalChunks is specified") michael@0: michael@0: if options.totalChunks: michael@0: if not 1 <= options.thisChunk <= options.totalChunks: michael@0: self.error("thisChunk must be between 1 and totalChunks") michael@0: michael@0: if options.logFile: michael@0: options.logFile = reftest.getFullPath(options.logFile) michael@0: michael@0: if options.xrePath is not None: michael@0: if not os.access(options.xrePath, os.F_OK): michael@0: self.error("--xre-path '%s' not found" % options.xrePath) michael@0: if not os.path.isdir(options.xrePath): michael@0: self.error("--xre-path '%s' is not a directory" % options.xrePath) michael@0: options.xrePath = reftest.getFullPath(options.xrePath) michael@0: michael@0: if options.runTestsInParallel: michael@0: if options.logFile is not None: michael@0: self.error("cannot specify logfile with parallel tests") michael@0: if options.totalChunks is not None and options.thisChunk is None: michael@0: self.error("cannot specify thisChunk or totalChunks with parallel tests") michael@0: if options.focusFilterMode != "all": michael@0: self.error("cannot specify focusFilterMode with parallel tests") michael@0: if options.debugger is not None: michael@0: self.error("cannot specify a debugger with parallel tests") michael@0: michael@0: return options michael@0: michael@0: def main(): michael@0: automation = Automation() michael@0: parser = ReftestOptions(automation) michael@0: reftest = RefTest(automation) michael@0: michael@0: options, args = parser.parse_args() michael@0: if len(args) != 1: michael@0: print >>sys.stderr, "No reftest.list specified." michael@0: sys.exit(1) michael@0: michael@0: options = parser.verifyCommonOptions(options, reftest) michael@0: michael@0: options.app = reftest.getFullPath(options.app) michael@0: if not os.path.exists(options.app): michael@0: print """Error: Path %(app)s doesn't exist. michael@0: Are you executing $objdir/_tests/reftest/runreftest.py?""" \ michael@0: % {"app": options.app} michael@0: sys.exit(1) michael@0: michael@0: if options.xrePath is None: michael@0: options.xrePath = os.path.dirname(options.app) michael@0: michael@0: if options.symbolsPath and not isURL(options.symbolsPath): michael@0: options.symbolsPath = reftest.getFullPath(options.symbolsPath) michael@0: options.utilityPath = reftest.getFullPath(options.utilityPath) michael@0: michael@0: sys.exit(reftest.runTests(args[0], options)) michael@0: michael@0: if __name__ == "__main__": michael@0: main()