michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: """ michael@0: Runs the Mochitest test harness. michael@0: """ michael@0: michael@0: from __future__ import with_statement michael@0: import os michael@0: import sys michael@0: SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) michael@0: sys.path.insert(0, SCRIPT_DIR); michael@0: michael@0: import glob michael@0: import json michael@0: import mozcrash michael@0: import mozinfo michael@0: import mozprocess michael@0: import mozrunner michael@0: import optparse michael@0: import re michael@0: import shutil michael@0: import signal michael@0: import subprocess michael@0: import tempfile michael@0: import time michael@0: import traceback michael@0: import urllib2 michael@0: import zipfile michael@0: michael@0: from automationutils import environment, getDebuggerInfo, isURL, KeyValueParseError, parseKeyValue, processLeakLog, systemMemory, dumpScreen, ShutdownLeaks, printstatus michael@0: from datetime import datetime michael@0: from manifestparser import TestManifest michael@0: from mochitest_options import MochitestOptions michael@0: from mozprofile import Profile, Preferences michael@0: from mozprofile.permissions import ServerLocations michael@0: from urllib import quote_plus as encodeURIComponent michael@0: michael@0: # This should use the `which` module already in tree, but it is michael@0: # not yet present in the mozharness environment michael@0: from mozrunner.utils import findInPath as which michael@0: michael@0: # set up logging handler a la automation.py.in for compatability michael@0: import logging michael@0: log = logging.getLogger() michael@0: def resetGlobalLog(): michael@0: while log.handlers: michael@0: log.removeHandler(log.handlers[0]) michael@0: handler = logging.StreamHandler(sys.stdout) michael@0: log.setLevel(logging.INFO) michael@0: log.addHandler(handler) michael@0: resetGlobalLog() michael@0: michael@0: ########################### michael@0: # Option for NSPR logging # michael@0: ########################### michael@0: michael@0: # Set the desired log modules you want an NSPR log be produced by a try run for, or leave blank to disable the feature. michael@0: # This will be passed to NSPR_LOG_MODULES environment variable. Try run will then put a download link for the log file michael@0: # on tbpl.mozilla.org. michael@0: michael@0: NSPR_LOG_MODULES = "" michael@0: michael@0: #################### michael@0: # PROCESS HANDLING # michael@0: #################### michael@0: michael@0: def call(*args, **kwargs): michael@0: """front-end function to mozprocess.ProcessHandler""" michael@0: # TODO: upstream -> mozprocess michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=791383 michael@0: process = mozprocess.ProcessHandler(*args, **kwargs) michael@0: process.run() michael@0: return process.wait() michael@0: michael@0: def killPid(pid): michael@0: # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58 michael@0: try: michael@0: os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM)) michael@0: except Exception, e: michael@0: log.info("Failed to kill process %d: %s", pid, str(e)) michael@0: michael@0: if mozinfo.isWin: michael@0: import ctypes, ctypes.wintypes, time, msvcrt michael@0: michael@0: def isPidAlive(pid): michael@0: STILL_ACTIVE = 259 michael@0: PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 michael@0: pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) michael@0: if not pHandle: michael@0: return False michael@0: pExitCode = ctypes.wintypes.DWORD() michael@0: ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode)) michael@0: ctypes.windll.kernel32.CloseHandle(pHandle) michael@0: return pExitCode.value == STILL_ACTIVE michael@0: michael@0: else: michael@0: import errno michael@0: michael@0: def isPidAlive(pid): michael@0: try: michael@0: # kill(pid, 0) checks for a valid PID without actually sending a signal michael@0: # The method throws OSError if the PID is invalid, which we catch below. michael@0: os.kill(pid, 0) michael@0: michael@0: # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if michael@0: # the process terminates before we get to this point. michael@0: wpid, wstatus = os.waitpid(pid, os.WNOHANG) michael@0: return wpid == 0 michael@0: except OSError, err: michael@0: # Catch the errors we might expect from os.kill/os.waitpid, michael@0: # and re-raise any others michael@0: if err.errno == errno.ESRCH or err.errno == errno.ECHILD: michael@0: return False michael@0: raise michael@0: # TODO: ^ upstream isPidAlive to mozprocess michael@0: michael@0: ####################### michael@0: # HTTP SERVER SUPPORT # michael@0: ####################### michael@0: michael@0: class MochitestServer(object): michael@0: "Web server used to serve Mochitests, for closer fidelity to the real web." michael@0: michael@0: def __init__(self, options): michael@0: if isinstance(options, optparse.Values): michael@0: options = vars(options) michael@0: self._closeWhenDone = options['closeWhenDone'] michael@0: self._utilityPath = options['utilityPath'] michael@0: self._xrePath = options['xrePath'] michael@0: self._profileDir = options['profilePath'] michael@0: self.webServer = options['webServer'] michael@0: self.httpPort = options['httpPort'] michael@0: self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort } michael@0: self.testPrefix = "'webapprt_'" if options.get('webapprtContent') else "undefined" michael@0: michael@0: if options.get('httpdPath'): michael@0: self._httpdPath = options['httpdPath'] michael@0: else: michael@0: self._httpdPath = SCRIPT_DIR michael@0: self._httpdPath = os.path.abspath(self._httpdPath) michael@0: michael@0: def start(self): michael@0: "Run the Mochitest server, returning the process ID of the server." michael@0: michael@0: # get testing environment michael@0: env = environment(xrePath=self._xrePath) michael@0: env["XPCOM_DEBUG_BREAK"] = "warn" michael@0: michael@0: # When running with an ASan build, our xpcshell server will also be ASan-enabled, michael@0: # thus consuming too much resources when running together with the browser on michael@0: # the test slaves. Try to limit the amount of resources by disabling certain michael@0: # features. michael@0: env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5" michael@0: michael@0: if mozinfo.isWin: michael@0: env["PATH"] = env["PATH"] + ";" + str(self._xrePath) michael@0: michael@0: args = ["-g", self._xrePath, michael@0: "-v", "170", michael@0: "-f", os.path.join(self._httpdPath, "httpd.js"), michael@0: "-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;""" % michael@0: {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer, michael@0: "testPrefix" : self.testPrefix, "displayResults" : str(not self._closeWhenDone).lower() }, michael@0: "-f", os.path.join(SCRIPT_DIR, "server.js")] michael@0: michael@0: xpcshell = os.path.join(self._utilityPath, michael@0: "xpcshell" + mozinfo.info['bin_suffix']) michael@0: command = [xpcshell] + args michael@0: self._process = mozprocess.ProcessHandler(command, cwd=SCRIPT_DIR, env=env) michael@0: self._process.run() michael@0: log.info("%s : launching %s", self.__class__.__name__, command) michael@0: pid = self._process.pid michael@0: log.info("runtests.py | Server pid: %d", pid) michael@0: michael@0: def ensureReady(self, timeout): michael@0: assert timeout >= 0 michael@0: michael@0: aliveFile = os.path.join(self._profileDir, "server_alive.txt") michael@0: i = 0 michael@0: while i < timeout: michael@0: if os.path.exists(aliveFile): michael@0: break michael@0: time.sleep(1) michael@0: i += 1 michael@0: else: michael@0: log.error("TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup.") michael@0: self.stop() michael@0: sys.exit(1) michael@0: michael@0: def stop(self): michael@0: try: michael@0: with urllib2.urlopen(self.shutdownURL) as c: michael@0: c.read() michael@0: michael@0: # TODO: need ProcessHandler.poll() michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=912285 michael@0: # rtncode = self._process.poll() michael@0: rtncode = self._process.proc.poll() michael@0: if rtncode is None: michael@0: # TODO: need ProcessHandler.terminate() and/or .send_signal() michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=912285 michael@0: # self._process.terminate() michael@0: self._process.proc.terminate() michael@0: except: michael@0: self._process.kill() michael@0: michael@0: class WebSocketServer(object): michael@0: "Class which encapsulates the mod_pywebsocket server" michael@0: michael@0: def __init__(self, options, scriptdir, debuggerInfo=None): michael@0: self.port = options.webSocketPort michael@0: self._scriptdir = scriptdir michael@0: self.debuggerInfo = debuggerInfo michael@0: michael@0: def start(self): michael@0: # Invoke pywebsocket through a wrapper which adds special SIGINT handling. michael@0: # michael@0: # If we're in an interactive debugger, the wrapper causes the server to michael@0: # ignore SIGINT so the server doesn't capture a ctrl+c meant for the michael@0: # debugger. michael@0: # michael@0: # If we're not in an interactive debugger, the wrapper causes the server to michael@0: # die silently upon receiving a SIGINT. michael@0: scriptPath = 'pywebsocket_wrapper.py' michael@0: script = os.path.join(self._scriptdir, scriptPath) michael@0: michael@0: cmd = [sys.executable, script] michael@0: if self.debuggerInfo and self.debuggerInfo['interactive']: michael@0: cmd += ['--interactive'] michael@0: cmd += ['-p', str(self.port), '-w', self._scriptdir, '-l', \ michael@0: os.path.join(self._scriptdir, "websock.log"), \ michael@0: '--log-level=debug', '--allow-handlers-outside-root-dir'] michael@0: # start the process michael@0: self._process = mozprocess.ProcessHandler(cmd, cwd=SCRIPT_DIR) michael@0: self._process.run() michael@0: pid = self._process.pid michael@0: log.info("runtests.py | Websocket server pid: %d", pid) michael@0: michael@0: def stop(self): michael@0: self._process.kill() michael@0: michael@0: class MochitestUtilsMixin(object): michael@0: """ michael@0: Class containing some utility functions common to both local and remote michael@0: mochitest runners michael@0: """ michael@0: michael@0: # TODO Utility classes are a code smell. This class is temporary michael@0: # and should be removed when desktop mochitests are refactored michael@0: # on top of mozbase. Each of the functions in here should michael@0: # probably live somewhere in mozbase michael@0: michael@0: oldcwd = os.getcwd() michael@0: jarDir = 'mochijar' michael@0: michael@0: # Path to the test script on the server michael@0: TEST_PATH = "tests" michael@0: CHROME_PATH = "redirect.html" michael@0: urlOpts = [] michael@0: michael@0: def __init__(self): michael@0: self.update_mozinfo() michael@0: self.server = None michael@0: self.wsserver = None michael@0: self.sslTunnel = None michael@0: self._locations = None michael@0: michael@0: def update_mozinfo(self): michael@0: """walk up directories to find mozinfo.json update the info""" michael@0: # TODO: This should go in a more generic place, e.g. mozinfo michael@0: michael@0: path = SCRIPT_DIR michael@0: dirs = set() michael@0: while path != os.path.expanduser('~'): michael@0: if path in dirs: michael@0: break michael@0: dirs.add(path) michael@0: path = os.path.split(path)[0] michael@0: michael@0: mozinfo.find_and_update_from_json(*dirs) michael@0: michael@0: def getFullPath(self, path): michael@0: " Get an absolute path relative to self.oldcwd." michael@0: return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path))) michael@0: michael@0: def getLogFilePath(self, logFile): michael@0: """ return the log file path relative to the device we are testing on, in most cases michael@0: it will be the full path on the local system michael@0: """ michael@0: return self.getFullPath(logFile) michael@0: michael@0: @property michael@0: def locations(self): michael@0: if self._locations is not None: michael@0: return self._locations michael@0: locations_file = os.path.join(SCRIPT_DIR, 'server-locations.txt') michael@0: self._locations = ServerLocations(locations_file) michael@0: return self._locations michael@0: michael@0: def buildURLOptions(self, options, env): michael@0: """ Add test control options from the command line to the url michael@0: michael@0: URL parameters to test URL: michael@0: michael@0: autorun -- kick off tests automatically michael@0: closeWhenDone -- closes the browser after the tests michael@0: hideResultsTable -- hides the table of individual test results michael@0: logFile -- logs test run to an absolute path michael@0: totalChunks -- how many chunks to split tests into michael@0: thisChunk -- which chunk to run michael@0: startAt -- name of test to start at michael@0: endAt -- name of test to end at michael@0: timeout -- per-test timeout in seconds michael@0: repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice. michael@0: """ michael@0: michael@0: # allow relative paths for logFile michael@0: if options.logFile: michael@0: options.logFile = self.getLogFilePath(options.logFile) michael@0: michael@0: # Note that all tests under options.subsuite need to be browser chrome tests. michael@0: if options.browserChrome or options.chrome or options.subsuite or \ michael@0: options.a11y or options.webapprtChrome: michael@0: self.makeTestConfig(options) michael@0: else: michael@0: if options.autorun: michael@0: self.urlOpts.append("autorun=1") michael@0: if options.timeout: michael@0: self.urlOpts.append("timeout=%d" % options.timeout) michael@0: if options.closeWhenDone: michael@0: self.urlOpts.append("closeWhenDone=1") michael@0: if options.logFile: michael@0: self.urlOpts.append("logFile=" + encodeURIComponent(options.logFile)) michael@0: self.urlOpts.append("fileLevel=" + encodeURIComponent(options.fileLevel)) michael@0: if options.consoleLevel: michael@0: self.urlOpts.append("consoleLevel=" + encodeURIComponent(options.consoleLevel)) michael@0: if options.totalChunks: michael@0: self.urlOpts.append("totalChunks=%d" % options.totalChunks) michael@0: self.urlOpts.append("thisChunk=%d" % options.thisChunk) michael@0: if options.chunkByDir: michael@0: self.urlOpts.append("chunkByDir=%d" % options.chunkByDir) michael@0: if options.startAt: michael@0: self.urlOpts.append("startAt=%s" % options.startAt) michael@0: if options.endAt: michael@0: self.urlOpts.append("endAt=%s" % options.endAt) michael@0: if options.shuffle: michael@0: self.urlOpts.append("shuffle=1") michael@0: if "MOZ_HIDE_RESULTS_TABLE" in env and env["MOZ_HIDE_RESULTS_TABLE"] == "1": michael@0: self.urlOpts.append("hideResultsTable=1") michael@0: if options.runUntilFailure: michael@0: self.urlOpts.append("runUntilFailure=1") michael@0: if options.repeat: michael@0: self.urlOpts.append("repeat=%d" % options.repeat) michael@0: if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, options.testPath)) and options.repeat > 0: michael@0: self.urlOpts.append("testname=%s" % ("/").join([self.TEST_PATH, options.testPath])) michael@0: if options.testManifest: michael@0: self.urlOpts.append("testManifest=%s" % options.testManifest) michael@0: if hasattr(options, 'runOnly') and options.runOnly: michael@0: self.urlOpts.append("runOnly=true") michael@0: else: michael@0: self.urlOpts.append("runOnly=false") michael@0: if options.manifestFile: michael@0: self.urlOpts.append("manifestFile=%s" % options.manifestFile) michael@0: if options.failureFile: michael@0: self.urlOpts.append("failureFile=%s" % self.getFullPath(options.failureFile)) michael@0: if options.runSlower: michael@0: self.urlOpts.append("runSlower=true") michael@0: if options.debugOnFailure: michael@0: self.urlOpts.append("debugOnFailure=true") michael@0: if options.dumpOutputDirectory: michael@0: self.urlOpts.append("dumpOutputDirectory=%s" % encodeURIComponent(options.dumpOutputDirectory)) michael@0: if options.dumpAboutMemoryAfterTest: michael@0: self.urlOpts.append("dumpAboutMemoryAfterTest=true") michael@0: if options.dumpDMDAfterTest: michael@0: self.urlOpts.append("dumpDMDAfterTest=true") michael@0: if options.quiet: michael@0: self.urlOpts.append("quiet=true") michael@0: michael@0: def getTestFlavor(self, options): michael@0: if options.browserChrome: michael@0: return "browser-chrome" michael@0: elif options.chrome: michael@0: return "chrome" michael@0: elif options.a11y: michael@0: return "a11y" michael@0: elif options.webapprtChrome: michael@0: return "webapprt-chrome" michael@0: else: michael@0: return "mochitest" michael@0: michael@0: # This check can be removed when bug 983867 is fixed. michael@0: def isTest(self, options, filename): michael@0: allow_js_css = False michael@0: if options.browserChrome: michael@0: allow_js_css = True michael@0: testPattern = re.compile(r"browser_.+\.js") michael@0: elif options.chrome or options.a11y: michael@0: testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)") michael@0: elif options.webapprtContent: michael@0: testPattern = re.compile(r"webapprt_") michael@0: elif options.webapprtChrome: michael@0: allow_js_css = True michael@0: testPattern = re.compile(r"browser_") michael@0: else: michael@0: testPattern = re.compile(r"test_") michael@0: michael@0: if not allow_js_css and (".js" in filename or ".css" in filename): michael@0: return False michael@0: michael@0: pathPieces = filename.split("/") michael@0: michael@0: return (testPattern.match(pathPieces[-1]) and michael@0: not re.search(r'\^headers\^$', filename)) michael@0: michael@0: def getTestPath(self, options): michael@0: if options.ipcplugins: michael@0: return "dom/plugins/test" michael@0: else: michael@0: return options.testPath michael@0: michael@0: def getTestRoot(self, options): michael@0: if options.browserChrome: michael@0: if options.immersiveMode: michael@0: return 'metro' michael@0: return 'browser' michael@0: elif options.a11y: michael@0: return 'a11y' michael@0: elif options.webapprtChrome: michael@0: return 'webapprtChrome' michael@0: elif options.chrome: michael@0: return 'chrome' michael@0: return self.TEST_PATH michael@0: michael@0: def buildTestURL(self, options): michael@0: testHost = "http://mochi.test:8888" michael@0: testPath = self.getTestPath(options) michael@0: testURL = "/".join([testHost, self.TEST_PATH, testPath]) michael@0: if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, testPath)) and options.repeat > 0: michael@0: testURL = "/".join([testHost, self.TEST_PATH, os.path.dirname(testPath)]) michael@0: if options.chrome or options.a11y: michael@0: testURL = "/".join([testHost, self.CHROME_PATH]) michael@0: elif options.browserChrome: michael@0: testURL = "about:blank" michael@0: return testURL michael@0: michael@0: def buildTestPath(self, options): michael@0: """ Build the url path to the specific test harness and test file or directory michael@0: Build a manifest of tests to run and write out a json file for the harness to read michael@0: """ michael@0: manifest = None michael@0: michael@0: testRoot = self.getTestRoot(options) michael@0: # testdir refers to 'mochitest' here. michael@0: testdir = SCRIPT_DIR.split(os.getcwd())[-1] michael@0: testdir = testdir.strip(os.sep) michael@0: testRootAbs = os.path.abspath(os.path.join(testdir, testRoot)) michael@0: if isinstance(options.manifestFile, TestManifest): michael@0: manifest = options.manifestFile michael@0: elif options.manifestFile and os.path.isfile(options.manifestFile): michael@0: manifestFileAbs = os.path.abspath(options.manifestFile) michael@0: assert manifestFileAbs.startswith(testRootAbs) michael@0: manifest = TestManifest([options.manifestFile], strict=False) michael@0: else: michael@0: masterName = self.getTestFlavor(options) + '.ini' michael@0: masterPath = os.path.join(testdir, testRoot, masterName) michael@0: michael@0: if os.path.exists(masterPath): michael@0: manifest = TestManifest([masterPath], strict=False) michael@0: michael@0: if manifest: michael@0: # Python 2.6 doesn't allow unicode keys to be used for keyword michael@0: # arguments. This gross hack works around the problem until we michael@0: # rid ourselves of 2.6. michael@0: info = {} michael@0: for k, v in mozinfo.info.items(): michael@0: if isinstance(k, unicode): michael@0: k = k.encode('ascii') michael@0: info[k] = v michael@0: michael@0: # Bug 883858 - return all tests including disabled tests michael@0: tests = manifest.active_tests(disabled=True, options=options, **info) michael@0: paths = [] michael@0: testPath = self.getTestPath(options) michael@0: michael@0: for test in tests: michael@0: pathAbs = os.path.abspath(test['path']) michael@0: assert pathAbs.startswith(testRootAbs) michael@0: tp = pathAbs[len(testRootAbs):].replace('\\', '/').strip('/') michael@0: michael@0: # Filter out tests if we are using --test-path michael@0: if testPath and not tp.startswith(testPath): michael@0: continue michael@0: michael@0: if not self.isTest(options, tp): michael@0: print 'Warning: %s from manifest %s is not a valid test' % (test['name'], test['manifest']) michael@0: continue michael@0: michael@0: testob = {'path': tp} michael@0: if test.has_key('disabled'): michael@0: testob['disabled'] = test['disabled'] michael@0: paths.append(testob) michael@0: michael@0: # Sort tests so they are run in a deterministic order. michael@0: def path_sort(ob1, ob2): michael@0: path1 = ob1['path'].split('/') michael@0: path2 = ob2['path'].split('/') michael@0: return cmp(path1, path2) michael@0: michael@0: paths.sort(path_sort) michael@0: michael@0: # Bug 883865 - add this functionality into manifestDestiny michael@0: with open(os.path.join(testdir, 'tests.json'), 'w') as manifestFile: michael@0: manifestFile.write(json.dumps({'tests': paths})) michael@0: options.manifestFile = 'tests.json' michael@0: michael@0: return self.buildTestURL(options) michael@0: michael@0: def startWebSocketServer(self, options, debuggerInfo): michael@0: """ Launch the websocket server """ michael@0: self.wsserver = WebSocketServer(options, SCRIPT_DIR, debuggerInfo) michael@0: self.wsserver.start() michael@0: michael@0: def startWebServer(self, options): michael@0: """Create the webserver and start it up""" michael@0: michael@0: self.server = MochitestServer(options) michael@0: self.server.start() michael@0: michael@0: if options.pidFile != "": michael@0: with open(options.pidFile + ".xpcshell.pid", 'w') as f: michael@0: f.write("%s" % self.server._process.pid) michael@0: michael@0: def startServers(self, options, debuggerInfo): michael@0: # start servers and set ports michael@0: # TODO: pass these values, don't set on `self` michael@0: self.webServer = options.webServer michael@0: self.httpPort = options.httpPort michael@0: self.sslPort = options.sslPort michael@0: self.webSocketPort = options.webSocketPort michael@0: michael@0: # httpd-path is specified by standard makefile targets and may be specified michael@0: # on the command line to select a particular version of httpd.js. If not michael@0: # specified, try to select the one from hostutils.zip, as required in bug 882932. michael@0: if not options.httpdPath: michael@0: options.httpdPath = os.path.join(options.utilityPath, "components") michael@0: michael@0: self.startWebServer(options) michael@0: self.startWebSocketServer(options, debuggerInfo) michael@0: michael@0: # start SSL pipe michael@0: self.sslTunnel = SSLTunnel(options) michael@0: self.sslTunnel.buildConfig(self.locations) michael@0: self.sslTunnel.start() michael@0: michael@0: # If we're lucky, the server has fully started by now, and all paths are michael@0: # ready, etc. However, xpcshell cold start times suck, at least for debug michael@0: # builds. We'll try to connect to the server for awhile, and if we fail, michael@0: # we'll try to kill the server and exit with an error. michael@0: if self.server is not None: michael@0: self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT) michael@0: michael@0: def stopServers(self): michael@0: """Servers are no longer needed, and perhaps more importantly, anything they michael@0: might spew to console might confuse things.""" michael@0: if self.server is not None: michael@0: try: michael@0: log.info('Stopping web server') michael@0: self.server.stop() michael@0: except Exception: michael@0: log.exception('Exception when stopping web server') michael@0: michael@0: if self.wsserver is not None: michael@0: try: michael@0: log.info('Stopping web socket server') michael@0: self.wsserver.stop() michael@0: except Exception: michael@0: log.exception('Exception when stopping web socket server'); michael@0: michael@0: if self.sslTunnel is not None: michael@0: try: michael@0: log.info('Stopping ssltunnel') michael@0: self.sslTunnel.stop() michael@0: except Exception: michael@0: log.exception('Exception stopping ssltunnel'); michael@0: michael@0: def copyExtraFilesToProfile(self, options): michael@0: "Copy extra files or dirs specified on the command line to the testing profile." michael@0: for f in options.extraProfileFiles: michael@0: abspath = self.getFullPath(f) michael@0: if os.path.isfile(abspath): michael@0: shutil.copy2(abspath, options.profilePath) michael@0: elif os.path.isdir(abspath): michael@0: dest = os.path.join(options.profilePath, os.path.basename(abspath)) michael@0: shutil.copytree(abspath, dest) michael@0: else: michael@0: log.warning("runtests.py | Failed to copy %s to profile", abspath) michael@0: michael@0: def installChromeJar(self, chrome, options): michael@0: """ michael@0: copy mochijar directory to profile as an extension so we have chrome://mochikit for all harness code michael@0: """ michael@0: # Write chrome.manifest. michael@0: with open(os.path.join(options.profilePath, "extensions", "staged", "mochikit@mozilla.org", "chrome.manifest"), "a") as mfile: michael@0: mfile.write(chrome) michael@0: michael@0: def addChromeToProfile(self, options): michael@0: "Adds MochiKit chrome tests to the profile." michael@0: michael@0: # Create (empty) chrome directory. michael@0: chromedir = os.path.join(options.profilePath, "chrome") michael@0: os.mkdir(chromedir) michael@0: michael@0: # Write userChrome.css. michael@0: chrome = """ michael@0: @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ michael@0: toolbar, michael@0: toolbarpalette { michael@0: background-color: rgb(235, 235, 235) !important; michael@0: } michael@0: toolbar#nav-bar { michael@0: background-image: none !important; michael@0: } michael@0: """ michael@0: with open(os.path.join(options.profilePath, "userChrome.css"), "a") as chromeFile: michael@0: chromeFile.write(chrome) michael@0: michael@0: manifest = os.path.join(options.profilePath, "tests.manifest") michael@0: with open(manifest, "w") as manifestFile: michael@0: # Register chrome directory. michael@0: chrometestDir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/" michael@0: if mozinfo.isWin: michael@0: chrometestDir = "file:///" + chrometestDir.replace("\\", "/") michael@0: manifestFile.write("content mochitests %s contentaccessible=yes\n" % chrometestDir) michael@0: michael@0: if options.testingModulesDir is not None: michael@0: manifestFile.write("resource testing-common file:///%s\n" % michael@0: options.testingModulesDir) michael@0: michael@0: # Call installChromeJar(). michael@0: if not os.path.isdir(os.path.join(SCRIPT_DIR, self.jarDir)): michael@0: log.testFail("invalid setup: missing mochikit extension") michael@0: return None michael@0: michael@0: # Support Firefox (browser), B2G (shell), SeaMonkey (navigator), and Webapp michael@0: # Runtime (webapp). michael@0: chrome = "" michael@0: if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome: michael@0: chrome += """ michael@0: overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul michael@0: overlay chrome://browser/content/shell.xhtml chrome://mochikit/content/browser-test-overlay.xul michael@0: overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul michael@0: overlay chrome://webapprt/content/webapp.xul chrome://mochikit/content/browser-test-overlay.xul michael@0: """ michael@0: michael@0: self.installChromeJar(chrome, options) michael@0: return manifest michael@0: michael@0: def getExtensionsToInstall(self, options): michael@0: "Return a list of extensions to install in the profile" michael@0: extensions = options.extensionsToInstall or [] michael@0: appDir = options.app[:options.app.rfind(os.sep)] if options.app else options.utilityPath michael@0: michael@0: extensionDirs = [ michael@0: # Extensions distributed with the test harness. michael@0: os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")), michael@0: ] michael@0: if appDir: michael@0: # Extensions distributed with the application. michael@0: extensionDirs.append(os.path.join(appDir, "distribution", "extensions")) michael@0: michael@0: for extensionDir in extensionDirs: michael@0: if os.path.isdir(extensionDir): michael@0: for dirEntry in os.listdir(extensionDir): michael@0: if dirEntry not in options.extensionsToExclude: michael@0: path = os.path.join(extensionDir, dirEntry) michael@0: if os.path.isdir(path) or (os.path.isfile(path) and path.endswith(".xpi")): michael@0: extensions.append(path) michael@0: michael@0: # append mochikit michael@0: extensions.append(os.path.join(SCRIPT_DIR, self.jarDir)) michael@0: return extensions michael@0: michael@0: class SSLTunnel: michael@0: def __init__(self, options): michael@0: self.process = None michael@0: self.utilityPath = options.utilityPath michael@0: self.xrePath = options.xrePath michael@0: self.certPath = options.certPath michael@0: self.sslPort = options.sslPort michael@0: self.httpPort = options.httpPort michael@0: self.webServer = options.webServer michael@0: self.webSocketPort = options.webSocketPort michael@0: michael@0: self.customCertRE = re.compile("^cert=(?P[0-9a-zA-Z_ ]+)") michael@0: self.clientAuthRE = re.compile("^clientauth=(?P[a-z]+)") michael@0: self.redirRE = re.compile("^redir=(?P[0-9a-zA-Z_ .]+)") michael@0: michael@0: def writeLocation(self, config, loc): michael@0: for option in loc.options: michael@0: match = self.customCertRE.match(option) michael@0: if match: michael@0: customcert = match.group("nickname"); michael@0: config.write("listen:%s:%s:%s:%s\n" % michael@0: (loc.host, loc.port, self.sslPort, customcert)) michael@0: michael@0: match = self.clientAuthRE.match(option) michael@0: if match: michael@0: clientauth = match.group("clientauth"); michael@0: config.write("clientauth:%s:%s:%s:%s\n" % michael@0: (loc.host, loc.port, self.sslPort, clientauth)) michael@0: michael@0: match = self.redirRE.match(option) michael@0: if match: michael@0: redirhost = match.group("redirhost") michael@0: config.write("redirhost:%s:%s:%s:%s\n" % michael@0: (loc.host, loc.port, self.sslPort, redirhost)) michael@0: michael@0: def buildConfig(self, locations): michael@0: """Create the ssltunnel configuration file""" michael@0: configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg") michael@0: with os.fdopen(configFd, "w") as config: michael@0: config.write("httpproxy:1\n") michael@0: config.write("certdbdir:%s\n" % self.certPath) michael@0: config.write("forward:127.0.0.1:%s\n" % self.httpPort) michael@0: config.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort)) michael@0: config.write("listen:*:%s:pgo server certificate\n" % self.sslPort) michael@0: michael@0: for loc in locations: michael@0: if loc.scheme == "https" and "nocert" not in loc.options: michael@0: self.writeLocation(config, loc) michael@0: michael@0: def start(self): michael@0: """ Starts the SSL Tunnel """ michael@0: michael@0: # start ssltunnel to provide https:// URLs capability michael@0: bin_suffix = mozinfo.info.get('bin_suffix', '') michael@0: ssltunnel = os.path.join(self.utilityPath, "ssltunnel" + bin_suffix) michael@0: if not os.path.exists(ssltunnel): michael@0: log.error("INFO | runtests.py | expected to find ssltunnel at %s", ssltunnel) michael@0: exit(1) michael@0: michael@0: env = environment(xrePath=self.xrePath) michael@0: self.process = mozprocess.ProcessHandler([ssltunnel, self.configFile], michael@0: env=env) michael@0: self.process.run() michael@0: log.info("INFO | runtests.py | SSL tunnel pid: %d", self.process.pid) michael@0: michael@0: def stop(self): michael@0: """ Stops the SSL Tunnel and cleans up """ michael@0: if self.process is not None: michael@0: self.process.kill() michael@0: if os.path.exists(self.configFile): michael@0: os.remove(self.configFile) michael@0: michael@0: class Mochitest(MochitestUtilsMixin): michael@0: certdbNew = False michael@0: sslTunnel = None michael@0: vmwareHelper = None michael@0: DEFAULT_TIMEOUT = 60.0 michael@0: michael@0: # XXX use automation.py for test name to avoid breaking legacy michael@0: # TODO: replace this with 'runtests.py' or 'mochitest' or the like michael@0: test_name = 'automation.py' michael@0: michael@0: def __init__(self): michael@0: super(Mochitest, self).__init__() michael@0: michael@0: # environment function for browserEnv michael@0: self.environment = environment michael@0: michael@0: # Max time in seconds to wait for server startup before tests will fail -- if michael@0: # this seems big, it's mostly for debug machines where cold startup michael@0: # (particularly after a build) takes forever. michael@0: self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get('debug') else 90 michael@0: michael@0: # metro browser sub process id michael@0: self.browserProcessId = None michael@0: michael@0: michael@0: self.haveDumpedScreen = False michael@0: michael@0: def extraPrefs(self, extraPrefs): michael@0: """interpolate extra preferences from option strings""" michael@0: michael@0: try: michael@0: return dict(parseKeyValue(extraPrefs, context='--setpref=')) michael@0: except KeyValueParseError, e: michael@0: print str(e) michael@0: sys.exit(1) michael@0: michael@0: def fillCertificateDB(self, options): michael@0: # TODO: move -> mozprofile: michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35 michael@0: michael@0: pwfilePath = os.path.join(options.profilePath, ".crtdbpw") michael@0: with open(pwfilePath, "w") as pwfile: michael@0: pwfile.write("\n") michael@0: michael@0: # Pre-create the certification database for the profile michael@0: env = self.environment(xrePath=options.xrePath) michael@0: bin_suffix = mozinfo.info.get('bin_suffix', '') michael@0: certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix) michael@0: pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix) michael@0: michael@0: if self.certdbNew: michael@0: # android and b2g use the new DB formats exclusively michael@0: certdbPath = "sql:" + options.profilePath michael@0: else: michael@0: # desktop seems to use the old michael@0: certdbPath = options.profilePath michael@0: michael@0: status = call([certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=env) michael@0: if status: michael@0: return status michael@0: michael@0: # Walk the cert directory and add custom CAs and client certs michael@0: files = os.listdir(options.certPath) michael@0: for item in files: michael@0: root, ext = os.path.splitext(item) michael@0: if ext == ".ca": michael@0: trustBits = "CT,," michael@0: if root.endswith("-object"): michael@0: trustBits = "CT,,CT" michael@0: call([certutil, "-A", "-i", os.path.join(options.certPath, item), michael@0: "-d", certdbPath, "-f", pwfilePath, "-n", root, "-t", trustBits], michael@0: env=env) michael@0: elif ext == ".client": michael@0: call([pk12util, "-i", os.path.join(options.certPath, item), michael@0: "-w", pwfilePath, "-d", certdbPath], michael@0: env=env) michael@0: michael@0: os.unlink(pwfilePath) michael@0: return 0 michael@0: michael@0: def buildProfile(self, options): michael@0: """ create the profile and add optional chrome bits and files if requested """ michael@0: if options.browserChrome and options.timeout: michael@0: options.extraPrefs.append("testing.browserTestHarness.timeout=%d" % options.timeout) michael@0: options.extraPrefs.append("browser.tabs.remote=%s" % ('true' if options.e10s else 'false')) michael@0: options.extraPrefs.append("browser.tabs.remote.autostart=%s" % ('true' if options.e10s else 'false')) michael@0: michael@0: # get extensions to install michael@0: extensions = self.getExtensionsToInstall(options) michael@0: michael@0: # web apps michael@0: appsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'webapps_mochitest.json') michael@0: if os.path.exists(appsPath): michael@0: with open(appsPath) as apps_file: michael@0: apps = json.load(apps_file) michael@0: else: michael@0: apps = None michael@0: michael@0: # preferences michael@0: prefsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'prefs_general.js') michael@0: prefs = dict(Preferences.read_prefs(prefsPath)) michael@0: prefs.update(self.extraPrefs(options.extraPrefs)) michael@0: michael@0: # interpolate preferences michael@0: interpolation = {"server": "%s:%s" % (options.webServer, options.httpPort)} michael@0: prefs = json.loads(json.dumps(prefs) % interpolation) michael@0: for pref in prefs: michael@0: prefs[pref] = Preferences.cast(prefs[pref]) michael@0: # TODO: make this less hacky michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=913152 michael@0: michael@0: # proxy michael@0: proxy = {'remote': options.webServer, michael@0: 'http': options.httpPort, michael@0: 'https': options.sslPort, michael@0: # use SSL port for legacy compatibility; see michael@0: # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66 michael@0: # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221 michael@0: # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d michael@0: # 'ws': str(self.webSocketPort) michael@0: 'ws': options.sslPort michael@0: } michael@0: michael@0: michael@0: # create a profile michael@0: self.profile = Profile(profile=options.profilePath, michael@0: addons=extensions, michael@0: locations=self.locations, michael@0: preferences=prefs, michael@0: apps=apps, michael@0: proxy=proxy michael@0: ) michael@0: michael@0: # Fix options.profilePath for legacy consumers. michael@0: options.profilePath = self.profile.profile michael@0: michael@0: manifest = self.addChromeToProfile(options) michael@0: self.copyExtraFilesToProfile(options) michael@0: michael@0: # create certificate database for the profile michael@0: # TODO: this should really be upstreamed somewhere, maybe mozprofile michael@0: certificateStatus = self.fillCertificateDB(options) michael@0: if certificateStatus: michael@0: log.info("TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed") michael@0: return None michael@0: michael@0: return manifest michael@0: michael@0: def buildBrowserEnv(self, options, debugger=False): michael@0: """build the environment variables for the specific test and operating system""" michael@0: browserEnv = self.environment(xrePath=options.xrePath, debugger=debugger, michael@0: dmdPath=options.dmdPath) michael@0: michael@0: # These variables are necessary for correct application startup; change michael@0: # via the commandline at your own risk. michael@0: browserEnv["XPCOM_DEBUG_BREAK"] = "stack" michael@0: michael@0: # interpolate environment passed with options michael@0: try: michael@0: browserEnv.update(dict(parseKeyValue(options.environment, context='--setenv'))) michael@0: except KeyValueParseError, e: michael@0: log.error(str(e)) michael@0: return michael@0: michael@0: browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file michael@0: michael@0: if options.fatalAssertions: michael@0: browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort" michael@0: michael@0: # Produce an NSPR log, is setup (see NSPR_LOG_MODULES global at the top of michael@0: # this script). michael@0: self.nsprLogs = NSPR_LOG_MODULES and "MOZ_UPLOAD_DIR" in os.environ michael@0: if self.nsprLogs: michael@0: browserEnv["NSPR_LOG_MODULES"] = NSPR_LOG_MODULES michael@0: michael@0: browserEnv["NSPR_LOG_FILE"] = "%s/nspr.log" % tempfile.gettempdir() michael@0: browserEnv["GECKO_SEPARATE_NSPR_LOGS"] = "1" michael@0: michael@0: if debugger and not options.slowscript: michael@0: browserEnv["JS_DISABLE_SLOW_SCRIPT_SIGNALS"] = "1" michael@0: michael@0: return browserEnv michael@0: michael@0: def cleanup(self, manifest, options): michael@0: """ remove temporary files and profile """ michael@0: os.remove(manifest) michael@0: del self.profile michael@0: if options.pidFile != "": michael@0: try: michael@0: os.remove(options.pidFile) michael@0: if os.path.exists(options.pidFile + ".xpcshell.pid"): michael@0: os.remove(options.pidFile + ".xpcshell.pid") michael@0: except: michael@0: log.warn("cleaning up pidfile '%s' was unsuccessful from the test harness", options.pidFile) michael@0: michael@0: def dumpScreen(self, utilityPath): michael@0: if self.haveDumpedScreen: michael@0: log.info("Not taking screenshot here: see the one that was previously logged") michael@0: return michael@0: self.haveDumpedScreen = True michael@0: dumpScreen(utilityPath) michael@0: michael@0: def killAndGetStack(self, processPID, utilityPath, debuggerInfo, dump_screen=False): michael@0: """ michael@0: Kill the process, preferrably in a way that gets us a stack trace. michael@0: Also attempts to obtain a screenshot before killing the process michael@0: if specified. michael@0: """ michael@0: michael@0: if dump_screen: michael@0: self.dumpScreen(utilityPath) michael@0: michael@0: if mozinfo.info.get('crashreporter', True) and not debuggerInfo: michael@0: if mozinfo.isWin: michael@0: # We should have a "crashinject" program in our utility path michael@0: crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe")) michael@0: if os.path.exists(crashinject): michael@0: status = subprocess.Popen([crashinject, str(processPID)]).wait() michael@0: printstatus(status, "crashinject") michael@0: if status == 0: michael@0: return michael@0: else: michael@0: try: michael@0: os.kill(processPID, signal.SIGABRT) michael@0: except OSError: michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=921509 michael@0: log.info("Can't trigger Breakpad, process no longer exists") michael@0: return michael@0: log.info("Can't trigger Breakpad, just killing process") michael@0: killPid(processPID) michael@0: michael@0: def checkForZombies(self, processLog, utilityPath, debuggerInfo): michael@0: """Look for hung processes""" michael@0: michael@0: if not os.path.exists(processLog): michael@0: log.info('Automation Error: PID log not found: %s', processLog) michael@0: # Whilst no hung process was found, the run should still display as a failure michael@0: return True michael@0: michael@0: # scan processLog for zombies michael@0: log.info('INFO | zombiecheck | Reading PID log: %s', processLog) michael@0: processList = [] michael@0: pidRE = re.compile(r'launched child process (\d+)$') michael@0: with open(processLog) as processLogFD: michael@0: for line in processLogFD: michael@0: log.info(line.rstrip()) michael@0: m = pidRE.search(line) michael@0: if m: michael@0: processList.append(int(m.group(1))) michael@0: michael@0: # kill zombies michael@0: foundZombie = False michael@0: for processPID in processList: michael@0: log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID) michael@0: if isPidAlive(processPID): michael@0: foundZombie = True michael@0: log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID) michael@0: self.killAndGetStack(processPID, utilityPath, debuggerInfo, dump_screen=not debuggerInfo) michael@0: michael@0: return foundZombie michael@0: michael@0: def startVMwareRecording(self, options): michael@0: """ starts recording inside VMware VM using the recording helper dll """ michael@0: assert mozinfo.isWin michael@0: from ctypes import cdll michael@0: self.vmwareHelper = cdll.LoadLibrary(self.vmwareHelperPath) michael@0: if self.vmwareHelper is None: michael@0: log.warning("runtests.py | Failed to load " michael@0: "VMware recording helper") michael@0: return michael@0: log.info("runtests.py | Starting VMware recording.") michael@0: try: michael@0: self.vmwareHelper.StartRecording() michael@0: except Exception, e: michael@0: log.warning("runtests.py | Failed to start " michael@0: "VMware recording: (%s)" % str(e)) michael@0: self.vmwareHelper = None michael@0: michael@0: def stopVMwareRecording(self): michael@0: """ stops recording inside VMware VM using the recording helper dll """ michael@0: try: michael@0: assert mozinfo.isWin michael@0: if self.vmwareHelper is not None: michael@0: log.info("runtests.py | Stopping VMware recording.") michael@0: self.vmwareHelper.StopRecording() michael@0: except Exception, e: michael@0: log.warning("runtests.py | Failed to stop " michael@0: "VMware recording: (%s)" % str(e)) michael@0: log.exception('Error stopping VMWare recording') michael@0: michael@0: self.vmwareHelper = None michael@0: michael@0: def runApp(self, michael@0: testUrl, michael@0: env, michael@0: app, michael@0: profile, michael@0: extraArgs, michael@0: utilityPath, michael@0: debuggerInfo=None, michael@0: symbolsPath=None, michael@0: timeout=-1, michael@0: onLaunch=None, michael@0: webapprtChrome=False, michael@0: hide_subtests=False, michael@0: screenshotOnFail=False): michael@0: """ michael@0: Run the app, log the duration it took to execute, return the status code. michael@0: Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds. michael@0: """ michael@0: michael@0: # debugger information michael@0: interactive = False michael@0: debug_args = None michael@0: if debuggerInfo: michael@0: interactive = debuggerInfo['interactive'] michael@0: debug_args = [debuggerInfo['path']] + debuggerInfo['args'] michael@0: michael@0: # fix default timeout michael@0: if timeout == -1: michael@0: timeout = self.DEFAULT_TIMEOUT michael@0: michael@0: # build parameters michael@0: is_test_build = mozinfo.info.get('tests_enabled', True) michael@0: bin_suffix = mozinfo.info.get('bin_suffix', '') michael@0: michael@0: # copy env so we don't munge the caller's environment michael@0: env = env.copy() michael@0: michael@0: # make sure we clean up after ourselves. michael@0: try: michael@0: # set process log environment variable michael@0: tmpfd, processLog = tempfile.mkstemp(suffix='pidlog') michael@0: os.close(tmpfd) michael@0: env["MOZ_PROCESS_LOG"] = processLog michael@0: michael@0: if interactive: michael@0: # If an interactive debugger is attached, michael@0: # don't use timeouts, and don't capture ctrl-c. michael@0: timeout = None michael@0: signal.signal(signal.SIGINT, lambda sigid, frame: None) michael@0: michael@0: # build command line michael@0: cmd = os.path.abspath(app) michael@0: args = list(extraArgs) michael@0: # TODO: mozrunner should use -foreground at least for mac michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=916512 michael@0: args.append('-foreground') michael@0: if testUrl: michael@0: if debuggerInfo and debuggerInfo['requiresEscapedArgs']: michael@0: testUrl = testUrl.replace("&", "\\&") michael@0: args.append(testUrl) michael@0: michael@0: if mozinfo.info["debug"] and not webapprtChrome: michael@0: shutdownLeaks = ShutdownLeaks(log.info) michael@0: else: michael@0: shutdownLeaks = None michael@0: michael@0: # create an instance to process the output michael@0: outputHandler = self.OutputHandler(harness=self, michael@0: utilityPath=utilityPath, michael@0: symbolsPath=symbolsPath, michael@0: dump_screen_on_timeout=not debuggerInfo, michael@0: dump_screen_on_fail=screenshotOnFail, michael@0: hide_subtests=hide_subtests, michael@0: shutdownLeaks=shutdownLeaks, michael@0: ) michael@0: michael@0: def timeoutHandler(): michael@0: outputHandler.log_output_buffer() michael@0: browserProcessId = outputHandler.browserProcessId michael@0: self.handleTimeout(timeout, proc, utilityPath, debuggerInfo, browserProcessId) michael@0: kp_kwargs = {'kill_on_timeout': False, michael@0: 'cwd': SCRIPT_DIR, michael@0: 'onTimeout': [timeoutHandler]} michael@0: kp_kwargs['processOutputLine'] = [outputHandler] michael@0: michael@0: # create mozrunner instance and start the system under test process michael@0: self.lastTestSeen = self.test_name michael@0: startTime = datetime.now() michael@0: michael@0: # b2g desktop requires FirefoxRunner even though appname is b2g michael@0: if mozinfo.info.get('appname') == 'b2g' and mozinfo.info.get('toolkit') != 'gonk': michael@0: runner_cls = mozrunner.FirefoxRunner michael@0: else: michael@0: runner_cls = mozrunner.runners.get(mozinfo.info.get('appname', 'firefox'), michael@0: mozrunner.Runner) michael@0: runner = runner_cls(profile=self.profile, michael@0: binary=cmd, michael@0: cmdargs=args, michael@0: env=env, michael@0: process_class=mozprocess.ProcessHandlerMixin, michael@0: kp_kwargs=kp_kwargs, michael@0: ) michael@0: michael@0: # XXX work around bug 898379 until mozrunner is updated for m-c; see michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c49 michael@0: runner.kp_kwargs = kp_kwargs michael@0: michael@0: # start the runner michael@0: runner.start(debug_args=debug_args, michael@0: interactive=interactive, michael@0: outputTimeout=timeout) michael@0: proc = runner.process_handler michael@0: log.info("INFO | runtests.py | Application pid: %d", proc.pid) michael@0: michael@0: if onLaunch is not None: michael@0: # Allow callers to specify an onLaunch callback to be fired after the michael@0: # app is launched. michael@0: # We call onLaunch for b2g desktop mochitests so that we can michael@0: # run a Marionette script after gecko has completed startup. michael@0: onLaunch() michael@0: michael@0: # wait until app is finished michael@0: # XXX copy functionality from michael@0: # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61 michael@0: # until bug 913970 is fixed regarding mozrunner `wait` not returning status michael@0: # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970 michael@0: status = proc.wait() michael@0: printstatus(status, "Main app process") michael@0: runner.process_handler = None michael@0: michael@0: if timeout is None: michael@0: didTimeout = False michael@0: else: michael@0: didTimeout = proc.didTimeout michael@0: michael@0: # finalize output handler michael@0: outputHandler.finish(didTimeout) michael@0: michael@0: # record post-test information michael@0: if status: michael@0: log.info("TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s", self.lastTestSeen, status) michael@0: else: michael@0: self.lastTestSeen = 'Main app process exited normally' michael@0: michael@0: log.info("INFO | runtests.py | Application ran for: %s", str(datetime.now() - startTime)) michael@0: michael@0: # Do a final check for zombie child processes. michael@0: zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo) michael@0: michael@0: # check for crashes michael@0: minidump_path = os.path.join(self.profile.profile, "minidumps") michael@0: crashed = mozcrash.check_for_crashes(minidump_path, michael@0: symbolsPath, michael@0: test_name=self.lastTestSeen) michael@0: michael@0: if crashed or zombieProcesses: michael@0: status = 1 michael@0: michael@0: finally: michael@0: # cleanup michael@0: if os.path.exists(processLog): michael@0: os.remove(processLog) michael@0: michael@0: return status michael@0: michael@0: def runTests(self, options, onLaunch=None): michael@0: """ Prepare, configure, run tests and cleanup """ michael@0: michael@0: # get debugger info, a dict of: michael@0: # {'path': path to the debugger (string), michael@0: # 'interactive': whether the debugger is interactive or not (bool) michael@0: # 'args': arguments to the debugger (list) michael@0: # TODO: use mozrunner.local.debugger_arguments: michael@0: # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42 michael@0: debuggerInfo = getDebuggerInfo(self.oldcwd, michael@0: options.debugger, michael@0: options.debuggerArgs, michael@0: options.debuggerInteractive) michael@0: michael@0: self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log") michael@0: michael@0: browserEnv = self.buildBrowserEnv(options, debuggerInfo is not None) michael@0: if browserEnv is None: michael@0: return 1 michael@0: michael@0: # buildProfile sets self.profile . michael@0: # This relies on sideeffects and isn't very stateful: michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=919300 michael@0: manifest = self.buildProfile(options) michael@0: if manifest is None: michael@0: return 1 michael@0: michael@0: try: michael@0: self.startServers(options, debuggerInfo) michael@0: michael@0: testURL = self.buildTestPath(options) michael@0: self.buildURLOptions(options, browserEnv) michael@0: if self.urlOpts: michael@0: testURL += "?" + "&".join(self.urlOpts) michael@0: michael@0: if options.webapprtContent: michael@0: options.browserArgs.extend(('-test-mode', testURL)) michael@0: testURL = None michael@0: michael@0: if options.immersiveMode: michael@0: options.browserArgs.extend(('-firefoxpath', options.app)) michael@0: options.app = self.immersiveHelperPath michael@0: michael@0: if options.jsdebugger: michael@0: options.browserArgs.extend(['-jsdebugger']) michael@0: michael@0: # Remove the leak detection file so it can't "leak" to the tests run. michael@0: # The file is not there if leak logging was not enabled in the application build. michael@0: if os.path.exists(self.leak_report_file): michael@0: os.remove(self.leak_report_file) michael@0: michael@0: # then again to actually run mochitest michael@0: if options.timeout: michael@0: timeout = options.timeout + 30 michael@0: elif options.debugger or not options.autorun: michael@0: timeout = None michael@0: else: michael@0: timeout = 330.0 # default JS harness timeout is 300 seconds michael@0: michael@0: if options.vmwareRecording: michael@0: self.startVMwareRecording(options); michael@0: michael@0: log.info("runtests.py | Running tests: start.\n") michael@0: try: michael@0: status = self.runApp(testURL, michael@0: browserEnv, michael@0: options.app, michael@0: profile=self.profile, michael@0: extraArgs=options.browserArgs, michael@0: utilityPath=options.utilityPath, michael@0: debuggerInfo=debuggerInfo, michael@0: symbolsPath=options.symbolsPath, michael@0: timeout=timeout, michael@0: onLaunch=onLaunch, michael@0: webapprtChrome=options.webapprtChrome, michael@0: hide_subtests=options.hide_subtests, michael@0: screenshotOnFail=options.screenshotOnFail michael@0: ) michael@0: except KeyboardInterrupt: michael@0: log.info("runtests.py | Received keyboard interrupt.\n"); michael@0: status = -1 michael@0: except: michael@0: traceback.print_exc() michael@0: log.error("Automation Error: Received unexpected exception while running application\n") michael@0: status = 1 michael@0: michael@0: finally: michael@0: if options.vmwareRecording: michael@0: self.stopVMwareRecording(); michael@0: self.stopServers() michael@0: michael@0: processLeakLog(self.leak_report_file, options.leakThreshold) michael@0: michael@0: if self.nsprLogs: michael@0: with zipfile.ZipFile("%s/nsprlog.zip" % browserEnv["MOZ_UPLOAD_DIR"], "w", zipfile.ZIP_DEFLATED) as logzip: michael@0: for logfile in glob.glob("%s/nspr*.log*" % tempfile.gettempdir()): michael@0: logzip.write(logfile) michael@0: os.remove(logfile) michael@0: michael@0: log.info("runtests.py | Running tests: end.") michael@0: michael@0: if manifest is not None: michael@0: self.cleanup(manifest, options) michael@0: michael@0: return status michael@0: michael@0: def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo, browserProcessId): michael@0: """handle process output timeout""" michael@0: # TODO: bug 913975 : _processOutput should call self.processOutputLine one more time one timeout (I think) michael@0: log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout)) michael@0: browserProcessId = browserProcessId or proc.pid michael@0: self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo, dump_screen=not debuggerInfo) michael@0: michael@0: ### output processing michael@0: michael@0: class OutputHandler(object): michael@0: """line output handler for mozrunner""" michael@0: def __init__(self, harness, utilityPath, symbolsPath=None, dump_screen_on_timeout=True, dump_screen_on_fail=False, michael@0: hide_subtests=False, shutdownLeaks=None): michael@0: """ michael@0: harness -- harness instance michael@0: dump_screen_on_timeout -- whether to dump the screen on timeout michael@0: """ michael@0: self.harness = harness michael@0: self.output_buffer = [] michael@0: self.running_test = False michael@0: self.utilityPath = utilityPath michael@0: self.symbolsPath = symbolsPath michael@0: self.dump_screen_on_timeout = dump_screen_on_timeout michael@0: self.dump_screen_on_fail = dump_screen_on_fail michael@0: self.hide_subtests = hide_subtests michael@0: self.shutdownLeaks = shutdownLeaks michael@0: michael@0: # perl binary to use michael@0: self.perl = which('perl') michael@0: michael@0: # With metro browser runs this script launches the metro test harness which launches the browser. michael@0: # The metro test harness hands back the real browser process id via log output which we need to michael@0: # pick up on and parse out. This variable tracks the real browser process id if we find it. michael@0: self.browserProcessId = None michael@0: michael@0: # stack fixer function and/or process michael@0: self.stackFixerFunction, self.stackFixerProcess = self.stackFixer() michael@0: michael@0: def processOutputLine(self, line): michael@0: """per line handler of output for mozprocess""" michael@0: for handler in self.outputHandlers(): michael@0: line = handler(line) michael@0: __call__ = processOutputLine michael@0: michael@0: def outputHandlers(self): michael@0: """returns ordered list of output handlers""" michael@0: return [self.fix_stack, michael@0: self.format, michael@0: self.dumpScreenOnTimeout, michael@0: self.dumpScreenOnFail, michael@0: self.metro_subprocess_id, michael@0: self.trackShutdownLeaks, michael@0: self.check_test_failure, michael@0: self.log, michael@0: self.record_last_test, michael@0: ] michael@0: michael@0: def stackFixer(self): michael@0: """ michael@0: return 2-tuple, (stackFixerFunction, StackFixerProcess), michael@0: if any, to use on the output lines michael@0: """ michael@0: michael@0: if not mozinfo.info.get('debug'): michael@0: return None, None michael@0: michael@0: stackFixerFunction = stackFixerProcess = None michael@0: michael@0: def import_stackFixerModule(module_name): michael@0: sys.path.insert(0, self.utilityPath) michael@0: module = __import__(module_name, globals(), locals(), []) michael@0: sys.path.pop(0) michael@0: return module michael@0: michael@0: if self.symbolsPath and os.path.exists(self.symbolsPath): michael@0: # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files) michael@0: # This method is preferred for Tinderbox builds, since native symbols may have been stripped. michael@0: stackFixerModule = import_stackFixerModule('fix_stack_using_bpsyms') michael@0: stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, self.symbolsPath) michael@0: michael@0: elif mozinfo.isLinux and self.perl: michael@0: # Run logsource through fix-linux-stack.pl (uses addr2line) michael@0: # This method is preferred for developer machines, so we don't have to run "make buildsymbols". michael@0: stackFixerCommand = [self.perl, os.path.join(self.utilityPath, "fix-linux-stack.pl")] michael@0: stackFixerProcess = subprocess.Popen(stackFixerCommand, stdin=subprocess.PIPE, michael@0: stdout=subprocess.PIPE) michael@0: def fixFunc(line): michael@0: stackFixerProcess.stdin.write(line + '\n') michael@0: return stackFixerProcess.stdout.readline().rstrip() michael@0: michael@0: stackFixerFunction = fixFunc michael@0: michael@0: return (stackFixerFunction, stackFixerProcess) michael@0: michael@0: def finish(self, didTimeout): michael@0: if self.stackFixerProcess: michael@0: self.stackFixerProcess.communicate() michael@0: status = self.stackFixerProcess.returncode michael@0: if status and not didTimeout: michael@0: log.info("TEST-UNEXPECTED-FAIL | runtests.py | Stack fixer process exited with code %d during test run", status) michael@0: michael@0: if self.shutdownLeaks: michael@0: self.shutdownLeaks.process() michael@0: michael@0: def log_output_buffer(self): michael@0: if self.output_buffer: michael@0: lines = [' %s' % line for line in self.output_buffer] michael@0: log.info("Buffered test output:\n%s" % '\n'.join(lines)) michael@0: michael@0: # output line handlers: michael@0: # these take a line and return a line michael@0: michael@0: def fix_stack(self, line): michael@0: if self.stackFixerFunction: michael@0: return self.stackFixerFunction(line) michael@0: return line michael@0: michael@0: def format(self, line): michael@0: """format the line""" michael@0: return line.rstrip().decode("UTF-8", "ignore") michael@0: michael@0: def dumpScreenOnTimeout(self, line): michael@0: 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: michael@0: self.log_output_buffer() michael@0: self.harness.dumpScreen(self.utilityPath) michael@0: return line michael@0: michael@0: def dumpScreenOnFail(self, line): michael@0: if self.dump_screen_on_fail and "TEST-UNEXPECTED-FAIL" in line: michael@0: self.log_output_buffer() michael@0: self.harness.dumpScreen(self.utilityPath) michael@0: return line michael@0: michael@0: def metro_subprocess_id(self, line): michael@0: """look for metro browser subprocess id""" michael@0: if "METRO_BROWSER_PROCESS" in line: michael@0: index = line.find("=") michael@0: if index != -1: michael@0: self.browserProcessId = line[index+1:].rstrip() michael@0: log.info("INFO | runtests.py | metro browser sub process id detected: %s", self.browserProcessId) michael@0: return line michael@0: michael@0: def trackShutdownLeaks(self, line): michael@0: if self.shutdownLeaks: michael@0: self.shutdownLeaks.log(line) michael@0: return line michael@0: michael@0: def check_test_failure(self, line): michael@0: if 'TEST-END' in line: michael@0: self.running_test = False michael@0: if any('TEST-UNEXPECTED' in l for l in self.output_buffer): michael@0: self.log_output_buffer() michael@0: return line michael@0: michael@0: def log(self, line): michael@0: if self.hide_subtests and self.running_test: michael@0: self.output_buffer.append(line) michael@0: else: michael@0: # hack to make separators align nicely, remove when we use mozlog michael@0: if self.hide_subtests and 'TEST-END' in line: michael@0: index = line.index('TEST-END') + len('TEST-END') michael@0: line = line[:index] + ' ' * (len('TEST-START')-len('TEST-END')) + line[index:] michael@0: log.info(line) michael@0: return line michael@0: michael@0: def record_last_test(self, line): michael@0: """record last test on harness""" michael@0: if "TEST-START" in line and "|" in line: michael@0: if not line.endswith('Shutdown'): michael@0: self.output_buffer = [] michael@0: self.running_test = True michael@0: self.harness.lastTestSeen = line.split("|")[1].strip() michael@0: return line michael@0: michael@0: michael@0: def makeTestConfig(self, options): michael@0: "Creates a test configuration file for customizing test execution." michael@0: options.logFile = options.logFile.replace("\\", "\\\\") michael@0: options.testPath = options.testPath.replace("\\", "\\\\") michael@0: testRoot = self.getTestRoot(options) michael@0: michael@0: if "MOZ_HIDE_RESULTS_TABLE" in os.environ and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1": michael@0: options.hideResultsTable = True michael@0: michael@0: d = dict(options.__dict__) michael@0: d['testRoot'] = testRoot michael@0: content = json.dumps(d) michael@0: michael@0: with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config: michael@0: config.write(content) michael@0: michael@0: def installExtensionFromPath(self, options, path, extensionID = None): michael@0: """install an extension to options.profilePath""" michael@0: michael@0: # TODO: currently extensionID is unused; see michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=914267 michael@0: # [mozprofile] make extensionID a parameter to install_from_path michael@0: # https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/addons.py#L169 michael@0: michael@0: extensionPath = self.getFullPath(path) michael@0: michael@0: log.info("runtests.py | Installing extension at %s to %s." % michael@0: (extensionPath, options.profilePath)) michael@0: michael@0: addons = AddonManager(options.profilePath) michael@0: michael@0: # XXX: del the __del__ michael@0: # hack can be removed when mozprofile is mirrored to m-c ; see michael@0: # https://bugzilla.mozilla.org/show_bug.cgi?id=911218 : michael@0: # [mozprofile] AddonManager should only cleanup on __del__ optionally: michael@0: # https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/addons.py#L266 michael@0: if hasattr(addons, '__del__'): michael@0: del addons.__del__ michael@0: michael@0: addons.install_from_path(path) michael@0: michael@0: def installExtensionsToProfile(self, options): michael@0: "Install special testing extensions, application distributed extensions, and specified on the command line ones to testing profile." michael@0: for path in self.getExtensionsToInstall(options): michael@0: self.installExtensionFromPath(options, path) michael@0: michael@0: michael@0: def main(): michael@0: michael@0: # parse command line options michael@0: mochitest = Mochitest() michael@0: parser = MochitestOptions() michael@0: options, args = parser.parse_args() michael@0: options = parser.verifyOptions(options, mochitest) michael@0: if options is None: michael@0: # parsing error michael@0: sys.exit(1) michael@0: michael@0: options.utilityPath = mochitest.getFullPath(options.utilityPath) michael@0: options.certPath = mochitest.getFullPath(options.certPath) michael@0: if options.symbolsPath and not isURL(options.symbolsPath): michael@0: options.symbolsPath = mochitest.getFullPath(options.symbolsPath) michael@0: michael@0: sys.exit(mochitest.runTests(options)) michael@0: michael@0: if __name__ == "__main__": michael@0: main()