michael@0: # michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: from __future__ import with_statement michael@0: import codecs michael@0: import itertools michael@0: import json michael@0: import logging michael@0: import os michael@0: import re michael@0: import select michael@0: import shutil michael@0: import signal michael@0: import subprocess michael@0: import sys michael@0: import threading michael@0: import tempfile michael@0: import sqlite3 michael@0: from datetime import datetime, timedelta michael@0: from string import Template michael@0: michael@0: SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) michael@0: sys.path.insert(0, SCRIPT_DIR) michael@0: import automationutils michael@0: michael@0: # -------------------------------------------------------------- michael@0: # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900 michael@0: # These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR michael@0: here = os.path.dirname(os.path.realpath(__file__)) michael@0: mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase')) michael@0: michael@0: if os.path.isdir(mozbase): michael@0: for package in os.listdir(mozbase): michael@0: package_path = os.path.join(mozbase, package) michael@0: if package_path not in sys.path: michael@0: sys.path.append(package_path) michael@0: michael@0: import mozcrash michael@0: from mozprofile import Profile, Preferences michael@0: from mozprofile.permissions import ServerLocations michael@0: michael@0: # --------------------------------------------------------------- michael@0: michael@0: _DEFAULT_PREFERENCE_FILE = os.path.join(SCRIPT_DIR, 'prefs_general.js') michael@0: _DEFAULT_APPS_FILE = os.path.join(SCRIPT_DIR, 'webapps_mochitest.json') michael@0: michael@0: _DEFAULT_WEB_SERVER = "127.0.0.1" michael@0: _DEFAULT_HTTP_PORT = 8888 michael@0: _DEFAULT_SSL_PORT = 4443 michael@0: _DEFAULT_WEBSOCKET_PORT = 9988 michael@0: michael@0: # from nsIPrincipal.idl michael@0: _APP_STATUS_NOT_INSTALLED = 0 michael@0: _APP_STATUS_INSTALLED = 1 michael@0: _APP_STATUS_PRIVILEGED = 2 michael@0: _APP_STATUS_CERTIFIED = 3 michael@0: michael@0: #expand _DIST_BIN = __XPC_BIN_PATH__ michael@0: #expand _IS_WIN32 = len("__WIN32__") != 0 michael@0: #expand _IS_MAC = __IS_MAC__ != 0 michael@0: #expand _IS_LINUX = __IS_LINUX__ != 0 michael@0: #ifdef IS_CYGWIN michael@0: #expand _IS_CYGWIN = __IS_CYGWIN__ == 1 michael@0: #else michael@0: _IS_CYGWIN = False michael@0: #endif michael@0: #expand _IS_CAMINO = __IS_CAMINO__ != 0 michael@0: #expand _BIN_SUFFIX = __BIN_SUFFIX__ michael@0: #expand _PERL = __PERL__ michael@0: michael@0: #expand _DEFAULT_APP = "./" + __BROWSER_PATH__ michael@0: #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__ michael@0: #expand _IS_TEST_BUILD = __IS_TEST_BUILD__ michael@0: #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__ michael@0: #expand _CRASHREPORTER = __CRASHREPORTER__ == 1 michael@0: #expand _IS_ASAN = __IS_ASAN__ == 1 michael@0: michael@0: michael@0: if _IS_WIN32: michael@0: import ctypes, ctypes.wintypes, time, msvcrt michael@0: else: michael@0: import errno michael@0: michael@0: michael@0: def getGlobalLog(): michael@0: return _log michael@0: michael@0: def resetGlobalLog(log): michael@0: while _log.handlers: michael@0: _log.removeHandler(_log.handlers[0]) michael@0: handler = logging.StreamHandler(log) michael@0: _log.setLevel(logging.INFO) michael@0: _log.addHandler(handler) michael@0: michael@0: # We use the logging system here primarily because it'll handle multiple michael@0: # threads, which is needed to process the output of the server and application michael@0: # processes simultaneously. michael@0: _log = logging.getLogger() michael@0: resetGlobalLog(sys.stdout) michael@0: michael@0: michael@0: ################# michael@0: # PROFILE SETUP # michael@0: ################# michael@0: michael@0: class SyntaxError(Exception): michael@0: "Signifies a syntax error on a particular line in server-locations.txt." michael@0: michael@0: def __init__(self, lineno, msg = None): michael@0: self.lineno = lineno michael@0: self.msg = msg michael@0: michael@0: def __str__(self): michael@0: s = "Syntax error on line " + str(self.lineno) michael@0: if self.msg: michael@0: s += ": %s." % self.msg michael@0: else: michael@0: s += "." michael@0: return s michael@0: michael@0: michael@0: class Location: michael@0: "Represents a location line in server-locations.txt." michael@0: michael@0: def __init__(self, scheme, host, port, options): michael@0: self.scheme = scheme michael@0: self.host = host michael@0: self.port = port michael@0: self.options = options michael@0: michael@0: class Automation(object): michael@0: """ michael@0: Runs the browser from a script, and provides useful utilities michael@0: for setting up the browser environment. michael@0: """ michael@0: michael@0: DIST_BIN = _DIST_BIN michael@0: IS_WIN32 = _IS_WIN32 michael@0: IS_MAC = _IS_MAC michael@0: IS_LINUX = _IS_LINUX michael@0: IS_CYGWIN = _IS_CYGWIN michael@0: IS_CAMINO = _IS_CAMINO michael@0: BIN_SUFFIX = _BIN_SUFFIX michael@0: PERL = _PERL michael@0: michael@0: UNIXISH = not IS_WIN32 and not IS_MAC michael@0: michael@0: DEFAULT_APP = _DEFAULT_APP michael@0: CERTS_SRC_DIR = _CERTS_SRC_DIR michael@0: IS_TEST_BUILD = _IS_TEST_BUILD michael@0: IS_DEBUG_BUILD = _IS_DEBUG_BUILD michael@0: CRASHREPORTER = _CRASHREPORTER michael@0: IS_ASAN = _IS_ASAN michael@0: michael@0: # timeout, in seconds michael@0: DEFAULT_TIMEOUT = 60.0 michael@0: DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER michael@0: DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT michael@0: DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT michael@0: DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT michael@0: michael@0: def __init__(self): michael@0: self.log = _log michael@0: self.lastTestSeen = "automation.py" michael@0: self.haveDumpedScreen = False michael@0: michael@0: def setServerInfo(self, michael@0: webServer = _DEFAULT_WEB_SERVER, michael@0: httpPort = _DEFAULT_HTTP_PORT, michael@0: sslPort = _DEFAULT_SSL_PORT, michael@0: webSocketPort = _DEFAULT_WEBSOCKET_PORT): michael@0: self.webServer = webServer michael@0: self.httpPort = httpPort michael@0: self.sslPort = sslPort michael@0: self.webSocketPort = webSocketPort michael@0: michael@0: @property michael@0: def __all__(self): michael@0: return [ michael@0: "UNIXISH", michael@0: "IS_WIN32", michael@0: "IS_MAC", michael@0: "log", michael@0: "runApp", michael@0: "Process", michael@0: "addCommonOptions", michael@0: "initializeProfile", michael@0: "DIST_BIN", michael@0: "DEFAULT_APP", michael@0: "CERTS_SRC_DIR", michael@0: "environment", michael@0: "IS_TEST_BUILD", michael@0: "IS_DEBUG_BUILD", michael@0: "DEFAULT_TIMEOUT", michael@0: ] michael@0: michael@0: class Process(subprocess.Popen): michael@0: """ michael@0: Represents our view of a subprocess. michael@0: It adds a kill() method which allows it to be stopped explicitly. michael@0: """ michael@0: michael@0: def __init__(self, michael@0: args, michael@0: bufsize=0, michael@0: executable=None, michael@0: stdin=None, michael@0: stdout=None, michael@0: stderr=None, michael@0: preexec_fn=None, michael@0: close_fds=False, michael@0: shell=False, michael@0: cwd=None, michael@0: env=None, michael@0: universal_newlines=False, michael@0: startupinfo=None, michael@0: creationflags=0): michael@0: _log.info("INFO | automation.py | Launching: %s", subprocess.list2cmdline(args)) michael@0: subprocess.Popen.__init__(self, args, bufsize, executable, michael@0: stdin, stdout, stderr, michael@0: preexec_fn, close_fds, michael@0: shell, cwd, env, michael@0: universal_newlines, startupinfo, creationflags) michael@0: self.log = _log michael@0: michael@0: def kill(self): michael@0: if Automation().IS_WIN32: michael@0: import platform michael@0: pid = "%i" % self.pid michael@0: if platform.release() == "2000": michael@0: # Windows 2000 needs 'kill.exe' from the michael@0: #'Windows 2000 Resource Kit tools'. (See bug 475455.) michael@0: try: michael@0: subprocess.Popen(["kill", "-f", pid]).wait() michael@0: except: michael@0: self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid) michael@0: else: michael@0: # Windows XP and later. michael@0: subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait() michael@0: else: michael@0: os.kill(self.pid, signal.SIGKILL) michael@0: michael@0: def readLocations(self, locationsPath = "server-locations.txt"): michael@0: """ michael@0: Reads the locations at which the Mochitest HTTP server is available from michael@0: server-locations.txt. michael@0: """ michael@0: michael@0: locationFile = codecs.open(locationsPath, "r", "UTF-8") michael@0: michael@0: # Perhaps more detail than necessary, but it's the easiest way to make sure michael@0: # we get exactly the format we want. See server-locations.txt for the exact michael@0: # format guaranteed here. michael@0: lineRe = re.compile(r"^(?P[a-z][-a-z0-9+.]*)" michael@0: r"://" michael@0: r"(?P" michael@0: r"\d+\.\d+\.\d+\.\d+" michael@0: r"|" michael@0: r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*" michael@0: r"[a-z](?:[-a-z0-9]*[a-z0-9])?" michael@0: r")" michael@0: r":" michael@0: r"(?P\d+)" michael@0: r"(?:" michael@0: r"\s+" michael@0: r"(?P\S+(?:,\S+)*)" michael@0: r")?$") michael@0: locations = [] michael@0: lineno = 0 michael@0: seenPrimary = False michael@0: for line in locationFile: michael@0: lineno += 1 michael@0: if line.startswith("#") or line == "\n": michael@0: continue michael@0: michael@0: match = lineRe.match(line) michael@0: if not match: michael@0: raise SyntaxError(lineno) michael@0: michael@0: options = match.group("options") michael@0: if options: michael@0: options = options.split(",") michael@0: if "primary" in options: michael@0: if seenPrimary: michael@0: raise SyntaxError(lineno, "multiple primary locations") michael@0: seenPrimary = True michael@0: else: michael@0: options = [] michael@0: michael@0: locations.append(Location(match.group("scheme"), match.group("host"), michael@0: match.group("port"), options)) michael@0: michael@0: if not seenPrimary: michael@0: raise SyntaxError(lineno + 1, "missing primary location") michael@0: michael@0: return locations michael@0: michael@0: def setupPermissionsDatabase(self, profileDir, permissions): michael@0: # Included for reftest compatibility; michael@0: # see https://bugzilla.mozilla.org/show_bug.cgi?id=688667 michael@0: michael@0: # Open database and create table michael@0: permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite")) michael@0: cursor = permDB.cursor(); michael@0: michael@0: cursor.execute("PRAGMA user_version=3"); michael@0: michael@0: # SQL copied from nsPermissionManager.cpp michael@0: cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts ( michael@0: id INTEGER PRIMARY KEY, michael@0: host TEXT, michael@0: type TEXT, michael@0: permission INTEGER, michael@0: expireType INTEGER, michael@0: expireTime INTEGER, michael@0: appId INTEGER, michael@0: isInBrowserElement INTEGER)""") michael@0: michael@0: # Insert desired permissions michael@0: for perm in permissions.keys(): michael@0: for host,allow in permissions[perm]: michael@0: cursor.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)", michael@0: (host, perm, 1 if allow else 2)) michael@0: michael@0: # Commit and close michael@0: permDB.commit() michael@0: cursor.close() michael@0: michael@0: def initializeProfile(self, profileDir, michael@0: extraPrefs=None, michael@0: useServerLocations=False, michael@0: prefsPath=_DEFAULT_PREFERENCE_FILE, michael@0: appsPath=_DEFAULT_APPS_FILE, michael@0: addons=None): michael@0: " Sets up the standard testing profile." michael@0: michael@0: extraPrefs = extraPrefs or [] michael@0: michael@0: # create the profile michael@0: prefs = {} michael@0: locations = None michael@0: if useServerLocations: michael@0: locations = ServerLocations() michael@0: locations.read(os.path.abspath('server-locations.txt'), True) michael@0: else: michael@0: prefs['network.proxy.type'] = 0 michael@0: michael@0: prefs.update(Preferences.read_prefs(prefsPath)) michael@0: michael@0: for v in extraPrefs: michael@0: thispref = v.split("=", 1) michael@0: if len(thispref) < 2: michael@0: print "Error: syntax error in --setpref=" + v michael@0: sys.exit(1) michael@0: prefs[thispref[0]] = thispref[1] michael@0: michael@0: michael@0: interpolation = {"server": "%s:%s" % (self.webServer, self.httpPort)} michael@0: prefs = json.loads(json.dumps(prefs) % interpolation) michael@0: for pref in prefs: michael@0: prefs[pref] = Preferences.cast(prefs[pref]) michael@0: michael@0: # load apps michael@0: apps = None michael@0: if appsPath and os.path.exists(appsPath): michael@0: with open(appsPath, 'r') as apps_file: michael@0: apps = json.load(apps_file) michael@0: michael@0: proxy = {'remote': str(self.webServer), michael@0: 'http': str(self.httpPort), michael@0: 'https': str(self.sslPort), michael@0: # use SSL port for legacy compatibility; see michael@0: # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66 michael@0: # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221 michael@0: # 'ws': str(self.webSocketPort) michael@0: 'ws': str(self.sslPort) michael@0: } michael@0: michael@0: # return profile object michael@0: profile = Profile(profile=profileDir, michael@0: addons=addons, michael@0: locations=locations, michael@0: preferences=prefs, michael@0: restore=False, michael@0: apps=apps, michael@0: proxy=proxy) michael@0: return profile michael@0: michael@0: def addCommonOptions(self, parser): michael@0: "Adds command-line options which are common to mochitest and reftest." michael@0: michael@0: parser.add_option("--setpref", michael@0: action = "append", type = "string", michael@0: default = [], michael@0: dest = "extraPrefs", metavar = "PREF=VALUE", michael@0: help = "defines an extra user preference") michael@0: michael@0: def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath): michael@0: pwfilePath = os.path.join(profileDir, ".crtdbpw") michael@0: pwfile = open(pwfilePath, "w") michael@0: pwfile.write("\n") michael@0: pwfile.close() michael@0: michael@0: # Create head of the ssltunnel configuration file michael@0: sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg") michael@0: sslTunnelConfig = open(sslTunnelConfigPath, "w") michael@0: michael@0: sslTunnelConfig.write("httpproxy:1\n") michael@0: sslTunnelConfig.write("certdbdir:%s\n" % certPath) michael@0: sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort) michael@0: sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort)) michael@0: sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort) michael@0: michael@0: # Configure automatic certificate and bind custom certificates, client authentication michael@0: locations = self.readLocations() michael@0: locations.pop(0) michael@0: for loc in locations: michael@0: if loc.scheme == "https" and "nocert" not in loc.options: michael@0: customCertRE = re.compile("^cert=(?P[0-9a-zA-Z_ ]+)") michael@0: clientAuthRE = re.compile("^clientauth=(?P[a-z]+)") michael@0: redirRE = re.compile("^redir=(?P[0-9a-zA-Z_ .]+)") michael@0: for option in loc.options: michael@0: match = customCertRE.match(option) michael@0: if match: michael@0: customcert = match.group("nickname"); michael@0: sslTunnelConfig.write("listen:%s:%s:%s:%s\n" % michael@0: (loc.host, loc.port, self.sslPort, customcert)) michael@0: michael@0: match = clientAuthRE.match(option) michael@0: if match: michael@0: clientauth = match.group("clientauth"); michael@0: sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" % michael@0: (loc.host, loc.port, self.sslPort, clientauth)) michael@0: michael@0: match = redirRE.match(option) michael@0: if match: michael@0: redirhost = match.group("redirhost") michael@0: sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" % michael@0: (loc.host, loc.port, self.sslPort, redirhost)) michael@0: michael@0: sslTunnelConfig.close() michael@0: michael@0: # Pre-create the certification database for the profile michael@0: env = self.environment(xrePath = xrePath) michael@0: certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX) michael@0: pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX) michael@0: michael@0: status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait() michael@0: automationutils.printstatus(status, "certutil") michael@0: if status != 0: michael@0: return status michael@0: michael@0: # Walk the cert directory and add custom CAs and client certs michael@0: files = os.listdir(certPath) michael@0: for item in files: michael@0: root, ext = os.path.splitext(item) michael@0: if ext == ".ca": michael@0: trustBits = "CT,," michael@0: if root.endswith("-object"): michael@0: trustBits = "CT,,CT" michael@0: status = self.Process([certutil, "-A", "-i", os.path.join(certPath, item), michael@0: "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits], michael@0: env = env).wait() michael@0: automationutils.printstatus(status, "certutil") michael@0: if ext == ".client": michael@0: status = self.Process([pk12util, "-i", os.path.join(certPath, item), "-w", michael@0: pwfilePath, "-d", profileDir], michael@0: env = env).wait() michael@0: automationutils.printstatus(status, "pk12util") michael@0: michael@0: os.unlink(pwfilePath) michael@0: return 0 michael@0: michael@0: def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None): michael@0: if xrePath == None: michael@0: xrePath = self.DIST_BIN michael@0: if env == None: michael@0: env = dict(os.environ) michael@0: michael@0: ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath)) michael@0: dmdLibrary = None michael@0: preloadEnvVar = None michael@0: if self.UNIXISH or self.IS_MAC: michael@0: envVar = "LD_LIBRARY_PATH" michael@0: preloadEnvVar = "LD_PRELOAD" michael@0: if self.IS_MAC: michael@0: envVar = "DYLD_LIBRARY_PATH" michael@0: dmdLibrary = "libdmd.dylib" michael@0: else: # unixish michael@0: env['MOZILLA_FIVE_HOME'] = xrePath michael@0: dmdLibrary = "libdmd.so" michael@0: if envVar in env: michael@0: ldLibraryPath = ldLibraryPath + ":" + env[envVar] michael@0: env[envVar] = ldLibraryPath michael@0: elif self.IS_WIN32: michael@0: env["PATH"] = env["PATH"] + ";" + str(ldLibraryPath) michael@0: dmdLibrary = "dmd.dll" michael@0: preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB" michael@0: michael@0: if dmdPath and dmdLibrary and preloadEnvVar: michael@0: env['DMD'] = '1' michael@0: env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary) michael@0: michael@0: if crashreporter and not debugger: michael@0: env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' michael@0: env['MOZ_CRASHREPORTER'] = '1' michael@0: else: michael@0: env['MOZ_CRASHREPORTER_DISABLE'] = '1' michael@0: michael@0: # Crash on non-local network connections. michael@0: env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1' michael@0: michael@0: env['GNOME_DISABLE_CRASH_DIALOG'] = '1' michael@0: env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1' michael@0: env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1' michael@0: michael@0: # Set WebRTC logging in case it is not set yet michael@0: env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:3') michael@0: env.setdefault('R_LOG_LEVEL', '5') michael@0: env.setdefault('R_LOG_DESTINATION', 'stderr') michael@0: env.setdefault('R_LOG_VERBOSE', '1') michael@0: michael@0: # ASan specific environment stuff michael@0: if self.IS_ASAN and (self.IS_LINUX or self.IS_MAC): michael@0: # Symbolizer support michael@0: llvmsym = os.path.join(xrePath, "llvm-symbolizer") michael@0: if os.path.isfile(llvmsym): michael@0: env["ASAN_SYMBOLIZER_PATH"] = llvmsym michael@0: self.log.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym) michael@0: michael@0: try: michael@0: totalMemory = int(os.popen("free").readlines()[1].split()[1]) michael@0: michael@0: # Only 4 GB RAM or less available? Use custom ASan options to reduce michael@0: # the amount of resources required to do the tests. Standard options michael@0: # will otherwise lead to OOM conditions on the current test slaves. michael@0: if totalMemory <= 1024 * 1024 * 4: michael@0: self.log.info("INFO | automation.py | ASan running in low-memory configuration") michael@0: env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5" michael@0: else: michael@0: self.log.info("INFO | automation.py | ASan running in default memory configuration") michael@0: except OSError,err: michael@0: self.log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror) michael@0: except: michael@0: self.log.info("Failed determine available memory, disabling ASan low-memory configuration") michael@0: michael@0: return env michael@0: michael@0: def killPid(self, pid): michael@0: try: michael@0: os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM)) michael@0: except WindowsError: michael@0: self.log.info("Failed to kill process %d." % pid) michael@0: michael@0: if IS_WIN32: michael@0: PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe michael@0: GetLastError = ctypes.windll.kernel32.GetLastError michael@0: michael@0: def readWithTimeout(self, f, timeout): michael@0: """ michael@0: Try to read a line of output from the file object |f|. |f| must be a michael@0: pipe, like the |stdout| member of a subprocess.Popen object created michael@0: with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout| michael@0: is True if the read timed out, and False otherwise. If no output is michael@0: received within |timeout| seconds, returns a blank line. michael@0: """ michael@0: michael@0: if timeout is None: michael@0: timeout = 0 michael@0: michael@0: x = msvcrt.get_osfhandle(f.fileno()) michael@0: l = ctypes.c_long() michael@0: done = time.time() + timeout michael@0: michael@0: buffer = "" michael@0: while timeout == 0 or time.time() < done: michael@0: if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0: michael@0: err = self.GetLastError() michael@0: if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE michael@0: return ('', False) michael@0: else: michael@0: self.log.error("readWithTimeout got error: %d", err) michael@0: # read a character at a time, checking for eol. Return once we get there. michael@0: index = 0 michael@0: while index < l.value: michael@0: char = f.read(1) michael@0: buffer += char michael@0: if char == '\n': michael@0: return (buffer, False) michael@0: index = index + 1 michael@0: time.sleep(0.01) michael@0: return (buffer, True) michael@0: michael@0: def isPidAlive(self, pid): michael@0: STILL_ACTIVE = 259 michael@0: PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 michael@0: pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) michael@0: if not pHandle: michael@0: return False michael@0: pExitCode = ctypes.wintypes.DWORD() michael@0: ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode)) michael@0: ctypes.windll.kernel32.CloseHandle(pHandle) michael@0: return pExitCode.value == STILL_ACTIVE michael@0: michael@0: else: michael@0: michael@0: def readWithTimeout(self, f, timeout): michael@0: """Try to read a line of output from the file object |f|. If no output michael@0: is received within |timeout| seconds, return a blank line. michael@0: Returns a tuple (line, did_timeout), where |did_timeout| is True michael@0: if the read timed out, and False otherwise.""" michael@0: (r, w, e) = select.select([f], [], [], timeout) michael@0: if len(r) == 0: michael@0: return ('', True) michael@0: return (f.readline(), False) michael@0: michael@0: def isPidAlive(self, pid): michael@0: try: michael@0: # kill(pid, 0) checks for a valid PID without actually sending a signal michael@0: # The method throws OSError if the PID is invalid, which we catch below. michael@0: os.kill(pid, 0) michael@0: michael@0: # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if michael@0: # the process terminates before we get to this point. michael@0: wpid, wstatus = os.waitpid(pid, os.WNOHANG) michael@0: return wpid == 0 michael@0: except OSError, err: michael@0: # Catch the errors we might expect from os.kill/os.waitpid, michael@0: # and re-raise any others michael@0: if err.errno == errno.ESRCH or err.errno == errno.ECHILD: michael@0: return False michael@0: raise michael@0: michael@0: def dumpScreen(self, utilityPath): michael@0: if self.haveDumpedScreen: michael@0: self.log.info("Not taking screenshot here: see the one that was previously logged") michael@0: return michael@0: michael@0: self.haveDumpedScreen = True; michael@0: automationutils.dumpScreen(utilityPath) michael@0: michael@0: michael@0: def killAndGetStack(self, processPID, utilityPath, debuggerInfo): michael@0: """Kill the process, preferrably in a way that gets us a stack trace. michael@0: Also attempts to obtain a screenshot before killing the process.""" michael@0: if not debuggerInfo: michael@0: self.dumpScreen(utilityPath) michael@0: self.killAndGetStackNoScreenshot(processPID, utilityPath, debuggerInfo) michael@0: michael@0: def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo): michael@0: """Kill the process, preferrably in a way that gets us a stack trace.""" michael@0: if self.CRASHREPORTER and not debuggerInfo: michael@0: if not self.IS_WIN32: michael@0: # ABRT will get picked up by Breakpad's signal handler michael@0: os.kill(processPID, signal.SIGABRT) michael@0: return michael@0: else: michael@0: # We should have a "crashinject" program in our utility path michael@0: crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe")) michael@0: if os.path.exists(crashinject): michael@0: status = subprocess.Popen([crashinject, str(processPID)]).wait() michael@0: automationutils.printstatus(status, "crashinject") michael@0: if status == 0: michael@0: return michael@0: self.log.info("Can't trigger Breakpad, just killing process") michael@0: self.killPid(processPID) michael@0: michael@0: def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath): michael@0: """ Look for timeout or crashes and return the status after the process terminates """ michael@0: stackFixerProcess = None michael@0: stackFixerFunction = None michael@0: didTimeout = False michael@0: hitMaxTime = False michael@0: if proc.stdout is None: michael@0: self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection") michael@0: else: michael@0: logsource = proc.stdout michael@0: michael@0: if self.IS_DEBUG_BUILD and symbolsPath and os.path.exists(symbolsPath): michael@0: # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files) michael@0: # This method is preferred for Tinderbox builds, since native symbols may have been stripped. michael@0: sys.path.insert(0, utilityPath) michael@0: import fix_stack_using_bpsyms as stackFixerModule michael@0: stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath) michael@0: del sys.path[0] michael@0: elif self.IS_DEBUG_BUILD and self.IS_MAC and False: michael@0: # Run each line through a function in fix_macosx_stack.py (uses atos) michael@0: sys.path.insert(0, utilityPath) michael@0: import fix_macosx_stack as stackFixerModule michael@0: stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line) michael@0: del sys.path[0] michael@0: elif self.IS_DEBUG_BUILD and self.IS_LINUX: michael@0: # Run logsource through fix-linux-stack.pl (uses addr2line) michael@0: # This method is preferred for developer machines, so we don't have to run "make buildsymbols". michael@0: stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")], michael@0: stdin=logsource, michael@0: stdout=subprocess.PIPE) michael@0: logsource = stackFixerProcess.stdout michael@0: michael@0: # With metro browser runs this script launches the metro test harness which launches the browser. michael@0: # The metro test harness hands back the real browser process id via log output which we need to michael@0: # pick up on and parse out. This variable tracks the real browser process id if we find it. michael@0: browserProcessId = -1 michael@0: michael@0: (line, didTimeout) = self.readWithTimeout(logsource, timeout) michael@0: while line != "" and not didTimeout: michael@0: if stackFixerFunction: michael@0: line = stackFixerFunction(line) michael@0: self.log.info(line.rstrip().decode("UTF-8", "ignore")) michael@0: if "TEST-START" in line and "|" in line: michael@0: self.lastTestSeen = line.split("|")[1].strip() michael@0: if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line: michael@0: self.dumpScreen(utilityPath) michael@0: michael@0: (line, didTimeout) = self.readWithTimeout(logsource, timeout) michael@0: michael@0: if "METRO_BROWSER_PROCESS" in line: michael@0: index = line.find("=") michael@0: if index: michael@0: browserProcessId = line[index+1:].rstrip() michael@0: self.log.info("INFO | automation.py | metro browser sub process id detected: %s", browserProcessId) michael@0: michael@0: if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime): michael@0: # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait(). michael@0: hitMaxTime = True michael@0: self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime)) michael@0: self.killAndGetStack(proc.pid, utilityPath, debuggerInfo) michael@0: if didTimeout: michael@0: if line: michael@0: self.log.info(line.rstrip().decode("UTF-8", "ignore")) michael@0: self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout)) michael@0: if browserProcessId == -1: michael@0: browserProcessId = proc.pid michael@0: self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo) michael@0: michael@0: status = proc.wait() michael@0: automationutils.printstatus(status, "Main app process") michael@0: if status == 0: michael@0: self.lastTestSeen = "Main app process exited normally" michael@0: if status != 0 and not didTimeout and not hitMaxTime: michael@0: self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status) michael@0: if stackFixerProcess is not None: michael@0: fixerStatus = stackFixerProcess.wait() michael@0: automationutils.printstatus(status, "stackFixerProcess") michael@0: if fixerStatus != 0 and not didTimeout and not hitMaxTime: michael@0: self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus) michael@0: return status michael@0: michael@0: def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): michael@0: """ build the application command line """ michael@0: michael@0: cmd = os.path.abspath(app) michael@0: if self.IS_MAC and not self.IS_CAMINO and os.path.exists(cmd + "-bin"): michael@0: # Prefer 'app-bin' in case 'app' is a shell script. michael@0: # We can remove this hack once bug 673899 etc are fixed. michael@0: cmd += "-bin" michael@0: michael@0: args = [] michael@0: michael@0: if debuggerInfo: michael@0: args.extend(debuggerInfo["args"]) michael@0: args.append(cmd) michael@0: cmd = os.path.abspath(debuggerInfo["path"]) michael@0: michael@0: if self.IS_MAC: michael@0: args.append("-foreground") michael@0: michael@0: if self.IS_CYGWIN: michael@0: profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"") michael@0: else: michael@0: profileDirectory = profileDir + "/" michael@0: michael@0: args.extend(("-no-remote", "-profile", profileDirectory)) michael@0: if testURL is not None: michael@0: if self.IS_CAMINO: michael@0: args.extend(("-url", testURL)) michael@0: else: michael@0: args.append((testURL)) michael@0: args.extend(extraArgs) michael@0: return cmd, args michael@0: michael@0: def checkForZombies(self, processLog, utilityPath, debuggerInfo): michael@0: """ Look for hung processes """ michael@0: if not os.path.exists(processLog): michael@0: self.log.info('Automation Error: PID log not found: %s', processLog) michael@0: # Whilst no hung process was found, the run should still display as a failure michael@0: return True michael@0: michael@0: foundZombie = False michael@0: self.log.info('INFO | zombiecheck | Reading PID log: %s', processLog) michael@0: processList = [] michael@0: pidRE = re.compile(r'launched child process (\d+)$') michael@0: processLogFD = open(processLog) michael@0: for line in processLogFD: michael@0: self.log.info(line.rstrip()) michael@0: m = pidRE.search(line) michael@0: if m: michael@0: processList.append(int(m.group(1))) michael@0: processLogFD.close() michael@0: michael@0: for processPID in processList: michael@0: self.log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID) michael@0: if self.isPidAlive(processPID): michael@0: foundZombie = True michael@0: self.log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID) michael@0: self.killAndGetStack(processPID, utilityPath, debuggerInfo) michael@0: return foundZombie michael@0: michael@0: def checkForCrashes(self, minidumpDir, symbolsPath): michael@0: return mozcrash.check_for_crashes(minidumpDir, symbolsPath, test_name=self.lastTestSeen) michael@0: michael@0: def runApp(self, testURL, env, app, profileDir, extraArgs, michael@0: runSSLTunnel = False, utilityPath = None, michael@0: xrePath = None, certPath = None, michael@0: debuggerInfo = None, symbolsPath = None, michael@0: timeout = -1, maxTime = None, onLaunch = None, michael@0: webapprtChrome = False, hide_subtests=None, screenshotOnFail=False): michael@0: """ michael@0: Run the app, log the duration it took to execute, return the status code. michael@0: Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds. michael@0: """ michael@0: michael@0: if utilityPath == None: michael@0: utilityPath = self.DIST_BIN michael@0: if xrePath == None: michael@0: xrePath = self.DIST_BIN michael@0: if certPath == None: michael@0: certPath = self.CERTS_SRC_DIR michael@0: if timeout == -1: michael@0: timeout = self.DEFAULT_TIMEOUT michael@0: michael@0: # copy env so we don't munge the caller's environment michael@0: env = dict(env); michael@0: env["NO_EM_RESTART"] = "1" michael@0: tmpfd, processLog = tempfile.mkstemp(suffix='pidlog') michael@0: os.close(tmpfd) michael@0: env["MOZ_PROCESS_LOG"] = processLog michael@0: michael@0: if self.IS_TEST_BUILD and runSSLTunnel: michael@0: # create certificate database for the profile michael@0: certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath) michael@0: if certificateStatus != 0: michael@0: self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed") michael@0: return certificateStatus michael@0: michael@0: # start ssltunnel to provide https:// URLs capability michael@0: ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX) michael@0: ssltunnelProcess = self.Process([ssltunnel, michael@0: os.path.join(profileDir, "ssltunnel.cfg")], michael@0: env = self.environment(xrePath = xrePath)) michael@0: self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid) michael@0: michael@0: cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs) michael@0: startTime = datetime.now() michael@0: michael@0: if debuggerInfo and debuggerInfo["interactive"]: michael@0: # If an interactive debugger is attached, don't redirect output, michael@0: # don't use timeouts, and don't capture ctrl-c. michael@0: timeout = None michael@0: maxTime = None michael@0: outputPipe = None michael@0: signal.signal(signal.SIGINT, lambda sigid, frame: None) michael@0: else: michael@0: outputPipe = subprocess.PIPE michael@0: michael@0: self.lastTestSeen = "automation.py" michael@0: proc = self.Process([cmd] + args, michael@0: env = self.environment(env, xrePath = xrePath, michael@0: crashreporter = not debuggerInfo), michael@0: stdout = outputPipe, michael@0: stderr = subprocess.STDOUT) michael@0: self.log.info("INFO | automation.py | Application pid: %d", proc.pid) michael@0: michael@0: if onLaunch is not None: michael@0: # Allow callers to specify an onLaunch callback to be fired after the michael@0: # app is launched. michael@0: onLaunch() michael@0: michael@0: status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath) michael@0: self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime)) michael@0: michael@0: # Do a final check for zombie child processes. michael@0: zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo) michael@0: michael@0: crashed = self.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath) michael@0: michael@0: if crashed or zombieProcesses: michael@0: status = 1 michael@0: michael@0: if os.path.exists(processLog): michael@0: os.unlink(processLog) michael@0: michael@0: if self.IS_TEST_BUILD and runSSLTunnel: michael@0: ssltunnelProcess.kill() michael@0: michael@0: return status michael@0: michael@0: def getExtensionIDFromRDF(self, rdfSource): michael@0: """ michael@0: Retrieves the extension id from an install.rdf file (or string). michael@0: """ michael@0: from xml.dom.minidom import parse, parseString, Node michael@0: michael@0: if isinstance(rdfSource, file): michael@0: document = parse(rdfSource) michael@0: else: michael@0: document = parseString(rdfSource) michael@0: michael@0: # Find the element. There can be multiple tags michael@0: # within tags, so we have to check this way. michael@0: for rdfChild in document.documentElement.childNodes: michael@0: if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description": michael@0: for descChild in rdfChild.childNodes: michael@0: if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id": michael@0: return descChild.childNodes[0].data michael@0: michael@0: return None michael@0: michael@0: def installExtension(self, extensionSource, profileDir, extensionID = None): michael@0: """ michael@0: Copies an extension into the extensions directory of the given profile. michael@0: extensionSource - the source location of the extension files. This can be either michael@0: a directory or a path to an xpi file. michael@0: profileDir - the profile directory we are copying into. We will create the michael@0: "extensions" directory there if it doesn't exist. michael@0: extensionID - the id of the extension to be used as the containing directory for the michael@0: extension, if extensionSource is a directory, i.e. michael@0: this is the name of the folder in the /extensions/ michael@0: """ michael@0: if not os.path.isdir(profileDir): michael@0: self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir) michael@0: return michael@0: michael@0: installRDFFilename = "install.rdf" michael@0: michael@0: extensionsRootDir = os.path.join(profileDir, "extensions", "staged") michael@0: if not os.path.isdir(extensionsRootDir): michael@0: os.makedirs(extensionsRootDir) michael@0: michael@0: if os.path.isfile(extensionSource): michael@0: reader = automationutils.ZipFileReader(extensionSource) michael@0: michael@0: for filename in reader.namelist(): michael@0: # Sanity check the zip file. michael@0: if os.path.isabs(filename): michael@0: self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi") michael@0: return michael@0: michael@0: # We may need to dig the extensionID out of the zip file... michael@0: if extensionID is None and filename == installRDFFilename: michael@0: extensionID = self.getExtensionIDFromRDF(reader.read(filename)) michael@0: michael@0: # We must know the extensionID now. michael@0: if extensionID is None: michael@0: self.log.info("INFO | automation.py | Cannot install extension, missing extensionID") michael@0: return michael@0: michael@0: # Make the extension directory. michael@0: extensionDir = os.path.join(extensionsRootDir, extensionID) michael@0: os.mkdir(extensionDir) michael@0: michael@0: # Extract all files. michael@0: reader.extractall(extensionDir) michael@0: michael@0: elif os.path.isdir(extensionSource): michael@0: if extensionID is None: michael@0: filename = os.path.join(extensionSource, installRDFFilename) michael@0: if os.path.isfile(filename): michael@0: with open(filename, "r") as installRDF: michael@0: extensionID = self.getExtensionIDFromRDF(installRDF) michael@0: michael@0: if extensionID is None: michael@0: self.log.info("INFO | automation.py | Cannot install extension, missing extensionID") michael@0: return michael@0: michael@0: # Copy extension tree into its own directory. michael@0: # "destination directory must not already exist". michael@0: shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID)) michael@0: michael@0: else: michael@0: self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource) michael@0: michael@0: def elf_arm(self, filename): michael@0: data = open(filename, 'rb').read(20) michael@0: return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM michael@0: