gecko/build/automation.py.in

601 lines
22 KiB
Python

#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import with_statement
import logging
import os
import re
import select
import signal
import subprocess
import sys
import tempfile
from datetime import datetime, timedelta
SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
sys.path.insert(0, SCRIPT_DIR)
# --------------------------------------------------------------
# TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
# These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR
here = os.path.dirname(os.path.realpath(__file__))
mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase'))
if os.path.isdir(mozbase):
for package in os.listdir(mozbase):
package_path = os.path.join(mozbase, package)
if package_path not in sys.path:
sys.path.append(package_path)
import mozcrash
from mozscreenshot import printstatus, dump_screen
# ---------------------------------------------------------------
_DEFAULT_PREFERENCE_FILE = os.path.join(SCRIPT_DIR, 'prefs_general.js')
_DEFAULT_APPS_FILE = os.path.join(SCRIPT_DIR, 'webapps_mochitest.json')
_DEFAULT_WEB_SERVER = "127.0.0.1"
_DEFAULT_HTTP_PORT = 8888
_DEFAULT_SSL_PORT = 4443
_DEFAULT_WEBSOCKET_PORT = 9988
# from nsIPrincipal.idl
_APP_STATUS_NOT_INSTALLED = 0
_APP_STATUS_INSTALLED = 1
_APP_STATUS_PRIVILEGED = 2
_APP_STATUS_CERTIFIED = 3
#expand _DIST_BIN = __XPC_BIN_PATH__
#expand _IS_WIN32 = len("__WIN32__") != 0
#expand _IS_MAC = __IS_MAC__ != 0
#expand _IS_LINUX = __IS_LINUX__ != 0
#ifdef IS_CYGWIN
#expand _IS_CYGWIN = __IS_CYGWIN__ == 1
#else
_IS_CYGWIN = False
#endif
#expand _BIN_SUFFIX = __BIN_SUFFIX__
#expand _DEFAULT_APP = "./" + __BROWSER_PATH__
#expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
#expand _IS_TEST_BUILD = __IS_TEST_BUILD__
#expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
#expand _CRASHREPORTER = __CRASHREPORTER__ == 1
#expand _IS_ASAN = __IS_ASAN__ == 1
if _IS_WIN32:
import ctypes, ctypes.wintypes, time, msvcrt
else:
import errno
def resetGlobalLog(log):
while _log.handlers:
_log.removeHandler(_log.handlers[0])
handler = logging.StreamHandler(log)
_log.setLevel(logging.INFO)
_log.addHandler(handler)
# We use the logging system here primarily because it'll handle multiple
# threads, which is needed to process the output of the server and application
# processes simultaneously.
_log = logging.getLogger()
resetGlobalLog(sys.stdout)
#################
# PROFILE SETUP #
#################
class Automation(object):
"""
Runs the browser from a script, and provides useful utilities
for setting up the browser environment.
"""
DIST_BIN = _DIST_BIN
IS_WIN32 = _IS_WIN32
IS_MAC = _IS_MAC
IS_LINUX = _IS_LINUX
IS_CYGWIN = _IS_CYGWIN
BIN_SUFFIX = _BIN_SUFFIX
UNIXISH = not IS_WIN32 and not IS_MAC
DEFAULT_APP = _DEFAULT_APP
CERTS_SRC_DIR = _CERTS_SRC_DIR
IS_TEST_BUILD = _IS_TEST_BUILD
IS_DEBUG_BUILD = _IS_DEBUG_BUILD
CRASHREPORTER = _CRASHREPORTER
IS_ASAN = _IS_ASAN
# timeout, in seconds
DEFAULT_TIMEOUT = 60.0
DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
def __init__(self):
self.log = _log
self.lastTestSeen = "automation.py"
self.haveDumpedScreen = False
def setServerInfo(self,
webServer = _DEFAULT_WEB_SERVER,
httpPort = _DEFAULT_HTTP_PORT,
sslPort = _DEFAULT_SSL_PORT,
webSocketPort = _DEFAULT_WEBSOCKET_PORT):
self.webServer = webServer
self.httpPort = httpPort
self.sslPort = sslPort
self.webSocketPort = webSocketPort
@property
def __all__(self):
return [
"UNIXISH",
"IS_WIN32",
"IS_MAC",
"log",
"runApp",
"Process",
"DIST_BIN",
"DEFAULT_APP",
"CERTS_SRC_DIR",
"environment",
"IS_TEST_BUILD",
"IS_DEBUG_BUILD",
"DEFAULT_TIMEOUT",
]
class Process(subprocess.Popen):
"""
Represents our view of a subprocess.
It adds a kill() method which allows it to be stopped explicitly.
"""
def __init__(self,
args,
bufsize=0,
executable=None,
stdin=None,
stdout=None,
stderr=None,
preexec_fn=None,
close_fds=False,
shell=False,
cwd=None,
env=None,
universal_newlines=False,
startupinfo=None,
creationflags=0):
_log.info("INFO | automation.py | Launching: %s", subprocess.list2cmdline(args))
subprocess.Popen.__init__(self, args, bufsize, executable,
stdin, stdout, stderr,
preexec_fn, close_fds,
shell, cwd, env,
universal_newlines, startupinfo, creationflags)
self.log = _log
def kill(self):
if Automation().IS_WIN32:
import platform
pid = "%i" % self.pid
if platform.release() == "2000":
# Windows 2000 needs 'kill.exe' from the
#'Windows 2000 Resource Kit tools'. (See bug 475455.)
try:
subprocess.Popen(["kill", "-f", pid]).wait()
except:
self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
else:
# Windows XP and later.
subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
else:
os.kill(self.pid, signal.SIGKILL)
def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None, lsanPath=None):
if xrePath == None:
xrePath = self.DIST_BIN
if env == None:
env = dict(os.environ)
ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
dmdLibrary = None
preloadEnvVar = None
if self.UNIXISH or self.IS_MAC:
envVar = "LD_LIBRARY_PATH"
preloadEnvVar = "LD_PRELOAD"
if self.IS_MAC:
envVar = "DYLD_LIBRARY_PATH"
dmdLibrary = "libdmd.dylib"
else: # unixish
env['MOZILLA_FIVE_HOME'] = xrePath
dmdLibrary = "libdmd.so"
if envVar in env:
ldLibraryPath = ldLibraryPath + ":" + env[envVar]
env[envVar] = ldLibraryPath
elif self.IS_WIN32:
env["PATH"] = env["PATH"] + ";" + str(ldLibraryPath)
dmdLibrary = "dmd.dll"
preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB"
if dmdPath and dmdLibrary and preloadEnvVar:
env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary)
if crashreporter and not debugger:
env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
env['MOZ_CRASHREPORTER'] = '1'
else:
env['MOZ_CRASHREPORTER_DISABLE'] = '1'
# Crash on non-local network connections by default.
# MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
# enable non-local connections for the purposes of local testing. Don't
# override the user's choice here. See bug 1049688.
env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1')
env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
# Set WebRTC logging in case it is not set yet
env.setdefault('NSPR_LOG_MODULES', 'signaling:3,mtransport:4,datachannel:4,jsep:4,MediaPipelineFactory:4')
env.setdefault('R_LOG_LEVEL', '6')
env.setdefault('R_LOG_DESTINATION', 'stderr')
env.setdefault('R_LOG_VERBOSE', '1')
# ASan specific environment stuff
if self.IS_ASAN and (self.IS_LINUX or self.IS_MAC):
# Symbolizer support
llvmsym = os.path.join(xrePath, "llvm-symbolizer")
if os.path.isfile(llvmsym):
env["ASAN_SYMBOLIZER_PATH"] = llvmsym
self.log.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym)
else:
self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Failed to find ASan symbolizer at %s", llvmsym)
try:
totalMemory = int(os.popen("free").readlines()[1].split()[1])
# Only 4 GB RAM or less available? Use custom ASan options to reduce
# the amount of resources required to do the tests. Standard options
# will otherwise lead to OOM conditions on the current test slaves.
if totalMemory <= 1024 * 1024 * 4:
self.log.info("INFO | automation.py | ASan running in low-memory configuration")
env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5"
else:
self.log.info("INFO | automation.py | ASan running in default memory configuration")
except OSError,err:
self.log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror)
except:
self.log.info("Failed determine available memory, disabling ASan low-memory configuration")
return env
def killPid(self, pid):
try:
os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
except WindowsError:
self.log.info("Failed to kill process %d." % pid)
if IS_WIN32:
PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
GetLastError = ctypes.windll.kernel32.GetLastError
def readWithTimeout(self, f, timeout):
"""
Try to read a line of output from the file object |f|. |f| must be a
pipe, like the |stdout| member of a subprocess.Popen object created
with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout|
is True if the read timed out, and False otherwise. If no output is
received within |timeout| seconds, returns a blank line.
"""
if timeout is None:
timeout = 0
x = msvcrt.get_osfhandle(f.fileno())
l = ctypes.c_long()
done = time.time() + timeout
buffer = ""
while timeout == 0 or time.time() < done:
if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
err = self.GetLastError()
if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
return ('', False)
else:
self.log.error("readWithTimeout got error: %d", err)
# read a character at a time, checking for eol. Return once we get there.
index = 0
while index < l.value:
char = f.read(1)
buffer += char
if char == '\n':
return (buffer, False)
index = index + 1
time.sleep(0.01)
return (buffer, True)
def isPidAlive(self, pid):
STILL_ACTIVE = 259
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
if not pHandle:
return False
pExitCode = ctypes.wintypes.DWORD()
ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
ctypes.windll.kernel32.CloseHandle(pHandle)
return pExitCode.value == STILL_ACTIVE
else:
def readWithTimeout(self, f, timeout):
"""Try to read a line of output from the file object |f|. If no output
is received within |timeout| seconds, return a blank line.
Returns a tuple (line, did_timeout), where |did_timeout| is True
if the read timed out, and False otherwise."""
(r, w, e) = select.select([f], [], [], timeout)
if len(r) == 0:
return ('', True)
return (f.readline(), False)
def isPidAlive(self, pid):
try:
# kill(pid, 0) checks for a valid PID without actually sending a signal
# The method throws OSError if the PID is invalid, which we catch below.
os.kill(pid, 0)
# Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
# the process terminates before we get to this point.
wpid, wstatus = os.waitpid(pid, os.WNOHANG)
return wpid == 0
except OSError, err:
# Catch the errors we might expect from os.kill/os.waitpid,
# and re-raise any others
if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
return False
raise
def dumpScreen(self, utilityPath):
if self.haveDumpedScreen:
self.log.info("Not taking screenshot here: see the one that was previously logged")
return
self.haveDumpedScreen = True;
dump_screen(utilityPath, self.log)
def killAndGetStack(self, processPID, utilityPath, debuggerInfo):
"""Kill the process, preferrably in a way that gets us a stack trace.
Also attempts to obtain a screenshot before killing the process."""
if not debuggerInfo:
self.dumpScreen(utilityPath)
self.killAndGetStackNoScreenshot(processPID, utilityPath, debuggerInfo)
def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo):
"""Kill the process, preferrably in a way that gets us a stack trace."""
if self.CRASHREPORTER and not debuggerInfo:
if not self.IS_WIN32:
# ABRT will get picked up by Breakpad's signal handler
os.kill(processPID, signal.SIGABRT)
return
else:
# We should have a "crashinject" program in our utility path
crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
if os.path.exists(crashinject):
status = subprocess.Popen([crashinject, str(processPID)]).wait()
printstatus("crashinject", status)
if status == 0:
return
self.log.info("Can't trigger Breakpad, just killing process")
self.killPid(processPID)
def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
""" Look for timeout or crashes and return the status after the process terminates """
stackFixerFunction = None
didTimeout = False
hitMaxTime = False
if proc.stdout is None:
self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
else:
logsource = proc.stdout
if self.IS_DEBUG_BUILD and symbolsPath and os.path.exists(symbolsPath):
# Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
# This method is preferred for Tinderbox builds, since native symbols may have been stripped.
sys.path.insert(0, utilityPath)
import fix_stack_using_bpsyms as stackFixerModule
stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
del sys.path[0]
elif self.IS_DEBUG_BUILD and self.IS_MAC:
# Run each line through a function in fix_macosx_stack.py (uses atos)
sys.path.insert(0, utilityPath)
import fix_macosx_stack as stackFixerModule
stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
del sys.path[0]
elif self.IS_DEBUG_BUILD and self.IS_LINUX:
# Run each line through a function in fix_linux_stack.py (uses addr2line)
# This method is preferred for developer machines, so we don't have to run "make buildsymbols".
sys.path.insert(0, utilityPath)
import fix_linux_stack as stackFixerModule
stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
del sys.path[0]
# With metro browser runs this script launches the metro test harness which launches the browser.
# The metro test harness hands back the real browser process id via log output which we need to
# pick up on and parse out. This variable tracks the real browser process id if we find it.
browserProcessId = -1
(line, didTimeout) = self.readWithTimeout(logsource, timeout)
while line != "" and not didTimeout:
if stackFixerFunction:
line = stackFixerFunction(line)
self.log.info(line.rstrip().decode("UTF-8", "ignore"))
if "TEST-START" in line and "|" in line:
self.lastTestSeen = line.split("|")[1].strip()
if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
self.dumpScreen(utilityPath)
(line, didTimeout) = self.readWithTimeout(logsource, timeout)
if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
# Kill the application.
hitMaxTime = True
self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
self.killAndGetStack(proc.pid, utilityPath, debuggerInfo)
if didTimeout:
if line:
self.log.info(line.rstrip().decode("UTF-8", "ignore"))
self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
if browserProcessId == -1:
browserProcessId = proc.pid
self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo)
status = proc.wait()
printstatus("Main app process", status)
if status == 0:
self.lastTestSeen = "Main app process exited normally"
if status != 0 and not didTimeout and not hitMaxTime:
self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
return status
def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
""" build the application command line """
cmd = os.path.abspath(app)
if self.IS_MAC and os.path.exists(cmd + "-bin"):
# Prefer 'app-bin' in case 'app' is a shell script.
# We can remove this hack once bug 673899 etc are fixed.
cmd += "-bin"
args = []
if debuggerInfo:
args.extend(debuggerInfo.args)
args.append(cmd)
cmd = os.path.abspath(debuggerInfo.path)
if self.IS_MAC:
args.append("-foreground")
if self.IS_CYGWIN:
profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
else:
profileDirectory = profileDir + "/"
args.extend(("-no-remote", "-profile", profileDirectory))
if testURL is not None:
args.append((testURL))
args.extend(extraArgs)
return cmd, args
def checkForZombies(self, processLog, utilityPath, debuggerInfo):
""" Look for hung processes """
if not os.path.exists(processLog):
self.log.info('Automation Error: PID log not found: %s', processLog)
# Whilst no hung process was found, the run should still display as a failure
return True
foundZombie = False
self.log.info('INFO | zombiecheck | Reading PID log: %s', processLog)
processList = []
pidRE = re.compile(r'launched child process (\d+)$')
processLogFD = open(processLog)
for line in processLogFD:
self.log.info(line.rstrip())
m = pidRE.search(line)
if m:
processList.append(int(m.group(1)))
processLogFD.close()
for processPID in processList:
self.log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID)
if self.isPidAlive(processPID):
foundZombie = True
self.log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID)
self.killAndGetStack(processPID, utilityPath, debuggerInfo)
return foundZombie
def checkForCrashes(self, minidumpDir, symbolsPath):
return mozcrash.check_for_crashes(minidumpDir, symbolsPath, test_name=self.lastTestSeen)
def runApp(self, testURL, env, app, profileDir, extraArgs, utilityPath = None,
xrePath = None, certPath = None,
debuggerInfo = None, symbolsPath = None,
timeout = -1, maxTime = None, onLaunch = None,
detectShutdownLeaks = False, screenshotOnFail=False, testPath=None, bisectChunk=None):
"""
Run the app, log the duration it took to execute, return the status code.
Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
"""
if utilityPath == None:
utilityPath = self.DIST_BIN
if xrePath == None:
xrePath = self.DIST_BIN
if certPath == None:
certPath = self.CERTS_SRC_DIR
if timeout == -1:
timeout = self.DEFAULT_TIMEOUT
# copy env so we don't munge the caller's environment
env = dict(env);
env["NO_EM_RESTART"] = "1"
tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
os.close(tmpfd)
env["MOZ_PROCESS_LOG"] = processLog
cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
startTime = datetime.now()
if debuggerInfo and debuggerInfo.interactive:
# If an interactive debugger is attached, don't redirect output,
# don't use timeouts, and don't capture ctrl-c.
timeout = None
maxTime = None
outputPipe = None
signal.signal(signal.SIGINT, lambda sigid, frame: None)
else:
outputPipe = subprocess.PIPE
self.lastTestSeen = "automation.py"
proc = self.Process([cmd] + args,
env = self.environment(env, xrePath = xrePath,
crashreporter = not debuggerInfo),
stdout = outputPipe,
stderr = subprocess.STDOUT)
self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
if onLaunch is not None:
# Allow callers to specify an onLaunch callback to be fired after the
# app is launched.
onLaunch()
status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
# Do a final check for zombie child processes.
zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo)
crashed = self.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath)
if crashed or zombieProcesses:
status = 1
if os.path.exists(processLog):
os.unlink(processLog)
return status
def elf_arm(self, filename):
data = open(filename, 'rb').read(20)
return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM