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