testing/mochitest/runtests.py

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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()

mercurial