testing/mochitest/runtests.py

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

     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 Mochitest test harness.
     7 """
     9 from __future__ import with_statement
    10 import os
    11 import sys
    12 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
    13 sys.path.insert(0, SCRIPT_DIR);
    15 import glob
    16 import json
    17 import mozcrash
    18 import mozinfo
    19 import mozprocess
    20 import mozrunner
    21 import optparse
    22 import re
    23 import shutil
    24 import signal
    25 import subprocess
    26 import tempfile
    27 import time
    28 import traceback
    29 import urllib2
    30 import zipfile
    32 from automationutils import environment, getDebuggerInfo, isURL, KeyValueParseError, parseKeyValue, processLeakLog, systemMemory, dumpScreen, ShutdownLeaks, printstatus
    33 from datetime import datetime
    34 from manifestparser import TestManifest
    35 from mochitest_options import MochitestOptions
    36 from mozprofile import Profile, Preferences
    37 from mozprofile.permissions import ServerLocations
    38 from urllib import quote_plus as encodeURIComponent
    40 # This should use the `which` module already in tree, but it is
    41 # not yet present in the mozharness environment
    42 from mozrunner.utils import findInPath as which
    44 # set up logging handler a la automation.py.in for compatability
    45 import logging
    46 log = logging.getLogger()
    47 def resetGlobalLog():
    48   while log.handlers:
    49     log.removeHandler(log.handlers[0])
    50   handler = logging.StreamHandler(sys.stdout)
    51   log.setLevel(logging.INFO)
    52   log.addHandler(handler)
    53 resetGlobalLog()
    55 ###########################
    56 # Option for NSPR logging #
    57 ###########################
    59 # Set the desired log modules you want an NSPR log be produced by a try run for, or leave blank to disable the feature.
    60 # This will be passed to NSPR_LOG_MODULES environment variable. Try run will then put a download link for the log file
    61 # on tbpl.mozilla.org.
    63 NSPR_LOG_MODULES = ""
    65 ####################
    66 # PROCESS HANDLING #
    67 ####################
    69 def call(*args, **kwargs):
    70   """front-end function to mozprocess.ProcessHandler"""
    71   # TODO: upstream -> mozprocess
    72   # https://bugzilla.mozilla.org/show_bug.cgi?id=791383
    73   process = mozprocess.ProcessHandler(*args, **kwargs)
    74   process.run()
    75   return process.wait()
    77 def killPid(pid):
    78   # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58
    79   try:
    80     os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
    81   except Exception, e:
    82     log.info("Failed to kill process %d: %s", pid, str(e))
    84 if mozinfo.isWin:
    85   import ctypes, ctypes.wintypes, time, msvcrt
    87   def isPidAlive(pid):
    88     STILL_ACTIVE = 259
    89     PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
    90     pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
    91     if not pHandle:
    92       return False
    93     pExitCode = ctypes.wintypes.DWORD()
    94     ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
    95     ctypes.windll.kernel32.CloseHandle(pHandle)
    96     return pExitCode.value == STILL_ACTIVE
    98 else:
    99   import errno
   101   def isPidAlive(pid):
   102     try:
   103       # kill(pid, 0) checks for a valid PID without actually sending a signal
   104       # The method throws OSError if the PID is invalid, which we catch below.
   105       os.kill(pid, 0)
   107       # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
   108       # the process terminates before we get to this point.
   109       wpid, wstatus = os.waitpid(pid, os.WNOHANG)
   110       return wpid == 0
   111     except OSError, err:
   112       # Catch the errors we might expect from os.kill/os.waitpid,
   113       # and re-raise any others
   114       if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
   115         return False
   116       raise
   117 # TODO: ^ upstream isPidAlive to mozprocess
   119 #######################
   120 # HTTP SERVER SUPPORT #
   121 #######################
   123 class MochitestServer(object):
   124   "Web server used to serve Mochitests, for closer fidelity to the real web."
   126   def __init__(self, options):
   127     if isinstance(options, optparse.Values):
   128       options = vars(options)
   129     self._closeWhenDone = options['closeWhenDone']
   130     self._utilityPath = options['utilityPath']
   131     self._xrePath = options['xrePath']
   132     self._profileDir = options['profilePath']
   133     self.webServer = options['webServer']
   134     self.httpPort = options['httpPort']
   135     self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort }
   136     self.testPrefix = "'webapprt_'" if options.get('webapprtContent') else "undefined"
   138     if options.get('httpdPath'):
   139         self._httpdPath = options['httpdPath']
   140     else:
   141         self._httpdPath = SCRIPT_DIR
   142     self._httpdPath = os.path.abspath(self._httpdPath)
   144   def start(self):
   145     "Run the Mochitest server, returning the process ID of the server."
   147     # get testing environment
   148     env = environment(xrePath=self._xrePath)
   149     env["XPCOM_DEBUG_BREAK"] = "warn"
   151     # When running with an ASan build, our xpcshell server will also be ASan-enabled,
   152     # thus consuming too much resources when running together with the browser on
   153     # the test slaves. Try to limit the amount of resources by disabling certain
   154     # features.
   155     env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5"
   157     if mozinfo.isWin:
   158       env["PATH"] = env["PATH"] + ";" + str(self._xrePath)
   160     args = ["-g", self._xrePath,
   161             "-v", "170",
   162             "-f", os.path.join(self._httpdPath, "httpd.js"),
   163             "-e", """const _PROFILE_PATH = '%(profile)s'; const _SERVER_PORT = '%(port)s'; const _SERVER_ADDR = '%(server)s'; const _TEST_PREFIX = %(testPrefix)s; const _DISPLAY_RESULTS = %(displayResults)s;""" %
   164                    {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer,
   165                     "testPrefix" : self.testPrefix, "displayResults" : str(not self._closeWhenDone).lower() },
   166             "-f", os.path.join(SCRIPT_DIR, "server.js")]
   168     xpcshell = os.path.join(self._utilityPath,
   169                             "xpcshell" + mozinfo.info['bin_suffix'])
   170     command = [xpcshell] + args
   171     self._process = mozprocess.ProcessHandler(command, cwd=SCRIPT_DIR, env=env)
   172     self._process.run()
   173     log.info("%s : launching %s", self.__class__.__name__, command)
   174     pid = self._process.pid
   175     log.info("runtests.py | Server pid: %d", pid)
   177   def ensureReady(self, timeout):
   178     assert timeout >= 0
   180     aliveFile = os.path.join(self._profileDir, "server_alive.txt")
   181     i = 0
   182     while i < timeout:
   183       if os.path.exists(aliveFile):
   184         break
   185       time.sleep(1)
   186       i += 1
   187     else:
   188       log.error("TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup.")
   189       self.stop()
   190       sys.exit(1)
   192   def stop(self):
   193     try:
   194       with urllib2.urlopen(self.shutdownURL) as c:
   195         c.read()
   197       # TODO: need ProcessHandler.poll()
   198       # https://bugzilla.mozilla.org/show_bug.cgi?id=912285
   199       #      rtncode = self._process.poll()
   200       rtncode = self._process.proc.poll()
   201       if rtncode is None:
   202         # TODO: need ProcessHandler.terminate() and/or .send_signal()
   203         # https://bugzilla.mozilla.org/show_bug.cgi?id=912285
   204         # self._process.terminate()
   205         self._process.proc.terminate()
   206     except:
   207       self._process.kill()
   209 class WebSocketServer(object):
   210   "Class which encapsulates the mod_pywebsocket server"
   212   def __init__(self, options, scriptdir, debuggerInfo=None):
   213     self.port = options.webSocketPort
   214     self._scriptdir = scriptdir
   215     self.debuggerInfo = debuggerInfo
   217   def start(self):
   218     # Invoke pywebsocket through a wrapper which adds special SIGINT handling.
   219     #
   220     # If we're in an interactive debugger, the wrapper causes the server to
   221     # ignore SIGINT so the server doesn't capture a ctrl+c meant for the
   222     # debugger.
   223     #
   224     # If we're not in an interactive debugger, the wrapper causes the server to
   225     # die silently upon receiving a SIGINT.
   226     scriptPath = 'pywebsocket_wrapper.py'
   227     script = os.path.join(self._scriptdir, scriptPath)
   229     cmd = [sys.executable, script]
   230     if self.debuggerInfo and self.debuggerInfo['interactive']:
   231         cmd += ['--interactive']
   232     cmd += ['-p', str(self.port), '-w', self._scriptdir, '-l',      \
   233            os.path.join(self._scriptdir, "websock.log"),            \
   234            '--log-level=debug', '--allow-handlers-outside-root-dir']
   235     # start the process
   236     self._process = mozprocess.ProcessHandler(cmd, cwd=SCRIPT_DIR)
   237     self._process.run()
   238     pid = self._process.pid
   239     log.info("runtests.py | Websocket server pid: %d", pid)
   241   def stop(self):
   242     self._process.kill()
   244 class MochitestUtilsMixin(object):
   245   """
   246   Class containing some utility functions common to both local and remote
   247   mochitest runners
   248   """
   250   # TODO Utility classes are a code smell. This class is temporary
   251   #      and should be removed when desktop mochitests are refactored
   252   #      on top of mozbase. Each of the functions in here should
   253   #      probably live somewhere in mozbase
   255   oldcwd = os.getcwd()
   256   jarDir = 'mochijar'
   258   # Path to the test script on the server
   259   TEST_PATH = "tests"
   260   CHROME_PATH = "redirect.html"
   261   urlOpts = []
   263   def __init__(self):
   264     self.update_mozinfo()
   265     self.server = None
   266     self.wsserver = None
   267     self.sslTunnel = None
   268     self._locations = None
   270   def update_mozinfo(self):
   271     """walk up directories to find mozinfo.json update the info"""
   272     # TODO: This should go in a more generic place, e.g. mozinfo
   274     path = SCRIPT_DIR
   275     dirs = set()
   276     while path != os.path.expanduser('~'):
   277         if path in dirs:
   278             break
   279         dirs.add(path)
   280         path = os.path.split(path)[0]
   282     mozinfo.find_and_update_from_json(*dirs)
   284   def getFullPath(self, path):
   285     " Get an absolute path relative to self.oldcwd."
   286     return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
   288   def getLogFilePath(self, logFile):
   289     """ return the log file path relative to the device we are testing on, in most cases
   290         it will be the full path on the local system
   291     """
   292     return self.getFullPath(logFile)
   294   @property
   295   def locations(self):
   296     if self._locations is not None:
   297       return self._locations
   298     locations_file = os.path.join(SCRIPT_DIR, 'server-locations.txt')
   299     self._locations = ServerLocations(locations_file)
   300     return self._locations
   302   def buildURLOptions(self, options, env):
   303     """ Add test control options from the command line to the url
   305         URL parameters to test URL:
   307         autorun -- kick off tests automatically
   308         closeWhenDone -- closes the browser after the tests
   309         hideResultsTable -- hides the table of individual test results
   310         logFile -- logs test run to an absolute path
   311         totalChunks -- how many chunks to split tests into
   312         thisChunk -- which chunk to run
   313         startAt -- name of test to start at
   314         endAt -- name of test to end at
   315         timeout -- per-test timeout in seconds
   316         repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice.
   317     """
   319     # allow relative paths for logFile
   320     if options.logFile:
   321       options.logFile = self.getLogFilePath(options.logFile)
   323     # Note that all tests under options.subsuite need to be browser chrome tests.
   324     if options.browserChrome or options.chrome or options.subsuite or \
   325        options.a11y or options.webapprtChrome:
   326       self.makeTestConfig(options)
   327     else:
   328       if options.autorun:
   329         self.urlOpts.append("autorun=1")
   330       if options.timeout:
   331         self.urlOpts.append("timeout=%d" % options.timeout)
   332       if options.closeWhenDone:
   333         self.urlOpts.append("closeWhenDone=1")
   334       if options.logFile:
   335         self.urlOpts.append("logFile=" + encodeURIComponent(options.logFile))
   336         self.urlOpts.append("fileLevel=" + encodeURIComponent(options.fileLevel))
   337       if options.consoleLevel:
   338         self.urlOpts.append("consoleLevel=" + encodeURIComponent(options.consoleLevel))
   339       if options.totalChunks:
   340         self.urlOpts.append("totalChunks=%d" % options.totalChunks)
   341         self.urlOpts.append("thisChunk=%d" % options.thisChunk)
   342       if options.chunkByDir:
   343         self.urlOpts.append("chunkByDir=%d" % options.chunkByDir)
   344       if options.startAt:
   345         self.urlOpts.append("startAt=%s" % options.startAt)
   346       if options.endAt:
   347         self.urlOpts.append("endAt=%s" % options.endAt)
   348       if options.shuffle:
   349         self.urlOpts.append("shuffle=1")
   350       if "MOZ_HIDE_RESULTS_TABLE" in env and env["MOZ_HIDE_RESULTS_TABLE"] == "1":
   351         self.urlOpts.append("hideResultsTable=1")
   352       if options.runUntilFailure:
   353         self.urlOpts.append("runUntilFailure=1")
   354       if options.repeat:
   355         self.urlOpts.append("repeat=%d" % options.repeat)
   356       if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, options.testPath)) and options.repeat > 0:
   357         self.urlOpts.append("testname=%s" % ("/").join([self.TEST_PATH, options.testPath]))
   358       if options.testManifest:
   359         self.urlOpts.append("testManifest=%s" % options.testManifest)
   360         if hasattr(options, 'runOnly') and options.runOnly:
   361           self.urlOpts.append("runOnly=true")
   362         else:
   363           self.urlOpts.append("runOnly=false")
   364       if options.manifestFile:
   365         self.urlOpts.append("manifestFile=%s" % options.manifestFile)
   366       if options.failureFile:
   367         self.urlOpts.append("failureFile=%s" % self.getFullPath(options.failureFile))
   368       if options.runSlower:
   369         self.urlOpts.append("runSlower=true")
   370       if options.debugOnFailure:
   371         self.urlOpts.append("debugOnFailure=true")
   372       if options.dumpOutputDirectory:
   373         self.urlOpts.append("dumpOutputDirectory=%s" % encodeURIComponent(options.dumpOutputDirectory))
   374       if options.dumpAboutMemoryAfterTest:
   375         self.urlOpts.append("dumpAboutMemoryAfterTest=true")
   376       if options.dumpDMDAfterTest:
   377         self.urlOpts.append("dumpDMDAfterTest=true")
   378       if options.quiet:
   379         self.urlOpts.append("quiet=true")
   381   def getTestFlavor(self, options):
   382     if options.browserChrome:
   383       return "browser-chrome"
   384     elif options.chrome:
   385       return "chrome"
   386     elif options.a11y:
   387       return "a11y"
   388     elif options.webapprtChrome:
   389       return "webapprt-chrome"
   390     else:
   391       return "mochitest"
   393   # This check can be removed when bug 983867 is fixed.
   394   def isTest(self, options, filename):
   395     allow_js_css = False
   396     if options.browserChrome:
   397       allow_js_css = True
   398       testPattern = re.compile(r"browser_.+\.js")
   399     elif options.chrome or options.a11y:
   400       testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)")
   401     elif options.webapprtContent:
   402       testPattern = re.compile(r"webapprt_")
   403     elif options.webapprtChrome:
   404       allow_js_css = True
   405       testPattern = re.compile(r"browser_")
   406     else:
   407       testPattern = re.compile(r"test_")
   409     if not allow_js_css and (".js" in filename or ".css" in filename):
   410       return False
   412     pathPieces = filename.split("/")
   414     return (testPattern.match(pathPieces[-1]) and
   415             not re.search(r'\^headers\^$', filename))
   417   def getTestPath(self, options):
   418     if options.ipcplugins:
   419       return "dom/plugins/test"
   420     else:
   421       return options.testPath
   423   def getTestRoot(self, options):
   424     if options.browserChrome:
   425       if options.immersiveMode:
   426         return 'metro'
   427       return 'browser'
   428     elif options.a11y:
   429       return 'a11y'
   430     elif options.webapprtChrome:
   431       return 'webapprtChrome'
   432     elif options.chrome:
   433       return 'chrome'
   434     return self.TEST_PATH
   436   def buildTestURL(self, options):
   437     testHost = "http://mochi.test:8888"
   438     testPath = self.getTestPath(options)
   439     testURL = "/".join([testHost, self.TEST_PATH, testPath])
   440     if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, testPath)) and options.repeat > 0:
   441       testURL = "/".join([testHost, self.TEST_PATH, os.path.dirname(testPath)])
   442     if options.chrome or options.a11y:
   443       testURL = "/".join([testHost, self.CHROME_PATH])
   444     elif options.browserChrome:
   445       testURL = "about:blank"
   446     return testURL
   448   def buildTestPath(self, options):
   449     """ Build the url path to the specific test harness and test file or directory
   450         Build a manifest of tests to run and write out a json file for the harness to read
   451     """
   452     manifest = None
   454     testRoot = self.getTestRoot(options)
   455     # testdir refers to 'mochitest' here.
   456     testdir = SCRIPT_DIR.split(os.getcwd())[-1]
   457     testdir = testdir.strip(os.sep)
   458     testRootAbs = os.path.abspath(os.path.join(testdir, testRoot))
   459     if isinstance(options.manifestFile, TestManifest):
   460         manifest = options.manifestFile
   461     elif options.manifestFile and os.path.isfile(options.manifestFile):
   462       manifestFileAbs = os.path.abspath(options.manifestFile)
   463       assert manifestFileAbs.startswith(testRootAbs)
   464       manifest = TestManifest([options.manifestFile], strict=False)
   465     else:
   466       masterName = self.getTestFlavor(options) + '.ini'
   467       masterPath = os.path.join(testdir, testRoot, masterName)
   469       if os.path.exists(masterPath):
   470         manifest = TestManifest([masterPath], strict=False)
   472     if manifest:
   473       # Python 2.6 doesn't allow unicode keys to be used for keyword
   474       # arguments. This gross hack works around the problem until we
   475       # rid ourselves of 2.6.
   476       info = {}
   477       for k, v in mozinfo.info.items():
   478         if isinstance(k, unicode):
   479           k = k.encode('ascii')
   480         info[k] = v
   482       # Bug 883858 - return all tests including disabled tests
   483       tests = manifest.active_tests(disabled=True, options=options, **info)
   484       paths = []
   485       testPath = self.getTestPath(options)
   487       for test in tests:
   488         pathAbs = os.path.abspath(test['path'])
   489         assert pathAbs.startswith(testRootAbs)
   490         tp = pathAbs[len(testRootAbs):].replace('\\', '/').strip('/')
   492         # Filter out tests if we are using --test-path
   493         if testPath and not tp.startswith(testPath):
   494           continue
   496         if not self.isTest(options, tp):
   497           print 'Warning: %s from manifest %s is not a valid test' % (test['name'], test['manifest'])
   498           continue
   500         testob = {'path': tp}
   501         if test.has_key('disabled'):
   502           testob['disabled'] = test['disabled']
   503         paths.append(testob)
   505       # Sort tests so they are run in a deterministic order.
   506       def path_sort(ob1, ob2):
   507         path1 = ob1['path'].split('/')
   508         path2 = ob2['path'].split('/')
   509         return cmp(path1, path2)
   511       paths.sort(path_sort)
   513       # Bug 883865 - add this functionality into manifestDestiny
   514       with open(os.path.join(testdir, 'tests.json'), 'w') as manifestFile:
   515         manifestFile.write(json.dumps({'tests': paths}))
   516       options.manifestFile = 'tests.json'
   518     return self.buildTestURL(options)
   520   def startWebSocketServer(self, options, debuggerInfo):
   521     """ Launch the websocket server """
   522     self.wsserver = WebSocketServer(options, SCRIPT_DIR, debuggerInfo)
   523     self.wsserver.start()
   525   def startWebServer(self, options):
   526     """Create the webserver and start it up"""
   528     self.server = MochitestServer(options)
   529     self.server.start()
   531     if options.pidFile != "":
   532       with open(options.pidFile + ".xpcshell.pid", 'w') as f:
   533         f.write("%s" % self.server._process.pid)
   535   def startServers(self, options, debuggerInfo):
   536     # start servers and set ports
   537     # TODO: pass these values, don't set on `self`
   538     self.webServer = options.webServer
   539     self.httpPort = options.httpPort
   540     self.sslPort = options.sslPort
   541     self.webSocketPort = options.webSocketPort
   543     # httpd-path is specified by standard makefile targets and may be specified
   544     # on the command line to select a particular version of httpd.js. If not
   545     # specified, try to select the one from hostutils.zip, as required in bug 882932.
   546     if not options.httpdPath:
   547       options.httpdPath = os.path.join(options.utilityPath, "components")
   549     self.startWebServer(options)
   550     self.startWebSocketServer(options, debuggerInfo)
   552     # start SSL pipe
   553     self.sslTunnel = SSLTunnel(options)
   554     self.sslTunnel.buildConfig(self.locations)
   555     self.sslTunnel.start()
   557     # If we're lucky, the server has fully started by now, and all paths are
   558     # ready, etc.  However, xpcshell cold start times suck, at least for debug
   559     # builds.  We'll try to connect to the server for awhile, and if we fail,
   560     # we'll try to kill the server and exit with an error.
   561     if self.server is not None:
   562       self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
   564   def stopServers(self):
   565     """Servers are no longer needed, and perhaps more importantly, anything they
   566         might spew to console might confuse things."""
   567     if self.server is not None:
   568       try:
   569         log.info('Stopping web server')
   570         self.server.stop()
   571       except Exception:
   572         log.exception('Exception when stopping web server')
   574     if self.wsserver is not None:
   575       try:
   576         log.info('Stopping web socket server')
   577         self.wsserver.stop()
   578       except Exception:
   579         log.exception('Exception when stopping web socket server');
   581     if self.sslTunnel is not None:
   582       try:
   583         log.info('Stopping ssltunnel')
   584         self.sslTunnel.stop()
   585       except Exception:
   586         log.exception('Exception stopping ssltunnel');
   588   def copyExtraFilesToProfile(self, options):
   589     "Copy extra files or dirs specified on the command line to the testing profile."
   590     for f in options.extraProfileFiles:
   591       abspath = self.getFullPath(f)
   592       if os.path.isfile(abspath):
   593         shutil.copy2(abspath, options.profilePath)
   594       elif os.path.isdir(abspath):
   595         dest = os.path.join(options.profilePath, os.path.basename(abspath))
   596         shutil.copytree(abspath, dest)
   597       else:
   598         log.warning("runtests.py | Failed to copy %s to profile", abspath)
   600   def installChromeJar(self, chrome, options):
   601     """
   602       copy mochijar directory to profile as an extension so we have chrome://mochikit for all harness code
   603     """
   604     # Write chrome.manifest.
   605     with open(os.path.join(options.profilePath, "extensions", "staged", "mochikit@mozilla.org", "chrome.manifest"), "a") as mfile:
   606       mfile.write(chrome)
   608   def addChromeToProfile(self, options):
   609     "Adds MochiKit chrome tests to the profile."
   611     # Create (empty) chrome directory.
   612     chromedir = os.path.join(options.profilePath, "chrome")
   613     os.mkdir(chromedir)
   615     # Write userChrome.css.
   616     chrome = """
   617 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
   618 toolbar,
   619 toolbarpalette {
   620   background-color: rgb(235, 235, 235) !important;
   621 }
   622 toolbar#nav-bar {
   623   background-image: none !important;
   624 }
   625 """
   626     with open(os.path.join(options.profilePath, "userChrome.css"), "a") as chromeFile:
   627       chromeFile.write(chrome)
   629     manifest = os.path.join(options.profilePath, "tests.manifest")
   630     with open(manifest, "w") as manifestFile:
   631       # Register chrome directory.
   632       chrometestDir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/"
   633       if mozinfo.isWin:
   634         chrometestDir = "file:///" + chrometestDir.replace("\\", "/")
   635       manifestFile.write("content mochitests %s contentaccessible=yes\n" % chrometestDir)
   637       if options.testingModulesDir is not None:
   638         manifestFile.write("resource testing-common file:///%s\n" %
   639           options.testingModulesDir)
   641     # Call installChromeJar().
   642     if not os.path.isdir(os.path.join(SCRIPT_DIR, self.jarDir)):
   643       log.testFail("invalid setup: missing mochikit extension")
   644       return None
   646     # Support Firefox (browser), B2G (shell), SeaMonkey (navigator), and Webapp
   647     # Runtime (webapp).
   648     chrome = ""
   649     if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome:
   650       chrome += """
   651 overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul
   652 overlay chrome://browser/content/shell.xhtml chrome://mochikit/content/browser-test-overlay.xul
   653 overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul
   654 overlay chrome://webapprt/content/webapp.xul chrome://mochikit/content/browser-test-overlay.xul
   655 """
   657     self.installChromeJar(chrome, options)
   658     return manifest
   660   def getExtensionsToInstall(self, options):
   661     "Return a list of extensions to install in the profile"
   662     extensions = options.extensionsToInstall or []
   663     appDir = options.app[:options.app.rfind(os.sep)] if options.app else options.utilityPath
   665     extensionDirs = [
   666       # Extensions distributed with the test harness.
   667       os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
   668     ]
   669     if appDir:
   670       # Extensions distributed with the application.
   671       extensionDirs.append(os.path.join(appDir, "distribution", "extensions"))
   673     for extensionDir in extensionDirs:
   674       if os.path.isdir(extensionDir):
   675         for dirEntry in os.listdir(extensionDir):
   676           if dirEntry not in options.extensionsToExclude:
   677             path = os.path.join(extensionDir, dirEntry)
   678             if os.path.isdir(path) or (os.path.isfile(path) and path.endswith(".xpi")):
   679               extensions.append(path)
   681     # append mochikit
   682     extensions.append(os.path.join(SCRIPT_DIR, self.jarDir))
   683     return extensions
   685 class SSLTunnel:
   686   def __init__(self, options):
   687     self.process = None
   688     self.utilityPath = options.utilityPath
   689     self.xrePath = options.xrePath
   690     self.certPath = options.certPath
   691     self.sslPort = options.sslPort
   692     self.httpPort = options.httpPort
   693     self.webServer = options.webServer
   694     self.webSocketPort = options.webSocketPort
   696     self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
   697     self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
   698     self.redirRE      = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
   700   def writeLocation(self, config, loc):
   701     for option in loc.options:
   702       match = self.customCertRE.match(option)
   703       if match:
   704         customcert = match.group("nickname");
   705         config.write("listen:%s:%s:%s:%s\n" %
   706                      (loc.host, loc.port, self.sslPort, customcert))
   708       match = self.clientAuthRE.match(option)
   709       if match:
   710         clientauth = match.group("clientauth");
   711         config.write("clientauth:%s:%s:%s:%s\n" %
   712                      (loc.host, loc.port, self.sslPort, clientauth))
   714       match = self.redirRE.match(option)
   715       if match:
   716         redirhost = match.group("redirhost")
   717         config.write("redirhost:%s:%s:%s:%s\n" %
   718                      (loc.host, loc.port, self.sslPort, redirhost))
   720   def buildConfig(self, locations):
   721     """Create the ssltunnel configuration file"""
   722     configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg")
   723     with os.fdopen(configFd, "w") as config:
   724       config.write("httpproxy:1\n")
   725       config.write("certdbdir:%s\n" % self.certPath)
   726       config.write("forward:127.0.0.1:%s\n" % self.httpPort)
   727       config.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
   728       config.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
   730       for loc in locations:
   731         if loc.scheme == "https" and "nocert" not in loc.options:
   732           self.writeLocation(config, loc)
   734   def start(self):
   735     """ Starts the SSL Tunnel """
   737     # start ssltunnel to provide https:// URLs capability
   738     bin_suffix = mozinfo.info.get('bin_suffix', '')
   739     ssltunnel = os.path.join(self.utilityPath, "ssltunnel" + bin_suffix)
   740     if not os.path.exists(ssltunnel):
   741       log.error("INFO | runtests.py | expected to find ssltunnel at %s", ssltunnel)
   742       exit(1)
   744     env = environment(xrePath=self.xrePath)
   745     self.process = mozprocess.ProcessHandler([ssltunnel, self.configFile],
   746                                                env=env)
   747     self.process.run()
   748     log.info("INFO | runtests.py | SSL tunnel pid: %d", self.process.pid)
   750   def stop(self):
   751     """ Stops the SSL Tunnel and cleans up """
   752     if self.process is not None:
   753       self.process.kill()
   754     if os.path.exists(self.configFile):
   755       os.remove(self.configFile)
   757 class Mochitest(MochitestUtilsMixin):
   758   certdbNew = False
   759   sslTunnel = None
   760   vmwareHelper = None
   761   DEFAULT_TIMEOUT = 60.0
   763   # XXX use automation.py for test name to avoid breaking legacy
   764   # TODO: replace this with 'runtests.py' or 'mochitest' or the like
   765   test_name = 'automation.py'
   767   def __init__(self):
   768     super(Mochitest, self).__init__()
   770     # environment function for browserEnv
   771     self.environment = environment
   773     # Max time in seconds to wait for server startup before tests will fail -- if
   774     # this seems big, it's mostly for debug machines where cold startup
   775     # (particularly after a build) takes forever.
   776     self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get('debug') else 90
   778     # metro browser sub process id
   779     self.browserProcessId = None
   782     self.haveDumpedScreen = False
   784   def extraPrefs(self, extraPrefs):
   785     """interpolate extra preferences from option strings"""
   787     try:
   788       return dict(parseKeyValue(extraPrefs, context='--setpref='))
   789     except KeyValueParseError, e:
   790       print str(e)
   791       sys.exit(1)
   793   def fillCertificateDB(self, options):
   794     # TODO: move -> mozprofile:
   795     # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35
   797     pwfilePath = os.path.join(options.profilePath, ".crtdbpw")
   798     with open(pwfilePath, "w") as pwfile:
   799       pwfile.write("\n")
   801     # Pre-create the certification database for the profile
   802     env = self.environment(xrePath=options.xrePath)
   803     bin_suffix = mozinfo.info.get('bin_suffix', '')
   804     certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix)
   805     pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix)
   807     if self.certdbNew:
   808       # android and b2g use the new DB formats exclusively
   809       certdbPath = "sql:" + options.profilePath
   810     else:
   811       # desktop seems to use the old
   812       certdbPath = options.profilePath
   814     status = call([certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=env)
   815     if status:
   816       return status
   818     # Walk the cert directory and add custom CAs and client certs
   819     files = os.listdir(options.certPath)
   820     for item in files:
   821       root, ext = os.path.splitext(item)
   822       if ext == ".ca":
   823         trustBits = "CT,,"
   824         if root.endswith("-object"):
   825           trustBits = "CT,,CT"
   826         call([certutil, "-A", "-i", os.path.join(options.certPath, item),
   827               "-d", certdbPath, "-f", pwfilePath, "-n", root, "-t", trustBits],
   828               env=env)
   829       elif ext == ".client":
   830         call([pk12util, "-i", os.path.join(options.certPath, item),
   831               "-w", pwfilePath, "-d", certdbPath],
   832               env=env)
   834     os.unlink(pwfilePath)
   835     return 0
   837   def buildProfile(self, options):
   838     """ create the profile and add optional chrome bits and files if requested """
   839     if options.browserChrome and options.timeout:
   840       options.extraPrefs.append("testing.browserTestHarness.timeout=%d" % options.timeout)
   841     options.extraPrefs.append("browser.tabs.remote=%s" % ('true' if options.e10s else 'false'))
   842     options.extraPrefs.append("browser.tabs.remote.autostart=%s" % ('true' if options.e10s else 'false'))
   844     # get extensions to install
   845     extensions = self.getExtensionsToInstall(options)
   847     # web apps
   848     appsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'webapps_mochitest.json')
   849     if os.path.exists(appsPath):
   850       with open(appsPath) as apps_file:
   851         apps = json.load(apps_file)
   852     else:
   853       apps = None
   855     # preferences
   856     prefsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'prefs_general.js')
   857     prefs = dict(Preferences.read_prefs(prefsPath))
   858     prefs.update(self.extraPrefs(options.extraPrefs))
   860     # interpolate preferences
   861     interpolation = {"server": "%s:%s" % (options.webServer, options.httpPort)}
   862     prefs = json.loads(json.dumps(prefs) % interpolation)
   863     for pref in prefs:
   864       prefs[pref] = Preferences.cast(prefs[pref])
   865     # TODO: make this less hacky
   866     # https://bugzilla.mozilla.org/show_bug.cgi?id=913152
   868     # proxy
   869     proxy = {'remote': options.webServer,
   870              'http': options.httpPort,
   871              'https': options.sslPort,
   872     # use SSL port for legacy compatibility; see
   873     # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
   874     # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
   875     # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
   876     #             'ws': str(self.webSocketPort)
   877              'ws': options.sslPort
   878              }
   881     # create a profile
   882     self.profile = Profile(profile=options.profilePath,
   883                            addons=extensions,
   884                            locations=self.locations,
   885                            preferences=prefs,
   886                            apps=apps,
   887                            proxy=proxy
   888                            )
   890     # Fix options.profilePath for legacy consumers.
   891     options.profilePath = self.profile.profile
   893     manifest = self.addChromeToProfile(options)
   894     self.copyExtraFilesToProfile(options)
   896     # create certificate database for the profile
   897     # TODO: this should really be upstreamed somewhere, maybe mozprofile
   898     certificateStatus = self.fillCertificateDB(options)
   899     if certificateStatus:
   900       log.info("TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed")
   901       return None
   903     return manifest
   905   def buildBrowserEnv(self, options, debugger=False):
   906     """build the environment variables for the specific test and operating system"""
   907     browserEnv = self.environment(xrePath=options.xrePath, debugger=debugger,
   908                                   dmdPath=options.dmdPath)
   910     # These variables are necessary for correct application startup; change
   911     # via the commandline at your own risk.
   912     browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
   914     # interpolate environment passed with options
   915     try:
   916       browserEnv.update(dict(parseKeyValue(options.environment, context='--setenv')))
   917     except KeyValueParseError, e:
   918       log.error(str(e))
   919       return
   921     browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
   923     if options.fatalAssertions:
   924       browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
   926     # Produce an NSPR log, is setup (see NSPR_LOG_MODULES global at the top of
   927     # this script).
   928     self.nsprLogs = NSPR_LOG_MODULES and "MOZ_UPLOAD_DIR" in os.environ
   929     if self.nsprLogs:
   930       browserEnv["NSPR_LOG_MODULES"] = NSPR_LOG_MODULES
   932       browserEnv["NSPR_LOG_FILE"] = "%s/nspr.log" % tempfile.gettempdir()
   933       browserEnv["GECKO_SEPARATE_NSPR_LOGS"] = "1"
   935     if debugger and not options.slowscript:
   936       browserEnv["JS_DISABLE_SLOW_SCRIPT_SIGNALS"] = "1"
   938     return browserEnv
   940   def cleanup(self, manifest, options):
   941     """ remove temporary files and profile """
   942     os.remove(manifest)
   943     del self.profile
   944     if options.pidFile != "":
   945       try:
   946         os.remove(options.pidFile)
   947         if os.path.exists(options.pidFile + ".xpcshell.pid"):
   948           os.remove(options.pidFile + ".xpcshell.pid")
   949       except:
   950         log.warn("cleaning up pidfile '%s' was unsuccessful from the test harness", options.pidFile)
   952   def dumpScreen(self, utilityPath):
   953     if self.haveDumpedScreen:
   954       log.info("Not taking screenshot here: see the one that was previously logged")
   955       return
   956     self.haveDumpedScreen = True
   957     dumpScreen(utilityPath)
   959   def killAndGetStack(self, processPID, utilityPath, debuggerInfo, dump_screen=False):
   960     """
   961     Kill the process, preferrably in a way that gets us a stack trace.
   962     Also attempts to obtain a screenshot before killing the process
   963     if specified.
   964     """
   966     if dump_screen:
   967       self.dumpScreen(utilityPath)
   969     if mozinfo.info.get('crashreporter', True) and not debuggerInfo:
   970       if mozinfo.isWin:
   971         # We should have a "crashinject" program in our utility path
   972         crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
   973         if os.path.exists(crashinject):
   974           status = subprocess.Popen([crashinject, str(processPID)]).wait()
   975           printstatus(status, "crashinject")
   976           if status == 0:
   977             return
   978       else:
   979         try:
   980           os.kill(processPID, signal.SIGABRT)
   981         except OSError:
   982           # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
   983           log.info("Can't trigger Breakpad, process no longer exists")
   984         return
   985     log.info("Can't trigger Breakpad, just killing process")
   986     killPid(processPID)
   988   def checkForZombies(self, processLog, utilityPath, debuggerInfo):
   989     """Look for hung processes"""
   991     if not os.path.exists(processLog):
   992       log.info('Automation Error: PID log not found: %s', processLog)
   993       # Whilst no hung process was found, the run should still display as a failure
   994       return True
   996     # scan processLog for zombies
   997     log.info('INFO | zombiecheck | Reading PID log: %s', processLog)
   998     processList = []
   999     pidRE = re.compile(r'launched child process (\d+)$')
  1000     with open(processLog) as processLogFD:
  1001       for line in processLogFD:
  1002         log.info(line.rstrip())
  1003         m = pidRE.search(line)
  1004         if m:
  1005           processList.append(int(m.group(1)))
  1007     # kill zombies
  1008     foundZombie = False
  1009     for processPID in processList:
  1010       log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID)
  1011       if isPidAlive(processPID):
  1012         foundZombie = True
  1013         log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID)
  1014         self.killAndGetStack(processPID, utilityPath, debuggerInfo, dump_screen=not debuggerInfo)
  1016     return foundZombie
  1018   def startVMwareRecording(self, options):
  1019     """ starts recording inside VMware VM using the recording helper dll """
  1020     assert mozinfo.isWin
  1021     from ctypes import cdll
  1022     self.vmwareHelper = cdll.LoadLibrary(self.vmwareHelperPath)
  1023     if self.vmwareHelper is None:
  1024       log.warning("runtests.py | Failed to load "
  1025                    "VMware recording helper")
  1026       return
  1027     log.info("runtests.py | Starting VMware recording.")
  1028     try:
  1029       self.vmwareHelper.StartRecording()
  1030     except Exception, e:
  1031       log.warning("runtests.py | Failed to start "
  1032                   "VMware recording: (%s)" % str(e))
  1033       self.vmwareHelper = None
  1035   def stopVMwareRecording(self):
  1036     """ stops recording inside VMware VM using the recording helper dll """
  1037     try:
  1038       assert mozinfo.isWin
  1039       if self.vmwareHelper is not None:
  1040         log.info("runtests.py | Stopping VMware recording.")
  1041         self.vmwareHelper.StopRecording()
  1042     except Exception, e:
  1043       log.warning("runtests.py | Failed to stop "
  1044                   "VMware recording: (%s)" % str(e))
  1045       log.exception('Error stopping VMWare recording')
  1047     self.vmwareHelper = None
  1049   def runApp(self,
  1050              testUrl,
  1051              env,
  1052              app,
  1053              profile,
  1054              extraArgs,
  1055              utilityPath,
  1056              debuggerInfo=None,
  1057              symbolsPath=None,
  1058              timeout=-1,
  1059              onLaunch=None,
  1060              webapprtChrome=False,
  1061              hide_subtests=False,
  1062              screenshotOnFail=False):
  1063     """
  1064     Run the app, log the duration it took to execute, return the status code.
  1065     Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
  1066     """
  1068     # debugger information
  1069     interactive = False
  1070     debug_args = None
  1071     if debuggerInfo:
  1072         interactive = debuggerInfo['interactive']
  1073         debug_args = [debuggerInfo['path']] + debuggerInfo['args']
  1075     # fix default timeout
  1076     if timeout == -1:
  1077       timeout = self.DEFAULT_TIMEOUT
  1079     # build parameters
  1080     is_test_build = mozinfo.info.get('tests_enabled', True)
  1081     bin_suffix = mozinfo.info.get('bin_suffix', '')
  1083     # copy env so we don't munge the caller's environment
  1084     env = env.copy()
  1086     # make sure we clean up after ourselves.
  1087     try:
  1088       # set process log environment variable
  1089       tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
  1090       os.close(tmpfd)
  1091       env["MOZ_PROCESS_LOG"] = processLog
  1093       if interactive:
  1094         # If an interactive debugger is attached,
  1095         # don't use timeouts, and don't capture ctrl-c.
  1096         timeout = None
  1097         signal.signal(signal.SIGINT, lambda sigid, frame: None)
  1099       # build command line
  1100       cmd = os.path.abspath(app)
  1101       args = list(extraArgs)
  1102       # TODO: mozrunner should use -foreground at least for mac
  1103       # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
  1104       args.append('-foreground')
  1105       if testUrl:
  1106         if debuggerInfo and debuggerInfo['requiresEscapedArgs']:
  1107           testUrl = testUrl.replace("&", "\\&")
  1108         args.append(testUrl)
  1110       if mozinfo.info["debug"] and not webapprtChrome:
  1111         shutdownLeaks = ShutdownLeaks(log.info)
  1112       else:
  1113         shutdownLeaks = None
  1115       # create an instance to process the output
  1116       outputHandler = self.OutputHandler(harness=self,
  1117                                          utilityPath=utilityPath,
  1118                                          symbolsPath=symbolsPath,
  1119                                          dump_screen_on_timeout=not debuggerInfo,
  1120                                          dump_screen_on_fail=screenshotOnFail,
  1121                                          hide_subtests=hide_subtests,
  1122                                          shutdownLeaks=shutdownLeaks,
  1125       def timeoutHandler():
  1126         outputHandler.log_output_buffer()
  1127         browserProcessId = outputHandler.browserProcessId
  1128         self.handleTimeout(timeout, proc, utilityPath, debuggerInfo, browserProcessId)
  1129       kp_kwargs = {'kill_on_timeout': False,
  1130                    'cwd': SCRIPT_DIR,
  1131                    'onTimeout': [timeoutHandler]}
  1132       kp_kwargs['processOutputLine'] = [outputHandler]
  1134       # create mozrunner instance and start the system under test process
  1135       self.lastTestSeen = self.test_name
  1136       startTime = datetime.now()
  1138       # b2g desktop requires FirefoxRunner even though appname is b2g
  1139       if mozinfo.info.get('appname') == 'b2g' and mozinfo.info.get('toolkit') != 'gonk':
  1140           runner_cls = mozrunner.FirefoxRunner
  1141       else:
  1142           runner_cls = mozrunner.runners.get(mozinfo.info.get('appname', 'firefox'),
  1143                                              mozrunner.Runner)
  1144       runner = runner_cls(profile=self.profile,
  1145                           binary=cmd,
  1146                           cmdargs=args,
  1147                           env=env,
  1148                           process_class=mozprocess.ProcessHandlerMixin,
  1149                           kp_kwargs=kp_kwargs,
  1152       # XXX work around bug 898379 until mozrunner is updated for m-c; see
  1153       # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c49
  1154       runner.kp_kwargs = kp_kwargs
  1156       # start the runner
  1157       runner.start(debug_args=debug_args,
  1158                    interactive=interactive,
  1159                    outputTimeout=timeout)
  1160       proc = runner.process_handler
  1161       log.info("INFO | runtests.py | Application pid: %d", proc.pid)
  1163       if onLaunch is not None:
  1164         # Allow callers to specify an onLaunch callback to be fired after the
  1165         # app is launched.
  1166         # We call onLaunch for b2g desktop mochitests so that we can
  1167         # run a Marionette script after gecko has completed startup.
  1168         onLaunch()
  1170       # wait until app is finished
  1171       # XXX copy functionality from
  1172       # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61
  1173       # until bug 913970 is fixed regarding mozrunner `wait` not returning status
  1174       # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970
  1175       status = proc.wait()
  1176       printstatus(status, "Main app process")
  1177       runner.process_handler = None
  1179       if timeout is None:
  1180         didTimeout = False
  1181       else:
  1182         didTimeout = proc.didTimeout
  1184       # finalize output handler
  1185       outputHandler.finish(didTimeout)
  1187       # record post-test information
  1188       if status:
  1189         log.info("TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s", self.lastTestSeen, status)
  1190       else:
  1191         self.lastTestSeen = 'Main app process exited normally'
  1193       log.info("INFO | runtests.py | Application ran for: %s", str(datetime.now() - startTime))
  1195       # Do a final check for zombie child processes.
  1196       zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo)
  1198       # check for crashes
  1199       minidump_path = os.path.join(self.profile.profile, "minidumps")
  1200       crashed = mozcrash.check_for_crashes(minidump_path,
  1201                                            symbolsPath,
  1202                                            test_name=self.lastTestSeen)
  1204       if crashed or zombieProcesses:
  1205         status = 1
  1207     finally:
  1208       # cleanup
  1209       if os.path.exists(processLog):
  1210         os.remove(processLog)
  1212     return status
  1214   def runTests(self, options, onLaunch=None):
  1215     """ Prepare, configure, run tests and cleanup """
  1217     # get debugger info, a dict of:
  1218     # {'path': path to the debugger (string),
  1219     #  'interactive': whether the debugger is interactive or not (bool)
  1220     #  'args': arguments to the debugger (list)
  1221     # TODO: use mozrunner.local.debugger_arguments:
  1222     # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
  1223     debuggerInfo = getDebuggerInfo(self.oldcwd,
  1224                                    options.debugger,
  1225                                    options.debuggerArgs,
  1226                                    options.debuggerInteractive)
  1228     self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
  1230     browserEnv = self.buildBrowserEnv(options, debuggerInfo is not None)
  1231     if browserEnv is None:
  1232       return 1
  1234     # buildProfile sets self.profile .
  1235     # This relies on sideeffects and isn't very stateful:
  1236     # https://bugzilla.mozilla.org/show_bug.cgi?id=919300
  1237     manifest = self.buildProfile(options)
  1238     if manifest is None:
  1239       return 1
  1241     try:
  1242       self.startServers(options, debuggerInfo)
  1244       testURL = self.buildTestPath(options)
  1245       self.buildURLOptions(options, browserEnv)
  1246       if self.urlOpts:
  1247         testURL += "?" + "&".join(self.urlOpts)
  1249       if options.webapprtContent:
  1250         options.browserArgs.extend(('-test-mode', testURL))
  1251         testURL = None
  1253       if options.immersiveMode:
  1254         options.browserArgs.extend(('-firefoxpath', options.app))
  1255         options.app = self.immersiveHelperPath
  1257       if options.jsdebugger:
  1258         options.browserArgs.extend(['-jsdebugger'])
  1260       # Remove the leak detection file so it can't "leak" to the tests run.
  1261       # The file is not there if leak logging was not enabled in the application build.
  1262       if os.path.exists(self.leak_report_file):
  1263         os.remove(self.leak_report_file)
  1265       # then again to actually run mochitest
  1266       if options.timeout:
  1267         timeout = options.timeout + 30
  1268       elif options.debugger or not options.autorun:
  1269         timeout = None
  1270       else:
  1271         timeout = 330.0 # default JS harness timeout is 300 seconds
  1273       if options.vmwareRecording:
  1274         self.startVMwareRecording(options);
  1276       log.info("runtests.py | Running tests: start.\n")
  1277       try:
  1278         status = self.runApp(testURL,
  1279                              browserEnv,
  1280                              options.app,
  1281                              profile=self.profile,
  1282                              extraArgs=options.browserArgs,
  1283                              utilityPath=options.utilityPath,
  1284                              debuggerInfo=debuggerInfo,
  1285                              symbolsPath=options.symbolsPath,
  1286                              timeout=timeout,
  1287                              onLaunch=onLaunch,
  1288                              webapprtChrome=options.webapprtChrome,
  1289                              hide_subtests=options.hide_subtests,
  1290                              screenshotOnFail=options.screenshotOnFail
  1292       except KeyboardInterrupt:
  1293         log.info("runtests.py | Received keyboard interrupt.\n");
  1294         status = -1
  1295       except:
  1296         traceback.print_exc()
  1297         log.error("Automation Error: Received unexpected exception while running application\n")
  1298         status = 1
  1300     finally:
  1301       if options.vmwareRecording:
  1302         self.stopVMwareRecording();
  1303       self.stopServers()
  1305     processLeakLog(self.leak_report_file, options.leakThreshold)
  1307     if self.nsprLogs:
  1308       with zipfile.ZipFile("%s/nsprlog.zip" % browserEnv["MOZ_UPLOAD_DIR"], "w", zipfile.ZIP_DEFLATED) as logzip:
  1309         for logfile in glob.glob("%s/nspr*.log*" % tempfile.gettempdir()):
  1310           logzip.write(logfile)
  1311           os.remove(logfile)
  1313     log.info("runtests.py | Running tests: end.")
  1315     if manifest is not None:
  1316       self.cleanup(manifest, options)
  1318     return status
  1320   def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo, browserProcessId):
  1321     """handle process output timeout"""
  1322     # TODO: bug 913975 : _processOutput should call self.processOutputLine one more time one timeout (I think)
  1323     log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
  1324     browserProcessId = browserProcessId or proc.pid
  1325     self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo, dump_screen=not debuggerInfo)
  1327   ### output processing
  1329   class OutputHandler(object):
  1330     """line output handler for mozrunner"""
  1331     def __init__(self, harness, utilityPath, symbolsPath=None, dump_screen_on_timeout=True, dump_screen_on_fail=False,
  1332                  hide_subtests=False, shutdownLeaks=None):
  1333       """
  1334       harness -- harness instance
  1335       dump_screen_on_timeout -- whether to dump the screen on timeout
  1336       """
  1337       self.harness = harness
  1338       self.output_buffer = []
  1339       self.running_test = False
  1340       self.utilityPath = utilityPath
  1341       self.symbolsPath = symbolsPath
  1342       self.dump_screen_on_timeout = dump_screen_on_timeout
  1343       self.dump_screen_on_fail = dump_screen_on_fail
  1344       self.hide_subtests = hide_subtests
  1345       self.shutdownLeaks = shutdownLeaks
  1347       # perl binary to use
  1348       self.perl = which('perl')
  1350       # With metro browser runs this script launches the metro test harness which launches the browser.
  1351       # The metro test harness hands back the real browser process id via log output which we need to
  1352       # pick up on and parse out. This variable tracks the real browser process id if we find it.
  1353       self.browserProcessId = None
  1355       # stack fixer function and/or process
  1356       self.stackFixerFunction, self.stackFixerProcess = self.stackFixer()
  1358     def processOutputLine(self, line):
  1359       """per line handler of output for mozprocess"""
  1360       for handler in self.outputHandlers():
  1361         line = handler(line)
  1362     __call__ = processOutputLine
  1364     def outputHandlers(self):
  1365       """returns ordered list of output handlers"""
  1366       return [self.fix_stack,
  1367               self.format,
  1368               self.dumpScreenOnTimeout,
  1369               self.dumpScreenOnFail,
  1370               self.metro_subprocess_id,
  1371               self.trackShutdownLeaks,
  1372               self.check_test_failure,
  1373               self.log,
  1374               self.record_last_test,
  1377     def stackFixer(self):
  1378       """
  1379       return 2-tuple, (stackFixerFunction, StackFixerProcess),
  1380       if any, to use on the output lines
  1381       """
  1383       if not mozinfo.info.get('debug'):
  1384         return None, None
  1386       stackFixerFunction = stackFixerProcess = None
  1388       def import_stackFixerModule(module_name):
  1389         sys.path.insert(0, self.utilityPath)
  1390         module = __import__(module_name, globals(), locals(), [])
  1391         sys.path.pop(0)
  1392         return module
  1394       if self.symbolsPath and os.path.exists(self.symbolsPath):
  1395         # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
  1396         # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
  1397         stackFixerModule = import_stackFixerModule('fix_stack_using_bpsyms')
  1398         stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, self.symbolsPath)
  1400       elif mozinfo.isLinux and self.perl:
  1401         # Run logsource through fix-linux-stack.pl (uses addr2line)
  1402         # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
  1403         stackFixerCommand = [self.perl, os.path.join(self.utilityPath, "fix-linux-stack.pl")]
  1404         stackFixerProcess = subprocess.Popen(stackFixerCommand, stdin=subprocess.PIPE,
  1405                                              stdout=subprocess.PIPE)
  1406         def fixFunc(line):
  1407           stackFixerProcess.stdin.write(line + '\n')
  1408           return stackFixerProcess.stdout.readline().rstrip()
  1410         stackFixerFunction = fixFunc
  1412       return (stackFixerFunction, stackFixerProcess)
  1414     def finish(self, didTimeout):
  1415       if self.stackFixerProcess:
  1416         self.stackFixerProcess.communicate()
  1417         status = self.stackFixerProcess.returncode
  1418         if status and not didTimeout:
  1419           log.info("TEST-UNEXPECTED-FAIL | runtests.py | Stack fixer process exited with code %d during test run", status)
  1421       if self.shutdownLeaks:
  1422         self.shutdownLeaks.process()
  1424     def log_output_buffer(self):
  1425         if self.output_buffer:
  1426             lines = ['  %s' % line for line in self.output_buffer]
  1427             log.info("Buffered test output:\n%s" % '\n'.join(lines))
  1429     # output line handlers:
  1430     # these take a line and return a line
  1432     def fix_stack(self, line):
  1433       if self.stackFixerFunction:
  1434         return self.stackFixerFunction(line)
  1435       return line
  1437     def format(self, line):
  1438       """format the line"""
  1439       return line.rstrip().decode("UTF-8", "ignore")
  1441     def dumpScreenOnTimeout(self, line):
  1442       if not self.dump_screen_on_fail and self.dump_screen_on_timeout and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
  1443         self.log_output_buffer()
  1444         self.harness.dumpScreen(self.utilityPath)
  1445       return line
  1447     def dumpScreenOnFail(self, line):
  1448       if self.dump_screen_on_fail and "TEST-UNEXPECTED-FAIL" in line:
  1449         self.log_output_buffer()
  1450         self.harness.dumpScreen(self.utilityPath)
  1451       return line
  1453     def metro_subprocess_id(self, line):
  1454       """look for metro browser subprocess id"""
  1455       if "METRO_BROWSER_PROCESS" in line:
  1456         index = line.find("=")
  1457         if index != -1:
  1458           self.browserProcessId = line[index+1:].rstrip()
  1459           log.info("INFO | runtests.py | metro browser sub process id detected: %s", self.browserProcessId)
  1460       return line
  1462     def trackShutdownLeaks(self, line):
  1463       if self.shutdownLeaks:
  1464         self.shutdownLeaks.log(line)
  1465       return line
  1467     def check_test_failure(self, line):
  1468       if 'TEST-END' in line:
  1469         self.running_test = False
  1470         if any('TEST-UNEXPECTED' in l for l in self.output_buffer):
  1471           self.log_output_buffer()
  1472       return line
  1474     def log(self, line):
  1475       if self.hide_subtests and self.running_test:
  1476         self.output_buffer.append(line)
  1477       else:
  1478         # hack to make separators align nicely, remove when we use mozlog
  1479         if self.hide_subtests and 'TEST-END' in line:
  1480             index = line.index('TEST-END') + len('TEST-END')
  1481             line = line[:index] + ' ' * (len('TEST-START')-len('TEST-END')) + line[index:]
  1482         log.info(line)
  1483       return line
  1485     def record_last_test(self, line):
  1486       """record last test on harness"""
  1487       if "TEST-START" in line and "|" in line:
  1488         if not line.endswith('Shutdown'):
  1489           self.output_buffer = []
  1490           self.running_test = True
  1491         self.harness.lastTestSeen = line.split("|")[1].strip()
  1492       return line
  1495   def makeTestConfig(self, options):
  1496     "Creates a test configuration file for customizing test execution."
  1497     options.logFile = options.logFile.replace("\\", "\\\\")
  1498     options.testPath = options.testPath.replace("\\", "\\\\")
  1499     testRoot = self.getTestRoot(options)
  1501     if "MOZ_HIDE_RESULTS_TABLE" in os.environ and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1":
  1502       options.hideResultsTable = True
  1504     d = dict(options.__dict__)
  1505     d['testRoot'] = testRoot
  1506     content = json.dumps(d)
  1508     with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
  1509       config.write(content)
  1511   def installExtensionFromPath(self, options, path, extensionID = None):
  1512     """install an extension to options.profilePath"""
  1514     # TODO: currently extensionID is unused; see
  1515     # https://bugzilla.mozilla.org/show_bug.cgi?id=914267
  1516     # [mozprofile] make extensionID a parameter to install_from_path
  1517     # https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/addons.py#L169
  1519     extensionPath = self.getFullPath(path)
  1521     log.info("runtests.py | Installing extension at %s to %s." %
  1522                 (extensionPath, options.profilePath))
  1524     addons = AddonManager(options.profilePath)
  1526     # XXX: del the __del__
  1527     # hack can be removed when mozprofile is mirrored to m-c ; see
  1528     # https://bugzilla.mozilla.org/show_bug.cgi?id=911218 :
  1529     # [mozprofile] AddonManager should only cleanup on __del__ optionally:
  1530     # https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/addons.py#L266
  1531     if hasattr(addons, '__del__'):
  1532       del addons.__del__
  1534     addons.install_from_path(path)
  1536   def installExtensionsToProfile(self, options):
  1537     "Install special testing extensions, application distributed extensions, and specified on the command line ones to testing profile."
  1538     for path in self.getExtensionsToInstall(options):
  1539       self.installExtensionFromPath(options, path)
  1542 def main():
  1544   # parse command line options
  1545   mochitest = Mochitest()
  1546   parser = MochitestOptions()
  1547   options, args = parser.parse_args()
  1548   options = parser.verifyOptions(options, mochitest)
  1549   if options is None:
  1550     # parsing error
  1551     sys.exit(1)
  1553   options.utilityPath = mochitest.getFullPath(options.utilityPath)
  1554   options.certPath = mochitest.getFullPath(options.certPath)
  1555   if options.symbolsPath and not isURL(options.symbolsPath):
  1556     options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
  1558   sys.exit(mochitest.runTests(options))
  1560 if __name__ == "__main__":
  1561   main()

mercurial