layout/tools/reftest/runreftest.py

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     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/.
     5 """
     6 Runs the reftest test harness.
     7 """
     9 from optparse import OptionParser
    10 import collections
    11 import json
    12 import multiprocessing
    13 import os
    14 import re
    15 import shutil
    16 import subprocess
    17 import sys
    18 import threading
    20 SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
    21 sys.path.insert(0, SCRIPT_DIRECTORY)
    23 from automation import Automation
    24 from automationutils import (
    25         addCommonOptions,
    26         getDebuggerInfo,
    27         isURL,
    28         processLeakLog
    29 )
    30 import mozprofile
    32 def categoriesToRegex(categoryList):
    33   return "\\(" + ', '.join(["(?P<%s>\\d+) %s" % c for c in categoryList]) + "\\)"
    34 summaryLines = [('Successful', [('pass', 'pass'), ('loadOnly', 'load only')]),
    35                 ('Unexpected', [('fail', 'unexpected fail'),
    36                                 ('pass', 'unexpected pass'),
    37                                 ('asserts', 'unexpected asserts'),
    38                                 ('fixedAsserts', 'unexpected fixed asserts'),
    39                                 ('failedLoad', 'failed load'),
    40                                 ('exception', 'exception')]),
    41                 ('Known problems', [('knownFail', 'known fail'),
    42                                     ('knownAsserts', 'known asserts'),
    43                                     ('random', 'random'),
    44                                     ('skipped', 'skipped'),
    45                                     ('slow', 'slow')])]
    47 # Python's print is not threadsafe.
    48 printLock = threading.Lock()
    50 class ReftestThread(threading.Thread):
    51   def __init__(self, cmdlineArgs):
    52     threading.Thread.__init__(self)
    53     self.cmdlineArgs = cmdlineArgs
    54     self.summaryMatches = {}
    55     self.retcode = -1
    56     for text, _ in summaryLines:
    57       self.summaryMatches[text] = None
    59   def run(self):
    60     with printLock:
    61       print "Starting thread with", self.cmdlineArgs
    62       sys.stdout.flush()
    63     process = subprocess.Popen(self.cmdlineArgs, stdout=subprocess.PIPE)
    64     for chunk in self.chunkForMergedOutput(process.stdout):
    65       with printLock:
    66         print chunk,
    67         sys.stdout.flush()
    68     self.retcode = process.wait()
    70   def chunkForMergedOutput(self, logsource):
    71     """Gather lines together that should be printed as one atomic unit.
    72     Individual test results--anything between 'REFTEST TEST-START' and
    73     'REFTEST TEST-END' lines--are an atomic unit.  Lines with data from
    74     summaries are parsed and the data stored for later aggregation.
    75     Other lines are considered their own atomic units and are permitted
    76     to intermix freely."""
    77     testStartRegex = re.compile("^REFTEST TEST-START")
    78     testEndRegex = re.compile("^REFTEST TEST-END")
    79     summaryHeadRegex = re.compile("^REFTEST INFO \\| Result summary:")
    80     summaryRegexFormatString = "^REFTEST INFO \\| (?P<message>{text}): (?P<total>\\d+) {regex}"
    81     summaryRegexStrings = [summaryRegexFormatString.format(text=text,
    82                                                            regex=categoriesToRegex(categories))
    83                            for (text, categories) in summaryLines]
    84     summaryRegexes = [re.compile(regex) for regex in summaryRegexStrings]
    86     for line in logsource:
    87       if testStartRegex.search(line) is not None:
    88         chunkedLines = [line]
    89         for lineToBeChunked in logsource:
    90           chunkedLines.append(lineToBeChunked)
    91           if testEndRegex.search(lineToBeChunked) is not None:
    92             break
    93         yield ''.join(chunkedLines)
    94         continue
    96       haveSuppressedSummaryLine = False
    97       for regex in summaryRegexes:
    98         match = regex.search(line)
    99         if match is not None:
   100           self.summaryMatches[match.group('message')] = match
   101           haveSuppressedSummaryLine = True
   102           break
   103       if haveSuppressedSummaryLine:
   104         continue
   106       if summaryHeadRegex.search(line) is None:
   107         yield line
   109 class RefTest(object):
   111   oldcwd = os.getcwd()
   113   def __init__(self, automation=None):
   114     self.automation = automation or Automation()
   116   def getFullPath(self, path):
   117     "Get an absolute path relative to self.oldcwd."
   118     return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
   120   def getManifestPath(self, path):
   121     "Get the path of the manifest, and for remote testing this function is subclassed to point to remote manifest"
   122     path = self.getFullPath(path)
   123     if os.path.isdir(path):
   124       defaultManifestPath = os.path.join(path, 'reftest.list')
   125       if os.path.exists(defaultManifestPath):
   126         path = defaultManifestPath
   127       else:
   128         defaultManifestPath = os.path.join(path, 'crashtests.list')
   129         if os.path.exists(defaultManifestPath):
   130           path = defaultManifestPath
   131     return path
   133   def makeJSString(self, s):
   134     return '"%s"' % re.sub(r'([\\"])', r'\\\1', s)
   136   def createReftestProfile(self, options, manifest, server='localhost',
   137                            special_powers=True, profile_to_clone=None):
   138     """
   139       Sets up a profile for reftest.
   140       'manifest' is the path to the reftest.list file we want to test with.  This is used in
   141       the remote subclass in remotereftest.py so we can write it to a preference for the
   142       bootstrap extension.
   143     """
   145     locations = mozprofile.permissions.ServerLocations()
   146     locations.add_host(server, port=0)
   147     locations.add_host('<file>', port=0)
   149     # Set preferences for communication between our command line arguments
   150     # and the reftest harness.  Preferences that are required for reftest
   151     # to work should instead be set in reftest-cmdline.js .
   152     prefs = {}
   153     prefs['reftest.timeout'] = options.timeout * 1000
   154     if options.totalChunks:
   155       prefs['reftest.totalChunks'] = options.totalChunks
   156     if options.thisChunk:
   157       prefs['reftest.thisChunk'] = options.thisChunk
   158     if options.logFile:
   159       prefs['reftest.logFile'] = options.logFile
   160     if options.ignoreWindowSize:
   161       prefs['reftest.ignoreWindowSize'] = True
   162     if options.filter:
   163       prefs['reftest.filter'] = options.filter
   164     if options.shuffle:
   165       prefs['reftest.shuffle'] = True
   166     prefs['reftest.focusFilterMode'] = options.focusFilterMode
   168     # Ensure that telemetry is disabled, so we don't connect to the telemetry
   169     # server in the middle of the tests.
   170     prefs['toolkit.telemetry.enabled'] = False
   171     # Likewise for safebrowsing.
   172     prefs['browser.safebrowsing.enabled'] = False
   173     prefs['browser.safebrowsing.malware.enabled'] = False
   174     # And for snippets.
   175     prefs['browser.snippets.enabled'] = False
   176     prefs['browser.snippets.syncPromo.enabled'] = False
   177     # And for useragent updates.
   178     prefs['general.useragent.updates.enabled'] = False
   179     # And for webapp updates.  Yes, it is supposed to be an integer.
   180     prefs['browser.webapps.checkForUpdates'] = 0
   182     if options.e10s:
   183       prefs['browser.tabs.remote.autostart'] = True
   185     for v in options.extraPrefs:
   186       thispref = v.split('=')
   187       if len(thispref) < 2:
   188         print "Error: syntax error in --setpref=" + v
   189         sys.exit(1)
   190       prefs[thispref[0]] = mozprofile.Preferences.cast(thispref[1].strip())
   192     # install the reftest extension bits into the profile
   193     addons = []
   194     addons.append(os.path.join(SCRIPT_DIRECTORY, "reftest"))
   196     # I would prefer to use "--install-extension reftest/specialpowers", but that requires tight coordination with
   197     # release engineering and landing on multiple branches at once.
   198     if special_powers and (manifest.endswith('crashtests.list') or manifest.endswith('jstests.list')):
   199       addons.append(os.path.join(SCRIPT_DIRECTORY, 'specialpowers'))
   201     # Install distributed extensions, if application has any.
   202     distExtDir = os.path.join(options.app[ : options.app.rfind(os.sep)], "distribution", "extensions")
   203     if os.path.isdir(distExtDir):
   204       for f in os.listdir(distExtDir):
   205         addons.append(os.path.join(distExtDir, f))
   207     # Install custom extensions.
   208     for f in options.extensionsToInstall:
   209       addons.append(self.getFullPath(f))
   211     kwargs = { 'addons': addons,
   212                'preferences': prefs,
   213                'locations': locations }
   214     if profile_to_clone:
   215         profile = mozprofile.Profile.clone(profile_to_clone, **kwargs)
   216     else:
   217         profile = mozprofile.Profile(**kwargs)
   219     self.copyExtraFilesToProfile(options, profile)
   220     return profile
   222   def buildBrowserEnv(self, options, profileDir):
   223     browserEnv = self.automation.environment(xrePath = options.xrePath, debugger=options.debugger)
   224     browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
   226     for v in options.environment:
   227       ix = v.find("=")
   228       if ix <= 0:
   229         print "Error: syntax error in --setenv=" + v
   230         return None
   231       browserEnv[v[:ix]] = v[ix + 1:]
   233     # Enable leaks detection to its own log file.
   234     self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log")
   235     browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile
   236     return browserEnv
   238   def cleanup(self, profileDir):
   239     if profileDir:
   240       shutil.rmtree(profileDir, True)
   242   def runTests(self, testPath, options, cmdlineArgs = None):
   243     if not options.runTestsInParallel:
   244       return self.runSerialTests(testPath, options, cmdlineArgs)
   246     cpuCount = multiprocessing.cpu_count()
   248     # We have the directive, technology, and machine to run multiple test instances.
   249     # Experimentation says that reftests are not overly CPU-intensive, so we can run
   250     # multiple jobs per CPU core.
   251     #
   252     # Our Windows machines in automation seem to get upset when we run a lot of
   253     # simultaneous tests on them, so tone things down there.
   254     if sys.platform == 'win32':
   255       jobsWithoutFocus = cpuCount
   256     else:
   257       jobsWithoutFocus = 2 * cpuCount
   259     totalJobs = jobsWithoutFocus + 1
   260     perProcessArgs = [sys.argv[:] for i in range(0, totalJobs)]
   262     # First job is only needs-focus tests.  Remaining jobs are non-needs-focus and chunked.
   263     perProcessArgs[0].insert(-1, "--focus-filter-mode=needs-focus")
   264     for (chunkNumber, jobArgs) in enumerate(perProcessArgs[1:], start=1):
   265       jobArgs[-1:-1] = ["--focus-filter-mode=non-needs-focus",
   266                         "--total-chunks=%d" % jobsWithoutFocus,
   267                         "--this-chunk=%d" % chunkNumber]
   269     for jobArgs in perProcessArgs:
   270       try:
   271         jobArgs.remove("--run-tests-in-parallel")
   272       except:
   273         pass
   274       jobArgs.insert(-1, "--no-run-tests-in-parallel")
   275       jobArgs[0:0] = [sys.executable, "-u"]
   277     threads = [ReftestThread(args) for args in perProcessArgs[1:]]
   278     for t in threads:
   279       t.start()
   281     while True:
   282       # The test harness in each individual thread will be doing timeout
   283       # handling on its own, so we shouldn't need to worry about any of
   284       # the threads hanging for arbitrarily long.
   285       for t in threads:
   286         t.join(10)
   287       if not any(t.is_alive() for t in threads):
   288         break
   290     # Run the needs-focus tests serially after the other ones, so we don't
   291     # have to worry about races between the needs-focus tests *actually*
   292     # needing focus and the dummy windows in the non-needs-focus tests
   293     # trying to focus themselves.
   294     focusThread = ReftestThread(perProcessArgs[0])
   295     focusThread.start()
   296     focusThread.join()
   298     # Output the summaries that the ReftestThread filters suppressed.
   299     summaryObjects = [collections.defaultdict(int) for s in summaryLines]
   300     for t in threads:
   301       for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines):
   302         threadMatches = t.summaryMatches[text]
   303         for (attribute, description) in categories:
   304           amount = int(threadMatches.group(attribute) if threadMatches else 0)
   305           summaryObj[attribute] += amount
   306         amount = int(threadMatches.group('total') if threadMatches else 0)
   307         summaryObj['total'] += amount
   309     print 'REFTEST INFO | Result summary:'
   310     for (summaryObj, (text, categories)) in zip(summaryObjects, summaryLines):
   311       details = ', '.join(["%d %s" % (summaryObj[attribute], description) for (attribute, description) in categories])
   312       print 'REFTEST INFO | ' + text + ': ' + str(summaryObj['total']) + ' (' +  details + ')'
   314     return int(any(t.retcode != 0 for t in threads))
   316   def runSerialTests(self, testPath, options, cmdlineArgs = None):
   317     debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs,
   318         options.debuggerInteractive);
   320     profileDir = None
   321     try:
   322       reftestlist = self.getManifestPath(testPath)
   323       if cmdlineArgs == None:
   324         cmdlineArgs = ['-reftest', reftestlist]
   325       profile = self.createReftestProfile(options, reftestlist)
   326       profileDir = profile.profile # name makes more sense
   328       # browser environment
   329       browserEnv = self.buildBrowserEnv(options, profileDir)
   331       self.automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n")
   332       status = self.automation.runApp(None, browserEnv, options.app, profileDir,
   333                                  cmdlineArgs,
   334                                  utilityPath = options.utilityPath,
   335                                  xrePath=options.xrePath,
   336                                  debuggerInfo=debuggerInfo,
   337                                  symbolsPath=options.symbolsPath,
   338                                  # give the JS harness 30 seconds to deal
   339                                  # with its own timeouts
   340                                  timeout=options.timeout + 30.0)
   341       processLeakLog(self.leakLogFile, options.leakThreshold)
   342       self.automation.log.info("\nREFTEST INFO | runreftest.py | Running tests: end.")
   343     finally:
   344       self.cleanup(profileDir)
   345     return status
   347   def copyExtraFilesToProfile(self, options, profile):
   348     "Copy extra files or dirs specified on the command line to the testing profile."
   349     profileDir = profile.profile
   350     for f in options.extraProfileFiles:
   351       abspath = self.getFullPath(f)
   352       if os.path.isfile(abspath):
   353         if os.path.basename(abspath) == 'user.js':
   354           extra_prefs = mozprofile.Preferences.read_prefs(abspath)
   355           profile.set_preferences(extra_prefs)
   356         else:
   357           shutil.copy2(abspath, profileDir)
   358       elif os.path.isdir(abspath):
   359         dest = os.path.join(profileDir, os.path.basename(abspath))
   360         shutil.copytree(abspath, dest)
   361       else:
   362         self.automation.log.warning("WARNING | runreftest.py | Failed to copy %s to profile", abspath)
   363         continue
   366 class ReftestOptions(OptionParser):
   368   def __init__(self, automation=None):
   369     self.automation = automation or Automation()
   370     OptionParser.__init__(self)
   371     defaults = {}
   373     # we want to pass down everything from automation.__all__
   374     addCommonOptions(self,
   375                      defaults=dict(zip(self.automation.__all__,
   376                             [getattr(self.automation, x) for x in self.automation.__all__])))
   377     self.automation.addCommonOptions(self)
   378     self.add_option("--appname",
   379                     action = "store", type = "string", dest = "app",
   380                     default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP),
   381                     help = "absolute path to application, overriding default")
   382     self.add_option("--extra-profile-file",
   383                     action = "append", dest = "extraProfileFiles",
   384                     default = [],
   385                     help = "copy specified files/dirs to testing profile")
   386     self.add_option("--timeout",
   387                     action = "store", dest = "timeout", type = "int",
   388                     default = 5 * 60, # 5 minutes per bug 479518
   389                     help = "reftest will timeout in specified number of seconds. [default %default s].")
   390     self.add_option("--leak-threshold",
   391                     action = "store", type = "int", dest = "leakThreshold",
   392                     default = 0,
   393                     help = "fail if the number of bytes leaked through "
   394                            "refcounted objects (or bytes in classes with "
   395                            "MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) is greater "
   396                            "than the given number")
   397     self.add_option("--utility-path",
   398                     action = "store", type = "string", dest = "utilityPath",
   399                     default = self.automation.DIST_BIN,
   400                     help = "absolute path to directory containing utility "
   401                            "programs (xpcshell, ssltunnel, certutil)")
   402     defaults["utilityPath"] = self.automation.DIST_BIN
   404     self.add_option("--total-chunks",
   405                     type = "int", dest = "totalChunks",
   406                     help = "how many chunks to split the tests up into")
   407     defaults["totalChunks"] = None
   409     self.add_option("--this-chunk",
   410                     type = "int", dest = "thisChunk",
   411                     help = "which chunk to run between 1 and --total-chunks")
   412     defaults["thisChunk"] = None
   414     self.add_option("--log-file",
   415                     action = "store", type = "string", dest = "logFile",
   416                     default = None,
   417                     help = "file to log output to in addition to stdout")
   418     defaults["logFile"] = None
   420     self.add_option("--skip-slow-tests",
   421                     dest = "skipSlowTests", action = "store_true",
   422                     help = "skip tests marked as slow when running")
   423     defaults["skipSlowTests"] = False
   425     self.add_option("--ignore-window-size",
   426                     dest = "ignoreWindowSize", action = "store_true",
   427                     help = "ignore the window size, which may cause spurious failures and passes")
   428     defaults["ignoreWindowSize"] = False
   430     self.add_option("--install-extension",
   431                     action = "append", dest = "extensionsToInstall",
   432                     help = "install the specified extension in the testing profile. "
   433                            "The extension file's name should be <id>.xpi where <id> is "
   434                            "the extension's id as indicated in its install.rdf. "
   435                            "An optional path can be specified too.")
   436     defaults["extensionsToInstall"] = []
   438     self.add_option("--run-tests-in-parallel",
   439                     action = "store_true", dest = "runTestsInParallel",
   440                     help = "run tests in parallel if possible")
   441     self.add_option("--no-run-tests-in-parallel",
   442                     action = "store_false", dest = "runTestsInParallel",
   443                     help = "do not run tests in parallel")
   444     defaults["runTestsInParallel"] = False
   446     self.add_option("--setenv",
   447                     action = "append", type = "string",
   448                     dest = "environment", metavar = "NAME=VALUE",
   449                     help = "sets the given variable in the application's "
   450                            "environment")
   451     defaults["environment"] = []
   453     self.add_option("--filter",
   454                     action = "store", type="string", dest = "filter",
   455                     help = "specifies a regular expression (as could be passed to the JS "
   456                            "RegExp constructor) to test against URLs in the reftest manifest; "
   457                            "only test items that have a matching test URL will be run.")
   458     defaults["filter"] = None
   460     self.add_option("--shuffle",
   461                     action = "store_true", dest = "shuffle",
   462                     help = "run reftests in random order")
   463     defaults["shuffle"] = False
   465     self.add_option("--focus-filter-mode",
   466                     action = "store", type = "string", dest = "focusFilterMode",
   467                     help = "filters tests to run by whether they require focus. "
   468                            "Valid values are `all', `needs-focus', or `non-needs-focus'. "
   469                            "Defaults to `all'.")
   470     defaults["focusFilterMode"] = "all"
   472     self.add_option("--e10s",
   473                     action = "store_true",
   474                     dest = "e10s",
   475                     help = "enables content processes")
   476     defaults["e10s"] = False
   478     self.set_defaults(**defaults)
   480   def verifyCommonOptions(self, options, reftest):
   481     if options.totalChunks is not None and options.thisChunk is None:
   482       self.error("thisChunk must be specified when totalChunks is specified")
   484     if options.totalChunks:
   485       if not 1 <= options.thisChunk <= options.totalChunks:
   486         self.error("thisChunk must be between 1 and totalChunks")
   488     if options.logFile:
   489       options.logFile = reftest.getFullPath(options.logFile)
   491     if options.xrePath is not None:
   492       if not os.access(options.xrePath, os.F_OK):
   493         self.error("--xre-path '%s' not found" % options.xrePath)
   494       if not os.path.isdir(options.xrePath):
   495         self.error("--xre-path '%s' is not a directory" % options.xrePath)
   496       options.xrePath = reftest.getFullPath(options.xrePath)
   498     if options.runTestsInParallel:
   499       if options.logFile is not None:
   500         self.error("cannot specify logfile with parallel tests")
   501       if options.totalChunks is not None and options.thisChunk is None:
   502         self.error("cannot specify thisChunk or totalChunks with parallel tests")
   503       if options.focusFilterMode != "all":
   504         self.error("cannot specify focusFilterMode with parallel tests")
   505       if options.debugger is not None:
   506         self.error("cannot specify a debugger with parallel tests")
   508     return options
   510 def main():
   511   automation = Automation()
   512   parser = ReftestOptions(automation)
   513   reftest = RefTest(automation)
   515   options, args = parser.parse_args()
   516   if len(args) != 1:
   517     print >>sys.stderr, "No reftest.list specified."
   518     sys.exit(1)
   520   options = parser.verifyCommonOptions(options, reftest)
   522   options.app = reftest.getFullPath(options.app)
   523   if not os.path.exists(options.app):
   524     print """Error: Path %(app)s doesn't exist.
   525 Are you executing $objdir/_tests/reftest/runreftest.py?""" \
   526             % {"app": options.app}
   527     sys.exit(1)
   529   if options.xrePath is None:
   530     options.xrePath = os.path.dirname(options.app)
   532   if options.symbolsPath and not isURL(options.symbolsPath):
   533     options.symbolsPath = reftest.getFullPath(options.symbolsPath)
   534   options.utilityPath = reftest.getFullPath(options.utilityPath)
   536   sys.exit(reftest.runTests(args[0], options))
   538 if __name__ == "__main__":
   539   main()

mercurial