diff --git a/build/automation.py.in b/build/automation.py.in index aa6f7de024b5..13d31d93c943 100644 --- a/build/automation.py.in +++ b/build/automation.py.in @@ -53,95 +53,25 @@ import tempfile from automationutils import checkForCrashes -""" -Runs the browser from a script, and provides useful utilities -for setting up the browser environment. -""" -SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) - -__all__ = [ - "UNIXISH", - "IS_WIN32", - "IS_MAC", - "log", - "runApp", - "Process", - "addExtraCommonOptions", - "initializeProfile", - "DIST_BIN", - "DEFAULT_APP", - "CERTS_SRC_DIR", - "environment", - "IS_TEST_BUILD", - "IS_DEBUG_BUILD", - "DEFAULT_TIMEOUT", - ] - -# timeout, in seconds -DEFAULT_TIMEOUT = 60.0 - -# These are generated in mozilla/build/Makefile.in -#expand DIST_BIN = __XPC_BIN_PATH__ -#expand IS_WIN32 = len("__WIN32__") != 0 -#expand IS_MAC = __IS_MAC__ != 0 -#expand IS_LINUX = __IS_LINUX__ != 0 +#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 +#expand _IS_CYGWIN = __IS_CYGWIN__ == 1 #else -IS_CYGWIN = False +_IS_CYGWIN = False #endif -#expand IS_CAMINO = __IS_CAMINO__ != 0 -#expand BIN_SUFFIX = __BIN_SUFFIX__ -#expand PERL = __PERL__ - -UNIXISH = not IS_WIN32 and not IS_MAC - -#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 - -########### -# LOGGING # -########### - -# 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() -handler = logging.StreamHandler(sys.stdout) -log.setLevel(logging.INFO) -log.addHandler(handler) - - -################# -# SUBPROCESSING # -################# - -class Process(subprocess.Popen): - """ - Represents our view of a subprocess. - It adds a kill() method which allows it to be stopped explicitly. - """ - - def kill(self): - if 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: - 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) +#expand _IS_CAMINO = __IS_CAMINO__ != 0 +#expand _BIN_SUFFIX = __BIN_SUFFIX__ +#expand _PERL = __PERL__ +#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 ################# # PROFILE SETUP # @@ -172,19 +102,100 @@ class Location: self.port = port self.options = options - -def readLocations(locationsPath = "server-locations.txt"): +class Automation(object): """ - Reads the locations at which the Mochitest HTTP server is available from - server-locations.txt. + Runs the browser from a script, and provides useful utilities + for setting up the browser environment. """ - locationFile = codecs.open(locationsPath, "r", "UTF-8") + DIST_BIN = _DIST_BIN + IS_WIN32 = _IS_WIN32 + IS_MAC = _IS_MAC + IS_LINUX = _IS_LINUX + IS_CYGWIN = _IS_CYGWIN + IS_CAMINO = _IS_CAMINO + BIN_SUFFIX = _BIN_SUFFIX + PERL = _PERL - # Perhaps more detail than necessary, but it's the easiest way to make sure - # we get exactly the format we want. See server-locations.txt for the exact - # format guaranteed here. - lineRe = re.compile(r"^(?P[a-z][-a-z0-9+.]*)" + 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 + + SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) + + # timeout, in seconds + DEFAULT_TIMEOUT = 60.0 + + log = logging.getLogger() + + def __init__(self): + + # 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. + handler = logging.StreamHandler(sys.stdout) + self.log.setLevel(logging.INFO) + self.log.addHandler(handler) + + @property + def __all__(self): + return [ + "UNIXISH", + "IS_WIN32", + "IS_MAC", + "log", + "runApp", + "Process", + "addCommonOptions", + "initializeProfile", + "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 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 readLocations(self, locationsPath = "server-locations.txt"): + """ + Reads the locations at which the Mochitest HTTP server is available from + server-locations.txt. + """ + + locationFile = codecs.open(locationsPath, "r", "UTF-8") + + # Perhaps more detail than necessary, but it's the easiest way to make sure + # we get exactly the format we want. See server-locations.txt for the exact + # format guaranteed here. + lineRe = re.compile(r"^(?P[a-z][-a-z0-9+.]*)" r"://" r"(?P" r"\d+\.\d+\.\d+\.\d+" @@ -198,47 +209,47 @@ def readLocations(locationsPath = "server-locations.txt"): r"\s+" r"(?P\S+(?:,\S+)*)" r")?$") - locations = [] - lineno = 0 - seenPrimary = False - for line in locationFile: - lineno += 1 - if line.startswith("#") or line == "\n": - continue + locations = [] + lineno = 0 + seenPrimary = False + for line in locationFile: + lineno += 1 + if line.startswith("#") or line == "\n": + continue - match = lineRe.match(line) - if not match: - raise SyntaxError(lineno) + match = lineRe.match(line) + if not match: + raise SyntaxError(lineno) - options = match.group("options") - if options: - options = options.split(",") - if "primary" in options: - if seenPrimary: - raise SyntaxError(lineno, "multiple primary locations") - seenPrimary = True - else: - options = [] + options = match.group("options") + if options: + options = options.split(",") + if "primary" in options: + if seenPrimary: + raise SyntaxError(lineno, "multiple primary locations") + seenPrimary = True + else: + options = [] - locations.append(Location(match.group("scheme"), match.group("host"), - match.group("port"), options)) + locations.append(Location(match.group("scheme"), match.group("host"), + match.group("port"), options)) - if not seenPrimary: - raise SyntaxError(lineno + 1, "missing primary location") + if not seenPrimary: + raise SyntaxError(lineno + 1, "missing primary location") - return locations + return locations -def initializeProfile(profileDir, extraPrefs = []): - "Sets up the standard testing profile." + def initializeProfile(self, profileDir, extraPrefs = []): + "Sets up the standard testing profile." - # Start with a clean slate. - shutil.rmtree(profileDir, True) - os.mkdir(profileDir) + # Start with a clean slate. + shutil.rmtree(profileDir, True) + os.mkdir(profileDir) - prefs = [] + prefs = [] - part = """\ + part = """\ user_pref("browser.dom.window.dump.enabled", true); user_pref("dom.allow_scripts_to_close_windows", true); user_pref("dom.disable_open_during_load", false); @@ -277,14 +288,14 @@ user_pref("browser.safebrowsing.provider.0.lookupURL", "http://localhost:8888/sa user_pref("browser.safebrowsing.provider.0.updateURL", "http://localhost:8888/safebrowsing-dummy/update"); """ - prefs.append(part) + prefs.append(part) - locations = readLocations() + locations = self.readLocations() - # Grant God-power to all the privileged servers on which tests run. - privileged = filter(lambda loc: "privileged" in loc.options, locations) - for (i, l) in itertools.izip(itertools.count(1), privileged): - part = """ + # Grant God-power to all the privileged servers on which tests run. + privileged = filter(lambda loc: "privileged" in loc.options, locations) + for (i, l) in itertools.izip(itertools.count(1), privileged): + part = """ user_pref("capability.principal.codebase.p%(i)d.granted", "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \ UniversalPreferencesRead UniversalPreferencesWrite \ @@ -293,14 +304,14 @@ user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s"); user_pref("capability.principal.codebase.p%(i)d.subjectName", ""); """ % { "i": i, "origin": (l.scheme + "://" + l.host + ":" + l.port) } - prefs.append(part) + prefs.append(part) - # We need to proxy every server but the primary one. - origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port) - for l in filter(lambda l: "primary" not in l.options, locations)] - origins = ", ".join(origins) + # We need to proxy every server but the primary one. + origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port) + for l in filter(lambda l: "primary" not in l.options, locations)] + origins = ", ".join(origins) - pacURL = """data:text/plain, + pacURL = """data:text/plain, function FindProxyForURL(url, host) { var origins = [%(origins)s]; @@ -329,382 +340,394 @@ function FindProxyForURL(url, host) return 'PROXY 127.0.0.1:4443'; return 'DIRECT'; }""" % { "origins": origins } - pacURL = "".join(pacURL.splitlines()) + pacURL = "".join(pacURL.splitlines()) - part = """ + part = """ user_pref("network.proxy.type", 2); user_pref("network.proxy.autoconfig_url", "%(pacURL)s"); user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others """ % {"pacURL": pacURL} - prefs.append(part) - - for v in extraPrefs: - thispref = v.split("=") - if len(thispref) < 2: - print "Error: syntax error in --setpref=" + v - sys.exit(1) - part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1]) prefs.append(part) - # write the preferences - prefsFile = open(profileDir + "/" + "user.js", "a") - prefsFile.write("".join(prefs)) - prefsFile.close() + for v in extraPrefs: + thispref = v.split("=") + if len(thispref) < 2: + print "Error: syntax error in --setpref=" + v + sys.exit(1) + part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1]) + prefs.append(part) -def addExtraCommonOptions(parser): - "Adds command-line options which are common to mochitest and reftest." + # write the preferences + prefsFile = open(profileDir + "/" + "user.js", "a") + prefsFile.write("".join(prefs)) + prefsFile.close() - parser.add_option("--setpref", - action = "append", type = "string", - default = [], - dest = "extraPrefs", metavar = "PREF=VALUE", - help = "defines an extra user preference") + def addCommonOptions(self, parser): + "Adds command-line options which are common to mochitest and reftest." -def fillCertificateDB(profileDir, certPath, utilityPath, xrePath): - pwfilePath = os.path.join(profileDir, ".crtdbpw") + parser.add_option("--setpref", + action = "append", type = "string", + default = [], + dest = "extraPrefs", metavar = "PREF=VALUE", + help = "defines an extra user preference") + + def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath): + pwfilePath = os.path.join(profileDir, ".crtdbpw") - pwfile = open(pwfilePath, "w") - pwfile.write("\n") - pwfile.close() + pwfile = open(pwfilePath, "w") + pwfile.write("\n") + pwfile.close() - # Create head of the ssltunnel configuration file - sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg") - sslTunnelConfig = open(sslTunnelConfigPath, "w") + # Create head of the ssltunnel configuration file + sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg") + sslTunnelConfig = open(sslTunnelConfigPath, "w") - sslTunnelConfig.write("httpproxy:1\n") - sslTunnelConfig.write("certdbdir:%s\n" % certPath) - sslTunnelConfig.write("forward:127.0.0.1:8888\n") - sslTunnelConfig.write("listen:*:4443:pgo server certificate\n") + sslTunnelConfig.write("httpproxy:1\n") + sslTunnelConfig.write("certdbdir:%s\n" % certPath) + sslTunnelConfig.write("forward:127.0.0.1:8888\n") + sslTunnelConfig.write("listen:*:4443:pgo server certificate\n") - # Configure automatic certificate and bind custom certificates, client authentication - locations = readLocations() - locations.pop(0) - for loc in locations: - if loc.scheme == "https" and "nocert" not in loc.options: - customCertRE = re.compile("^cert=(?P[0-9a-zA-Z_ ]+)") - clientAuthRE = re.compile("^clientauth=(?P[a-z]+)") - for option in loc.options: - match = customCertRE.match(option) - if match: - customcert = match.group("nickname"); - sslTunnelConfig.write("listen:%s:%s:4443:%s\n" % - (loc.host, loc.port, customcert)) + # Configure automatic certificate and bind custom certificates, client authentication + locations = self.readLocations() + locations.pop(0) + for loc in locations: + if loc.scheme == "https" and "nocert" not in loc.options: + customCertRE = re.compile("^cert=(?P[0-9a-zA-Z_ ]+)") + clientAuthRE = re.compile("^clientauth=(?P[a-z]+)") + for option in loc.options: + match = customCertRE.match(option) + if match: + customcert = match.group("nickname"); + sslTunnelConfig.write("listen:%s:%s:4443:%s\n" % + (loc.host, loc.port, customcert)) - match = clientAuthRE.match(option) - if match: - clientauth = match.group("clientauth"); - sslTunnelConfig.write("clientauth:%s:%s:4443:%s\n" % - (loc.host, loc.port, clientauth)) + match = clientAuthRE.match(option) + if match: + clientauth = match.group("clientauth"); + sslTunnelConfig.write("clientauth:%s:%s:4443:%s\n" % + (loc.host, loc.port, clientauth)) - sslTunnelConfig.close() + sslTunnelConfig.close() - # Pre-create the certification database for the profile - env = environment(xrePath = xrePath) - certutil = os.path.join(utilityPath, "certutil" + BIN_SUFFIX) - pk12util = os.path.join(utilityPath, "pk12util" + BIN_SUFFIX) + # Pre-create the certification database for the profile + env = self.environment(xrePath = xrePath) + certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX) + pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX) - status = Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait() - if status != 0: - return status + status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait() + if status != 0: + return status - # Walk the cert directory and add custom CAs and client certs - files = os.listdir(certPath) - for item in files: - root, ext = os.path.splitext(item) - if ext == ".ca": - trustBits = "CT,," - if root.endswith("-object"): - trustBits = "CT,,CT" - Process([certutil, "-A", "-i", os.path.join(certPath, item), - "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits], - env = env).wait() - if ext == ".client": - Process([pk12util, "-i", os.path.join(certPath, item), "-w", - pwfilePath, "-d", profileDir], - env = env).wait() + # Walk the cert directory and add custom CAs and client certs + files = os.listdir(certPath) + for item in files: + root, ext = os.path.splitext(item) + if ext == ".ca": + trustBits = "CT,," + if root.endswith("-object"): + trustBits = "CT,,CT" + self.Process([certutil, "-A", "-i", os.path.join(certPath, item), + "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits], + env = env).wait() + if ext == ".client": + self.Process([pk12util, "-i", os.path.join(certPath, item), "-w", + pwfilePath, "-d", profileDir], + env = env).wait() - os.unlink(pwfilePath) - return 0 + os.unlink(pwfilePath) + return 0 -def environment(env = None, xrePath = DIST_BIN, crashreporter = True): - if env == None: - env = dict(os.environ) + def environment(self, env = None, xrePath = None, crashreporter = True): + if xrePath == None: + xrePath = self.DIST_BIN + if env == None: + env = dict(os.environ) - ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath)) - if UNIXISH or IS_MAC: - envVar = "LD_LIBRARY_PATH" - if IS_MAC: - envVar = "DYLD_LIBRARY_PATH" - else: # unixish - env['MOZILLA_FIVE_HOME'] = xrePath - if envVar in env: - ldLibraryPath = ldLibraryPath + ":" + env[envVar] - env[envVar] = ldLibraryPath - elif IS_WIN32: - env["PATH"] = env["PATH"] + ";" + ldLibraryPath + ldLibraryPath = os.path.abspath(os.path.join(self.SCRIPT_DIR, xrePath)) + if self.UNIXISH or self.IS_MAC: + envVar = "LD_LIBRARY_PATH" + if self.IS_MAC: + envVar = "DYLD_LIBRARY_PATH" + else: # unixish + env['MOZILLA_FIVE_HOME'] = xrePath + if envVar in env: + ldLibraryPath = ldLibraryPath + ":" + env[envVar] + env[envVar] = ldLibraryPath + elif self.IS_WIN32: + env["PATH"] = env["PATH"] + ";" + ldLibraryPath - if crashreporter: - env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' - env['MOZ_CRASHREPORTER'] = '1' - else: - env['MOZ_CRASHREPORTER_DISABLE'] = '1' + if crashreporter: + env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' + env['MOZ_CRASHREPORTER'] = '1' + else: + env['MOZ_CRASHREPORTER_DISABLE'] = '1' - env['GNOME_DISABLE_CRASH_DIALOG'] = "1" - env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1' - return env + env['GNOME_DISABLE_CRASH_DIALOG'] = "1" + return env -if IS_WIN32: - import ctypes, time, msvcrt - PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe - GetLastError = ctypes.windll.kernel32.GetLastError + if IS_WIN32: + ctypes = __import__('ctypes') + time = __import__('time') + msvcrt = __import__('msvcrt') + PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe + GetLastError = ctypes.windll.kernel32.GetLastError - def readWithTimeout(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. 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.""" - if timeout is None: - # shortcut to allow callers to pass in "None" for no timeout. - return (f.readline(), False) - x = msvcrt.get_osfhandle(f.fileno()) - l = ctypes.c_long() - done = time.time() + timeout - while time.time() < done: - if PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0: - err = GetLastError() - if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE - return ('', False) - else: - log.error("readWithTimeout got error: %d", err) - if l > 0: - # we're assuming that the output is line-buffered, - # which is not unreasonable + 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. 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.""" + if timeout is None: + # shortcut to allow callers to pass in "None" for no timeout. return (f.readline(), False) - time.sleep(0.01) - return ('', True) - - def isPidAlive(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) - if (pExitCode.value == STILL_ACTIVE): - return True - else: - return False - - def killPid(pid): - PROCESS_TERMINATE = 0x0001 - pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid) - if not pHandle: - return - success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1) - ctypes.windll.kernel32.CloseHandle(pHandle) - -else: - import errno - - def readWithTimeout(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: + x = self.msvcrt.get_osfhandle(f.fileno()) + l = self.ctypes.c_long() + done = self.time.time() + timeout + while self.time.time() < done: + if self.PeekNamedPipe(x, None, 0, None, self.ctypes.byref(l), None) == 0: + err = self.GetLastError() + if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE + return ('', False) + else: + log.error("readWithTimeout got error: %d", err) + if l > 0: + # we're assuming that the output is line-buffered, + # which is not unreasonable + return (f.readline(), False) + self.time.sleep(0.01) return ('', True) - return (f.readline(), False) - def isPidAlive(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) - if wpid == 0: - return True - - return False - 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: + def isPidAlive(self, pid): + STILL_ACTIVE = 259 + PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + pHandle = self.ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) + if not pHandle: + return False + pExitCode = self.ctypes.wintypes.DWORD() + self.ctypes.windll.kernel32.GetExitCodeProcess(pHandle, self.ctypes.byref(pExitCode)) + self.ctypes.windll.kernel32.CloseHandle(pHandle) + if (pExitCode.value == STILL_ACTIVE): + return True + else: return False - raise - def killPid(pid): - os.kill(pid, signal.SIGKILL) - -def triggerBreakpad(proc, utilityPath): - """Attempt to kill this process in a way that triggers Breakpad crash - reporting, if we know how for this platform. Otherwise just .kill() it.""" - if CRASHREPORTER: - if UNIXISH: - # SEGV will get picked up by Breakpad's signal handler - os.kill(proc.pid, signal.SIGSEGV) - return - elif IS_WIN32: - # 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) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0: + def killPid(self, pid): + PROCESS_TERMINATE = 0x0001 + pHandle = self.ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid) + if not pHandle: return - #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296) - log.info("Can't trigger Breakpad, just killing process") - proc.kill() + success = self.ctypes.windll.kernel32.TerminateProcess(pHandle, 1) + self.ctypes.windll.kernel32.CloseHandle(pHandle) -############### -# RUN THE APP # -############### - -def runApp(testURL, env, app, profileDir, extraArgs, - runSSLTunnel = False, utilityPath = DIST_BIN, - xrePath = DIST_BIN, certPath = CERTS_SRC_DIR, - debuggerInfo = None, symbolsPath = None, - timeout = DEFAULT_TIMEOUT, maxTime = 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. - """ - - # 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 - - if IS_TEST_BUILD and runSSLTunnel: - # create certificate database for the profile - certificateStatus = fillCertificateDB(profileDir, certPath, utilityPath, xrePath) - if certificateStatus != 0: - log.info("TEST-UNEXPECTED FAIL | automation.py | Certificate integration failed") - return certificateStatus - - # start ssltunnel to provide https:// URLs capability - ssltunnel = os.path.join(utilityPath, "ssltunnel" + BIN_SUFFIX) - ssltunnelProcess = Process([ssltunnel, os.path.join(profileDir, "ssltunnel.cfg")], env = environment(xrePath = xrePath)) - log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid) - - # now run with the profile we created - cmd = app - if IS_MAC and not IS_CAMINO and not cmd.endswith("-bin"): - cmd += "-bin" - cmd = os.path.abspath(cmd) - - args = [] - - if debuggerInfo: - args.extend(debuggerInfo["args"]) - args.append(cmd) - cmd = os.path.abspath(debuggerInfo["path"]) - - if IS_MAC: - args.append("-foreground") - - if IS_CYGWIN: - profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"") else: - profileDirectory = profileDir + "/" + errno = __import__('errno') - args.extend(("-no-remote", "-profile", profileDirectory)) - if testURL is not None: - if IS_CAMINO: - args.extend(("-url", testURL)) + 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) + if wpid == 0: + return True + + return False + except OSError, err: + # Catch the errors we might expect from os.kill/os.waitpid, + # and re-raise any others + if err.errno == self.errno.ESRCH or err.errno == self.errno.ECHILD: + return False + raise + + def killPid(self, pid): + os.kill(pid, signal.SIGKILL) + + def triggerBreakpad(self, proc, utilityPath): + """Attempt to kill this process in a way that triggers Breakpad crash + reporting, if we know how for this platform. Otherwise just .kill() it.""" + if self.CRASHREPORTER: + if self.UNIXISH: + # SEGV will get picked up by Breakpad's signal handler + os.kill(proc.pid, signal.SIGSEGV) + return + elif self.IS_WIN32: + # 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) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0: + return + #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296) + self.log.info("Can't trigger Breakpad, just killing process") + proc.kill() + + def runApp(self, testURL, env, app, profileDir, extraArgs, + runSSLTunnel = False, utilityPath = None, + xrePath = None, certPath = None, + debuggerInfo = None, symbolsPath = None, + timeout = None, maxTime = 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 == None: + 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 + + if self.IS_TEST_BUILD and runSSLTunnel: + # create certificate database for the profile + certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath) + if certificateStatus != 0: + self.log.info("TEST-UNEXPECTED FAIL | automation.py | Certificate integration failed") + return certificateStatus + + # start ssltunnel to provide https:// URLs capability + ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX) + ssltunnelProcess = self.Process([ssltunnel, + os.path.join(profileDir, "ssltunnel.cfg")], + env = self.environment(xrePath = xrePath)) + self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid) + + # now run with the profile we created + cmd = app + if self.IS_MAC and not self.IS_CAMINO and not cmd.endswith("-bin"): + cmd += "-bin" + cmd = os.path.abspath(cmd) + + 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: - args.append((testURL)) - args.extend(extraArgs) + profileDirectory = profileDir + "/" - startTime = datetime.now() + args.extend(("-no-remote", "-profile", profileDirectory)) + if testURL is not None: + if self.IS_CAMINO: + args.extend(("-url", testURL)) + else: + args.append((testURL)) + args.extend(extraArgs) - # Don't redirect stdout and stderr if an interactive debugger is attached - if debuggerInfo and debuggerInfo["interactive"]: - outputPipe = None - else: - outputPipe = subprocess.PIPE + startTime = datetime.now() - proc = Process([cmd] + args, - env = environment(env, xrePath = xrePath, + # Don't redirect stdout and stderr if an interactive debugger is attached + if debuggerInfo and debuggerInfo["interactive"]: + outputPipe = None + else: + outputPipe = subprocess.PIPE + + proc = self.Process([cmd] + args, + env = self.environment(env, xrePath = xrePath, crashreporter = not debuggerInfo), stdout = outputPipe, stderr = subprocess.STDOUT) - log.info("INFO | automation.py | Application pid: %d", proc.pid) + self.log.info("INFO | automation.py | Application pid: %d", proc.pid) - stackFixerProcess = None - didTimeout = False - if outputPipe is None: - log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection") - else: - logsource = proc.stdout - if IS_DEBUG_BUILD: - stackFixerCommand = None - if IS_MAC: - stackFixerCommand = "fix-macosx-stack.pl" - elif IS_LINUX: - stackFixerCommand = "fix-linux-stack.pl" - if stackFixerCommand is not None: - stackFixerProcess = Process([PERL, os.path.join(utilityPath, stackFixerCommand)], stdin=logsource, stdout=subprocess.PIPE) - logsource = stackFixerProcess.stdout + stackFixerProcess = None + didTimeout = False + if outputPipe 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: + stackFixerCommand = None + if self.IS_MAC: + stackFixerCommand = "fix-macosx-stack.pl" + elif self.IS_LINUX: + stackFixerCommand = "fix-linux-stack.pl" + if stackFixerCommand is not None: + stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, stackFixerCommand)], + stdin=logsource, + stdout=subprocess.PIPE) + logsource = stackFixerProcess.stdout - (line, didTimeout) = readWithTimeout(logsource, timeout) - hitMaxTime = False - while line != "" and not didTimeout: - log.info(line.rstrip()) - (line, didTimeout) = readWithTimeout(logsource, timeout) - if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime): - # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait(). - hitMaxTime = True - log.info("TEST-UNEXPECTED-FAIL | automation.py | application ran for longer than allowed maximum time of %d seconds", int(maxTime)) - triggerBreakpad(proc, utilityPath) - if didTimeout: - log.info("TEST-UNEXPECTED-FAIL | automation.py | application timed out after %d seconds with no output", int(timeout)) - triggerBreakpad(proc, utilityPath) + (line, didTimeout) = self.readWithTimeout(logsource, timeout) + hitMaxTime = False + while line != "" and not didTimeout: + self.log.info(line.rstrip()) + (line, didTimeout) = self.readWithTimeout(logsource, timeout) + if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime): + # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait(). + hitMaxTime = True + self.log.info("TEST-UNEXPECTED-FAIL | automation.py | application ran for longer than allowed maximum time of %d seconds", int(maxTime)) + self.triggerBreakpad(proc, utilityPath) + if didTimeout: + self.log.info("TEST-UNEXPECTED-FAIL | automation.py | application timed out after %d seconds with no output", int(timeout)) + self.triggerBreakpad(proc, utilityPath) - status = proc.wait() - if status != 0 and not didTimeout and not hitMaxTime: - log.info("TEST-UNEXPECTED-FAIL | automation.py | Exited with code %d during test run", status) - if stackFixerProcess is not None: - fixerStatus = stackFixerProcess.wait() - if fixerStatus != 0 and not didTimeout and not hitMaxTime: - log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus) - log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime)) + status = proc.wait() + if status != 0 and not didTimeout and not hitMaxTime: + self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Exited with code %d during test run", status) + if stackFixerProcess is not None: + fixerStatus = stackFixerProcess.wait() + if fixerStatus != 0 and not didTimeout and not hitMaxTime: + self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus) + self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime)) - # Do a final check for zombie child processes. - if not os.path.exists(processLog): - log.info('INFO | automation.py | PID log not found: %s', processLog) - else: - log.info('INFO | automation.py | Reading PID log: %s', processLog) - processList = [] - pidRE = re.compile(r'launched child process (\d+)$') - processLogFD = open(processLog) - for line in processLogFD: - log.info(line.rstrip()) - m = pidRE.search(line) - if m: - processList.append(int(m.group(1))) - processLogFD.close() + # Do a final check for zombie child processes. + if not os.path.exists(processLog): + self.log.info('INFO | automation.py | PID log not found: %s', processLog) + else: + self.log.info('INFO | automation.py | 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: - log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID) - if isPidAlive(processPID): - log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID) - killPid(processPID) + for processPID in processList: + self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID) + if self.isPidAlive(processPID): + self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID) + self.killPid(processPID) - if checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath): - status = -1 + if checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath): + status = -1 - if os.path.exists(processLog): - os.unlink(processLog) + if os.path.exists(processLog): + os.unlink(processLog) - if IS_TEST_BUILD and runSSLTunnel: - ssltunnelProcess.kill() + if self.IS_TEST_BUILD and runSSLTunnel: + ssltunnelProcess.kill() - return status + return status diff --git a/build/leaktest.py.in b/build/leaktest.py.in index fa490a974d37..5408caec99d6 100644 --- a/build/leaktest.py.in +++ b/build/leaktest.py.in @@ -46,18 +46,19 @@ import os import sys import logging from getopt import getopt -import automation +from automation import Automation PORT = 8888 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) PROFILE_DIRECTORY = os.path.abspath(os.path.join(SCRIPT_DIR, "./leakprofile")) -DIST_BIN = os.path.join(SCRIPT_DIR, automation.DIST_BIN) os.chdir(SCRIPT_DIR) class EasyServer(SocketServer.TCPServer): allow_reuse_address = True if __name__ == '__main__': + automation = Automation() + DIST_BIN = os.path.join(SCRIPT_DIR, automation.DIST_BIN) opts, extraArgs = getopt(sys.argv[1:], 'l:') if len(opts) > 0: try: diff --git a/build/pgo/genpgocert.py.in b/build/pgo/genpgocert.py.in index d96206bc83e2..872b6439ea95 100644 --- a/build/pgo/genpgocert.py.in +++ b/build/pgo/genpgocert.py.in @@ -36,7 +36,7 @@ # # ***** END LICENSE BLOCK ***** -import automation +from automation import Automation import os import re import shutil @@ -47,6 +47,8 @@ import sys #expand PROFILE_DIR = __PROFILE_DIR__ #expand CERTS_SRC_DIR = __CERTS_SRC_DIR__ +automation = Automation() + dbFiles = [ re.compile("^cert[0-9]+\.db$"), re.compile("^key[0-9]+\.db$"), diff --git a/build/pgo/profileserver.py.in b/build/pgo/profileserver.py.in index 7239825c6913..ca667bf47e08 100644 --- a/build/pgo/profileserver.py.in +++ b/build/pgo/profileserver.py.in @@ -46,7 +46,7 @@ import os import sys import shutil from datetime import datetime -import automation +from automation import Automation PORT = 8888 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) @@ -57,6 +57,7 @@ class EasyServer(SocketServer.TCPServer): allow_reuse_address = True if __name__ == '__main__': + automation = Automation() httpd = EasyServer(("", PORT), SimpleHTTPServer.SimpleHTTPRequestHandler) t = threading.Thread(target=httpd.serve_forever) t.setDaemon(True) # don't hang on exit diff --git a/layout/tools/reftest/runreftest.py b/layout/tools/reftest/runreftest.py index 2e90540ddabd..53d647ca04eb 100644 --- a/layout/tools/reftest/runreftest.py +++ b/layout/tools/reftest/runreftest.py @@ -44,54 +44,128 @@ Runs the reftest test harness. import sys, shutil, os, os.path SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) sys.path.append(SCRIPT_DIRECTORY) -import automation +from automation import Automation from automationutils import * from optparse import OptionParser from tempfile import mkdtemp -oldcwd = os.getcwd() -os.chdir(SCRIPT_DIRECTORY) +class RefTest(object): -def getFullPath(path): - "Get an absolute path relative to oldcwd." - return os.path.normpath(os.path.join(oldcwd, os.path.expanduser(path))) + oldcwd = os.getcwd() -def createReftestProfile(options, profileDir): - "Sets up a profile for reftest." + def __init__(self, automation): + self.automation = automation + os.chdir(SCRIPT_DIRECTORY) - # Set preferences. - prefsFile = open(os.path.join(profileDir, "user.js"), "w") - prefsFile.write("""user_pref("browser.dom.window.dump.enabled", true); -""") - prefsFile.write('user_pref("reftest.timeout", %d);\n' % (options.timeout * 1000)) - prefsFile.write('user_pref("ui.caretBlinkTime", -1);\n') + def getFullPath(self, path): + "Get an absolute path relative to self.oldcwd." + return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path))) - for v in options.extraPrefs: - thispref = v.split("=") - if len(thispref) < 2: - print "Error: syntax error in --setpref=" + v - sys.exit(1) - part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1]) - prefsFile.write(part) - # no slow script dialogs - prefsFile.write('user_pref("dom.max_script_run_time", 0);') - prefsFile.write('user_pref("dom.max_chrome_script_run_time", 0);') - prefsFile.close() + def createReftestProfile(self, options, profileDir): + "Sets up a profile for reftest." + + # Set preferences. + prefsFile = open(os.path.join(profileDir, "user.js"), "w") + prefsFile.write("""user_pref("browser.dom.window.dump.enabled", true); + """) + prefsFile.write('user_pref("reftest.timeout", %d);\n' % (options.timeout * 1000)) + prefsFile.write('user_pref("ui.caretBlinkTime", -1);\n') + + for v in options.extraPrefs: + thispref = v.split("=") + if len(thispref) < 2: + print "Error: syntax error in --setpref=" + v + sys.exit(1) + part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1]) + prefsFile.write(part) + # no slow script dialogs + prefsFile.write('user_pref("dom.max_script_run_time", 0);') + prefsFile.write('user_pref("dom.max_chrome_script_run_time", 0);') + prefsFile.close() + + # install the reftest extension bits into the profile + profileExtensionsPath = os.path.join(profileDir, "extensions") + os.mkdir(profileExtensionsPath) + reftestExtensionPath = os.path.join(SCRIPT_DIRECTORY, "reftest") + extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w") + extFile.write(reftestExtensionPath) + extFile.close() + + def runTests(self, manifest, options): + debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs, + options.debuggerInteractive); + + profileDir = None + try: + profileDir = mkdtemp() + self.createReftestProfile(options, profileDir) + self.copyExtraFilesToProfile(options, profileDir) + + # browser environment + browserEnv = self.automation.environment(xrePath = options.xrePath) + browserEnv["XPCOM_DEBUG_BREAK"] = "stack" + + # Enable leaks detection to its own log file. + leakLogFile = os.path.join(profileDir, "runreftest_leaks.log") + browserEnv["XPCOM_MEM_BLOAT_LOG"] = leakLogFile + + # run once with -silent to let the extension manager do its thing + # and then exit the app + self.automation.log.info("REFTEST INFO | runreftest.py | Performing extension manager registration: start.\n") + # Don't care about this |status|: |runApp()| reporting it should be enough. + status = self.automation.runApp(None, browserEnv, options.app, profileDir, + ["-silent"], + utilityPath = options.utilityPath, + xrePath=options.xrePath, + symbolsPath=options.symbolsPath) + # We don't care to call |processLeakLog()| for this step. + self.automation.log.info("\nREFTEST INFO | runreftest.py | Performing extension manager registration: end.") + + # Remove the leak detection file so it can't "leak" to the tests run. + # The file is not there if leak logging was not enabled in the application build. + if os.path.exists(leakLogFile): + os.remove(leakLogFile) + + # then again to actually run reftest + self.automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n") + reftestlist = self.getFullPath(manifest) + status = self.automation.runApp(None, browserEnv, options.app, profileDir, + ["-reftest", reftestlist], + utilityPath = options.utilityPath, + xrePath=options.xrePath, + debuggerInfo=debuggerInfo, + symbolsPath=options.symbolsPath, + # give the JS harness 30 seconds to deal + # with its own timeouts + timeout=options.timeout + 30.0) + processLeakLog(leakLogFile, options.leakThreshold) + self.automation.log.info("\nREFTEST INFO | runreftest.py | Running tests: end.") + finally: + if profileDir: + shutil.rmtree(profileDir) + return status + + def copyExtraFilesToProfile(self, options, profileDir): + "Copy extra files or dirs specified on the command line to the testing profile." + for f in options.extraProfileFiles: + abspath = self.getFullPath(f) + dest = os.path.join(profileDir, os.path.basename(abspath)) + if os.path.isdir(abspath): + shutil.copytree(abspath, dest) + else: + shutil.copy(abspath, dest) - # install the reftest extension bits into the profile - profileExtensionsPath = os.path.join(profileDir, "extensions") - os.mkdir(profileExtensionsPath) - reftestExtensionPath = os.path.join(SCRIPT_DIRECTORY, "reftest") - extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w") - extFile.write(reftestExtensionPath) - extFile.close() def main(): + automation = Automation() parser = OptionParser() + reftest = RefTest(automation) # we want to pass down everything from automation.__all__ - addCommonOptions(parser, defaults=dict(zip(automation.__all__, [getattr(automation, x) for x in automation.__all__]))) - automation.addExtraCommonOptions(parser) + addCommonOptions(parser, + defaults=dict(zip(automation.__all__, + [getattr(automation, x) for x in automation.__all__]))) + automation.addCommonOptions(parser) parser.add_option("--appname", action = "store", type = "string", dest = "app", default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP), @@ -118,90 +192,28 @@ def main(): "programs (xpcshell, ssltunnel, certutil)") options, args = parser.parse_args() - if len(args) != 1: print >>sys.stderr, "No reftest.list specified." sys.exit(1) - options.app = getFullPath(options.app) + options.app = reftest.getFullPath(options.app) if not os.path.exists(options.app): print """Error: Path %(app)s doesn't exist. Are you executing $objdir/_tests/reftest/runreftest.py?""" \ - % {"app": options.app} + % {"app": options.app} sys.exit(1) if options.xrePath is None: options.xrePath = os.path.dirname(options.app) else: # allow relative paths - options.xrePath = getFullPath(options.xrePath) + options.xrePath = reftest.getFullPath(options.xrePath) if options.symbolsPath: - options.symbolsPath = getFullPath(options.symbolsPath) - options.utilityPath = getFullPath(options.utilityPath) - - debuggerInfo = getDebuggerInfo(oldcwd, options.debugger, options.debuggerArgs, - options.debuggerInteractive); - - profileDir = None - try: - profileDir = mkdtemp() - createReftestProfile(options, profileDir) - copyExtraFilesToProfile(options, profileDir) - - # browser environment - browserEnv = automation.environment(xrePath = options.xrePath) - browserEnv["XPCOM_DEBUG_BREAK"] = "stack" - - # Enable leaks detection to its own log file. - leakLogFile = os.path.join(profileDir, "runreftest_leaks.log") - browserEnv["XPCOM_MEM_BLOAT_LOG"] = leakLogFile - - # run once with -silent to let the extension manager do its thing - # and then exit the app - automation.log.info("REFTEST INFO | runreftest.py | Performing extension manager registration: start.\n") - # Don't care about this |status|: |runApp()| reporting it should be enough. - status = automation.runApp(None, browserEnv, options.app, profileDir, - ["-silent"], - utilityPath = options.utilityPath, - xrePath=options.xrePath, - symbolsPath=options.symbolsPath) - # We don't care to call |processLeakLog()| for this step. - automation.log.info("\nREFTEST INFO | runreftest.py | Performing extension manager registration: end.") - - # Remove the leak detection file so it can't "leak" to the tests run. - # The file is not there if leak logging was not enabled in the application build. - if os.path.exists(leakLogFile): - os.remove(leakLogFile) - - # then again to actually run reftest - automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n") - reftestlist = getFullPath(args[0]) - status = automation.runApp(None, browserEnv, options.app, profileDir, - ["-reftest", reftestlist], - utilityPath = options.utilityPath, - xrePath=options.xrePath, - debuggerInfo=debuggerInfo, - symbolsPath=options.symbolsPath, - # give the JS harness 30 seconds to deal - # with its own timeouts - timeout=options.timeout + 30.0) - processLeakLog(leakLogFile, options.leakThreshold) - automation.log.info("\nREFTEST INFO | runreftest.py | Running tests: end.") - finally: - if profileDir: - shutil.rmtree(profileDir) - sys.exit(status) - -def copyExtraFilesToProfile(options, profileDir): - "Copy extra files or dirs specified on the command line to the testing profile." - for f in options.extraProfileFiles: - abspath = getFullPath(f) - dest = os.path.join(profileDir, os.path.basename(abspath)) - if os.path.isdir(abspath): - shutil.copytree(abspath, dest) - else: - shutil.copy(abspath, dest) + options.symbolsPath = reftest.getFullPath(options.symbolsPath) + options.utilityPath = reftest.getFullPath(options.utilityPath) + sys.exit(reftest.runTests(args[0], options)) + if __name__ == "__main__": main() diff --git a/testing/mochitest/runtests.py.in b/testing/mochitest/runtests.py.in index 87f3a9fc58e9..dc740922410d 100644 --- a/testing/mochitest/runtests.py.in +++ b/testing/mochitest/runtests.py.in @@ -52,40 +52,9 @@ import shutil from urllib import quote_plus as encodeURIComponent import urllib2 import commands -import automation +from automation import Automation from automationutils import * -# Path to the test script on the server -TEST_SERVER_HOST = "localhost:8888" -TEST_PATH = "/tests/" -CHROME_PATH = "/redirect.html"; -A11Y_PATH = "/redirect-a11y.html" -TESTS_URL = "http://" + TEST_SERVER_HOST + TEST_PATH -CHROMETESTS_URL = "http://" + TEST_SERVER_HOST + CHROME_PATH -A11YTESTS_URL = "http://" + TEST_SERVER_HOST + A11Y_PATH -SERVER_SHUTDOWN_URL = "http://" + TEST_SERVER_HOST + "/server/shutdown" -# main browser chrome URL, same as browser.chromeURL pref -#ifdef MOZ_SUITE -BROWSER_CHROME_URL = "chrome://navigator/content/navigator.xul" -#else -BROWSER_CHROME_URL = "chrome://browser/content/browser.xul" -#endif - -# Max time in seconds to wait for server startup before tests will fail -- if -# this seems big, it's mostly for debug machines where cold startup -# (particularly after a build) takes forever. -if automation.IS_DEBUG_BUILD: - SERVER_STARTUP_TIMEOUT = 180 -else: - SERVER_STARTUP_TIMEOUT = 90 - -oldcwd = os.getcwd() -SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) -os.chdir(SCRIPT_DIRECTORY) - -PROFILE_DIRECTORY = os.path.abspath("./mochitesttestingprofile") - -LEAK_REPORT_FILE = os.path.join(PROFILE_DIRECTORY, "runtests_leaks.log") ####################### # COMMANDLINE OPTIONS # @@ -93,13 +62,15 @@ LEAK_REPORT_FILE = os.path.join(PROFILE_DIRECTORY, "runtests_leaks.log") class MochitestOptions(optparse.OptionParser): """Parses Mochitest commandline options.""" - def __init__(self, **kwargs): + def __init__(self, automation, scriptdir, **kwargs): + self._automation = automation optparse.OptionParser.__init__(self, **kwargs) defaults = {} - # we want to pass down everything from automation.__all__ - addCommonOptions(self, defaults=dict(zip(automation.__all__, [getattr(automation, x) for x in automation.__all__]))) - automation.addExtraCommonOptions(self) + # we want to pass down everything from self._automation.__all__ + addCommonOptions(self, defaults=dict(zip(self._automation.__all__, + [getattr(self._automation, x) for x in self._automation.__all__]))) + self._automation.addCommonOptions(self) self.add_option("--close-when-done", action = "store_true", dest = "closeWhenDone", @@ -109,17 +80,17 @@ class MochitestOptions(optparse.OptionParser): self.add_option("--appname", action = "store", type = "string", dest = "app", help = "absolute path to application, overriding default") - defaults["app"] = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP) + defaults["app"] = os.path.join(scriptdir, self._automation.DEFAULT_APP) self.add_option("--utility-path", action = "store", type = "string", dest = "utilityPath", help = "absolute path to directory containing utility programs (xpcshell, ssltunnel, certutil)") - defaults["utilityPath"] = automation.DIST_BIN + defaults["utilityPath"] = self._automation.DIST_BIN self.add_option("--certificate-path", action = "store", type = "string", dest = "certPath", help = "absolute path to directory containing certificate store to use testing profile") - defaults["certPath"] = automation.CERTS_SRC_DIR + defaults["certPath"] = self._automation.CERTS_SRC_DIR self.add_option("--log-file", action = "store", type = "string", @@ -249,17 +220,19 @@ See for details on the logg class MochitestServer: "Web server used to serve Mochitests, for closer fidelity to the real web." - def __init__(self, options): + def __init__(self, automation, options, profileDir): + self._automation = automation self._closeWhenDone = options.closeWhenDone self._utilityPath = options.utilityPath self._xrePath = options.xrePath + self._profileDir = profileDir def start(self): "Run the Mochitest server, returning the process ID of the server." - env = automation.environment(xrePath = self._xrePath) + env = self._automation.environment(xrePath = self._xrePath) env["XPCOM_DEBUG_BREAK"] = "warn" - if automation.IS_WIN32: + if self._automation.IS_WIN32: env["PATH"] = env["PATH"] + ";" + self._xrePath args = ["-g", self._xrePath, @@ -268,19 +241,18 @@ class MochitestServer: "-f", "./" + "server.js"] xpcshell = os.path.join(self._utilityPath, - "xpcshell" + automation.BIN_SUFFIX) - self._process = automation.Process([xpcshell] + args, env = env) + "xpcshell" + self._automation.BIN_SUFFIX) + self._process = self._automation.Process([xpcshell] + args, env = env) pid = self._process.pid if pid < 0: print "Error starting server." sys.exit(2) - automation.log.info("INFO | runtests.py | Server pid: %d", pid) - + self._automation.log.info("INFO | runtests.py | Server pid: %d", pid) def ensureReady(self, timeout): assert timeout >= 0 - aliveFile = os.path.join(PROFILE_DIRECTORY, "server_alive.txt") + aliveFile = os.path.join(self._profileDir, "server_alive.txt") i = 0 while i < timeout: if os.path.exists(aliveFile): @@ -301,16 +273,261 @@ class MochitestServer: except: self._process.kill() -def getFullPath(path): - "Get an absolute path relative to oldcwd." - return os.path.normpath(os.path.join(oldcwd, os.path.expanduser(path))) -################# -# MAIN FUNCTION # -################# +class Mochitest(object): + # Path to the test script on the server + TEST_SERVER_HOST = "localhost:8888" + TEST_PATH = "/tests/" + CHROME_PATH = "/redirect.html"; + A11Y_PATH = "/redirect-a11y.html" + TESTS_URL = "http://" + TEST_SERVER_HOST + TEST_PATH + CHROMETESTS_URL = "http://" + TEST_SERVER_HOST + CHROME_PATH + A11YTESTS_URL = "http://" + TEST_SERVER_HOST + A11Y_PATH + SERVER_SHUTDOWN_URL = "http://" + TEST_SERVER_HOST + "/server/shutdown" + + oldcwd = os.getcwd() + + def __init__(self, automation): + self.automation = automation + + # Max time in seconds to wait for server startup before tests will fail -- if + # this seems big, it's mostly for debug machines where cold startup + # (particularly after a build) takes forever. + if self.automation.IS_DEBUG_BUILD: + self.SERVER_STARTUP_TIMEOUT = 180 + else: + self.SERVER_STARTUP_TIMEOUT = 90 + + self.SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) + os.chdir(self.SCRIPT_DIRECTORY) + + self.PROFILE_DIRECTORY = os.path.abspath("./mochitesttestingprofile") + + self.LEAK_REPORT_FILE = os.path.join(self.PROFILE_DIRECTORY, "runtests_leaks.log") + + def getFullPath(self, path): + "Get an absolute path relative to self.oldcwd." + return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path))) + + def runTests(self, options): + debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs, + options.debuggerInteractive); + + # browser environment + browserEnv = self.automation.environment(xrePath = options.xrePath) + + # These variables are necessary for correct application startup; change + # via the commandline at your own risk. + browserEnv["XPCOM_DEBUG_BREAK"] = "stack" + + for v in options.environment: + ix = v.find("=") + if ix <= 0: + print "Error: syntax error in --setenv=" + v + return 1 + browserEnv[v[:ix]] = v[ix + 1:] + + self.automation.initializeProfile(self.PROFILE_DIRECTORY, options.extraPrefs) + manifest = self.addChromeToProfile(options) + self.copyExtraFilesToProfile(options) + server = MochitestServer(self.automation, options, self.PROFILE_DIRECTORY) + server.start() + + # If we're lucky, the server has fully started by now, and all paths are + # ready, etc. However, xpcshell cold start times suck, at least for debug + # builds. We'll try to connect to the server for awhile, and if we fail, + # we'll try to kill the server and exit with an error. + server.ensureReady(self.SERVER_STARTUP_TIMEOUT) + + # URL parameters to test URL: + # + # autorun -- kick off tests automatically + # closeWhenDone -- runs quit.js after tests + # logFile -- logs test run to an absolute path + # totalChunks -- how many chunks to split tests into + # thisChunk -- which chunk to run + # timeout -- per-test timeout in seconds + # + + # consoleLevel, fileLevel: set the logging level of the console and + # file logs, if activated. + # + + testURL = self.TESTS_URL + options.testPath + urlOpts = [] + if options.chrome: + testURL = self.CHROMETESTS_URL + if options.testPath: + urlOpts.append("testPath=" + encodeURIComponent(options.testPath)) + elif options.a11y: + testURL = self.A11YTESTS_URL + if options.testPath: + urlOpts.append("testPath=" + encodeURIComponent(options.testPath)) + elif options.browserChrome: + testURL = "about:blank" + + # allow relative paths for logFile + if options.logFile: + options.logFile = self.getFullPath(options.logFile) + if options.browserChrome: + self.makeTestConfig(options) + else: + if options.autorun: + urlOpts.append("autorun=1") + if options.timeout: + urlOpts.append("timeout=%d" % options.timeout) + if options.closeWhenDone: + urlOpts.append("closeWhenDone=1") + if options.logFile: + urlOpts.append("logFile=" + encodeURIComponent(options.logFile)) + urlOpts.append("fileLevel=" + encodeURIComponent(options.fileLevel)) + if options.consoleLevel: + urlOpts.append("consoleLevel=" + encodeURIComponent(options.consoleLevel)) + if options.totalChunks: + urlOpts.append("totalChunks=%d" % options.totalChunks) + urlOpts.append("thisChunk=%d" % options.thisChunk) + if options.chunkByDir: + urlOpts.append("chunkByDir=%d" % options.chunkByDir) + if options.shuffle: + urlOpts.append("shuffle=1") + if len(urlOpts) > 0: + testURL += "?" + "&".join(urlOpts) + + browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.LEAK_REPORT_FILE + + if options.fatalAssertions: + browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort" + + # run once with -silent to let the extension manager do its thing + # and then exit the app + self.automation.log.info("INFO | runtests.py | Performing extension manager registration: start.\n") + # Don't care about this |status|: |runApp()| reporting it should be enough. + status = self.automation.runApp(None, browserEnv, options.app, + self.PROFILE_DIRECTORY, ["-silent"], + utilityPath = options.utilityPath, + xrePath = options.xrePath, + symbolsPath=options.symbolsPath) + # We don't care to call |processLeakLog()| for this step. + self.automation.log.info("\nINFO | runtests.py | Performing extension manager registration: end.") + + # Remove the leak detection file so it can't "leak" to the tests run. + # The file is not there if leak logging was not enabled in the application build. + if os.path.exists(self.LEAK_REPORT_FILE): + os.remove(self.LEAK_REPORT_FILE) + + # then again to actually run mochitest + if options.timeout: + timeout = options.timeout + 30 + elif options.autorun: + timeout = None + else: + timeout = 330.0 # default JS harness timeout is 300 seconds + self.automation.log.info("INFO | runtests.py | Running tests: start.\n") + status = self.automation.runApp(testURL, browserEnv, options.app, + self.PROFILE_DIRECTORY, options.browserArgs, + runSSLTunnel = True, + utilityPath = options.utilityPath, + xrePath = options.xrePath, + certPath=options.certPath, + debuggerInfo=debuggerInfo, + symbolsPath=options.symbolsPath, + timeout = timeout) + + # Server's no longer needed, and perhaps more importantly, anything it might + # spew to console shouldn't disrupt the leak information table we print next. + server.stop() + + processLeakLog(self.LEAK_REPORT_FILE, options.leakThreshold) + self.automation.log.info("\nINFO | runtests.py | Running tests: end.") + + # delete the profile and manifest + os.remove(manifest) + + # hanging due to non-halting threads is no fun; assume we hit the errors we + # were going to hit already and exit. + return status + + def makeTestConfig(self, options): + "Creates a test configuration file for customizing test execution." + def boolString(b): + if b: + return "true" + return "false" + + logFile = options.logFile.replace("\\", "\\\\") + testPath = options.testPath.replace("\\", "\\\\") + content = """\ +({ + autoRun: %(autorun)s, + closeWhenDone: %(closeWhenDone)s, + logPath: "%(logPath)s", + testPath: "%(testPath)s" +})""" % {"autorun": boolString(options.autorun), + "closeWhenDone": boolString(options.closeWhenDone), + "logPath": logFile, + "testPath": testPath} + + config = open(os.path.join(self.PROFILE_DIRECTORY, "testConfig.js"), "w") + config.write(content) + config.close() + + + def addChromeToProfile(self, options): + "Adds MochiKit chrome tests to the profile." + + chromedir = os.path.join(self.PROFILE_DIRECTORY, "chrome") + os.mkdir(chromedir) + + chrome = """ +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ +toolbar, +toolbarpalette { + background-color: rgb(235, 235, 235) !important; +} +toolbar#nav-bar { + background-image: none !important; +} +""" + + # write userChrome.css + chromeFile = open(os.path.join(self.PROFILE_DIRECTORY, "userChrome.css"), "a") + chromeFile.write(chrome) + chromeFile.close() + + + # register our chrome dir + chrometestDir = os.path.abspath(".") + "/" + if self.automation.IS_WIN32: + chrometestDir = "file:///" + chrometestDir.replace("\\", "/") + + + (path, leaf) = os.path.split(options.app) + manifest = os.path.join(path, "chrome", "mochikit.manifest") + manifestFile = open(manifest, "w") + manifestFile.write("content mochikit " + chrometestDir + " contentaccessible=yes\n") + + if options.browserChrome: + manifestFile.write("""overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul +overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul +""") + manifestFile.close() + + return manifest + + def copyExtraFilesToProfile(self, options): + "Copy extra files or dirs specified on the command line to the testing profile." + for f in options.extraProfileFiles: + abspath = self.getFullPath(f) + dest = os.path.join(self.PROFILE_DIRECTORY, os.path.basename(abspath)) + if os.path.isdir(abspath): + shutil.copytree(abspath, dest) + else: + shutil.copy(abspath, dest) def main(): - parser = MochitestOptions() + automation = Automation() + mochitest = Mochitest(automation) + parser = MochitestOptions(automation, mochitest.SCRIPT_DIRECTORY) options, args = parser.parse_args() if options.totalChunks is not None and options.thisChunk is None: @@ -318,7 +535,7 @@ def main(): if options.totalChunks: if not 1 <= options.thisChunk <= options.totalChunks: - parser.error("thisChunk must be between 1 and totalChunks") + parser.error("thisChunk must be between 1 and totalChunks") if options.xrePath is None: # default xrePath to the app path if not provided @@ -330,248 +547,22 @@ def main(): options.xrePath = automation.DIST_BIN # allow relative paths - options.xrePath = getFullPath(options.xrePath) + options.xrePath = mochitest.getFullPath(options.xrePath) - options.app = getFullPath(options.app) + options.app = mochitest.getFullPath(options.app) if not os.path.exists(options.app): msg = """\ -Error: Path %(app)s doesn't exist. -Are you executing $objdir/_tests/testing/mochitest/runtests.py?""" + Error: Path %(app)s doesn't exist. + Are you executing $objdir/_tests/testing/mochitest/runtests.py?""" print msg % {"app": options.app} sys.exit(1) - options.utilityPath = getFullPath(options.utilityPath) - options.certPath = getFullPath(options.certPath) + options.utilityPath = mochitest.getFullPath(options.utilityPath) + options.certPath = mochitest.getFullPath(options.certPath) if options.symbolsPath: - options.symbolsPath = getFullPath(options.symbolsPath) + options.symbolsPath = mochitest.getFullPath(options.symbolsPath) - debuggerInfo = getDebuggerInfo(oldcwd, options.debugger, options.debuggerArgs, - options.debuggerInteractive); - - # browser environment - browserEnv = automation.environment(xrePath = options.xrePath) - - # These variables are necessary for correct application startup; change - # via the commandline at your own risk. - browserEnv["XPCOM_DEBUG_BREAK"] = "stack" - - for v in options.environment: - ix = v.find("=") - if ix <= 0: - print "Error: syntax error in --setenv=" + v - sys.exit(1) - browserEnv[v[:ix]] = v[ix + 1:] - - automation.initializeProfile(PROFILE_DIRECTORY, options.extraPrefs) - manifest = addChromeToProfile(options) - copyExtraFilesToProfile(options) - server = MochitestServer(options) - server.start() - - # If we're lucky, the server has fully started by now, and all paths are - # ready, etc. However, xpcshell cold start times suck, at least for debug - # builds. We'll try to connect to the server for awhile, and if we fail, - # we'll try to kill the server and exit with an error. - server.ensureReady(SERVER_STARTUP_TIMEOUT) - - # URL parameters to test URL: - # - # autorun -- kick off tests automatically - # closeWhenDone -- runs quit.js after tests - # logFile -- logs test run to an absolute path - # totalChunks -- how many chunks to split tests into - # thisChunk -- which chunk to run - # timeout -- per-test timeout in seconds - # - - # consoleLevel, fileLevel: set the logging level of the console and - # file logs, if activated. - # - - testURL = TESTS_URL + options.testPath - urlOpts = [] - if options.chrome: - testURL = CHROMETESTS_URL - if options.testPath: - urlOpts.append("testPath=" + encodeURIComponent(options.testPath)) - elif options.a11y: - testURL = A11YTESTS_URL - if options.testPath: - urlOpts.append("testPath=" + encodeURIComponent(options.testPath)) - elif options.browserChrome: - testURL = "about:blank" - - # allow relative paths for logFile - if options.logFile: - options.logFile = getFullPath(options.logFile) - if options.browserChrome: - makeTestConfig(options) - else: - if options.autorun: - urlOpts.append("autorun=1") - if options.timeout: - urlOpts.append("timeout=%d" % options.timeout) - if options.closeWhenDone: - urlOpts.append("closeWhenDone=1") - if options.logFile: - urlOpts.append("logFile=" + encodeURIComponent(options.logFile)) - urlOpts.append("fileLevel=" + encodeURIComponent(options.fileLevel)) - if options.consoleLevel: - urlOpts.append("consoleLevel=" + encodeURIComponent(options.consoleLevel)) - if options.totalChunks: - urlOpts.append("totalChunks=%d" % options.totalChunks) - urlOpts.append("thisChunk=%d" % options.thisChunk) - if options.chunkByDir: - urlOpts.append("chunkByDir=%d" % options.chunkByDir) - if options.shuffle: - urlOpts.append("shuffle=1") - if len(urlOpts) > 0: - testURL += "?" + "&".join(urlOpts) - - browserEnv["XPCOM_MEM_BLOAT_LOG"] = LEAK_REPORT_FILE - - if options.fatalAssertions: - browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort" - - # run once with -silent to let the extension manager do its thing - # and then exit the app - automation.log.info("INFO | runtests.py | Performing extension manager registration: start.\n") - # Don't care about this |status|: |runApp()| reporting it should be enough. - status = automation.runApp(None, browserEnv, options.app, - PROFILE_DIRECTORY, ["-silent"], - utilityPath = options.utilityPath, - xrePath = options.xrePath, - symbolsPath=options.symbolsPath) - # We don't care to call |processLeakLog()| for this step. - automation.log.info("\nINFO | runtests.py | Performing extension manager registration: end.") - - # Remove the leak detection file so it can't "leak" to the tests run. - # The file is not there if leak logging was not enabled in the application build. - if os.path.exists(LEAK_REPORT_FILE): - os.remove(LEAK_REPORT_FILE) - - # then again to actually run mochitest - if options.timeout: - timeout = options.timeout + 30 - elif options.autorun: - timeout = None - else: - timeout = 330.0 # default JS harness timeout is 300 seconds - automation.log.info("INFO | runtests.py | Running tests: start.\n") - status = automation.runApp(testURL, browserEnv, options.app, - PROFILE_DIRECTORY, options.browserArgs, - runSSLTunnel = True, - utilityPath = options.utilityPath, - xrePath = options.xrePath, - certPath=options.certPath, - debuggerInfo=debuggerInfo, - symbolsPath=options.symbolsPath, - timeout = timeout) - - # Server's no longer needed, and perhaps more importantly, anything it might - # spew to console shouldn't disrupt the leak information table we print next. - server.stop() - - processLeakLog(LEAK_REPORT_FILE, options.leakThreshold) - automation.log.info("\nINFO | runtests.py | Running tests: end.") - - # delete the profile and manifest - os.remove(manifest) - - # hanging due to non-halting threads is no fun; assume we hit the errors we - # were going to hit already and exit. - sys.exit(status) - - - -####################### -# CONFIGURATION SETUP # -####################### - -def makeTestConfig(options): - "Creates a test configuration file for customizing test execution." - def boolString(b): - if b: - return "true" - return "false" - - logFile = options.logFile.replace("\\", "\\\\") - testPath = options.testPath.replace("\\", "\\\\") - content = """\ -({ - autoRun: %(autorun)s, - closeWhenDone: %(closeWhenDone)s, - logPath: "%(logPath)s", - testPath: "%(testPath)s" -})""" % {"autorun": boolString(options.autorun), - "closeWhenDone": boolString(options.closeWhenDone), - "logPath": logFile, - "testPath": testPath} - - config = open(os.path.join(PROFILE_DIRECTORY, "testConfig.js"), "w") - config.write(content) - config.close() - - -def addChromeToProfile(options): - "Adds MochiKit chrome tests to the profile." - - chromedir = os.path.join(PROFILE_DIRECTORY, "chrome") - os.mkdir(chromedir) - - chrome = [] - - part = """ -@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ -toolbar, -toolbarpalette { - background-color: rgb(235, 235, 235) !important; -} -toolbar#nav-bar { - background-image: none !important; -} -""" - chrome.append(part) - - - - # write userChrome.css - chromeFile = open(os.path.join(PROFILE_DIRECTORY, "userChrome.css"), "a") - chromeFile.write("".join(chrome)) - chromeFile.close() - - - # register our chrome dir - chrometestDir = os.path.abspath(".") + "/" - if automation.IS_WIN32: - chrometestDir = "file:///" + chrometestDir.replace("\\", "/") - - - (path, leaf) = os.path.split(options.app) - manifest = os.path.join(path, "chrome", "mochikit.manifest") - manifestFile = open(manifest, "w") - manifestFile.write("content mochikit " + chrometestDir + " contentaccessible=yes\n") - if options.browserChrome: - overlayLine = "overlay " + BROWSER_CHROME_URL + " " \ - "chrome://mochikit/content/browser-test-overlay.xul\n" - manifestFile.write(overlayLine) - manifestFile.close() - - return manifest - -def copyExtraFilesToProfile(options): - "Copy extra files or dirs specified on the command line to the testing profile." - for f in options.extraProfileFiles: - abspath = getFullPath(f) - dest = os.path.join(PROFILE_DIRECTORY, os.path.basename(abspath)) - if os.path.isdir(abspath): - shutil.copytree(abspath, dest) - else: - shutil.copy(abspath, dest) - -######### -# DO IT # -######### + sys.exit(mochitest.runTests(options)) if __name__ == "__main__": main() diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py index 7eb3294c8a31..d7cd474e056a 100644 --- a/testing/xpcshell/runxpcshelltests.py +++ b/testing/xpcshell/runxpcshelltests.py @@ -22,6 +22,7 @@ # Contributor(s): # Serge Gautherie # Ted Mielczarek +# Joel Maher # # Alternatively, the contents of this file may be used under the terms of # either the GNU General Public License Version 2 or later (the "GPL"), or @@ -45,243 +46,246 @@ from tempfile import mkdtemp from automationutils import * -# Init logging -log = logging.getLogger() -handler = logging.StreamHandler(sys.stdout) -log.setLevel(logging.INFO) -log.addHandler(handler) +class XPCShellTests(object): -oldcwd = os.getcwd() + log = logging.getLogger() + oldcwd = os.getcwd() -def readManifest(manifest): - """Given a manifest file containing a list of test directories, - return a list of absolute paths to the directories contained within.""" - manifestdir = os.path.dirname(manifest) - testdirs = [] - try: - f = open(manifest, "r") - for line in f: - dir = line.rstrip() - path = os.path.join(manifestdir, dir) - if os.path.isdir(path): - testdirs.append(path) - f.close() - except: - pass # just eat exceptions - return testdirs + def __init__(self): + # Init logging + handler = logging.StreamHandler(sys.stdout) + self.log.setLevel(logging.INFO) + self.log.addHandler(handler) -def runTests(xpcshell, xrePath=None, symbolsPath=None, - manifest=None, testdirs=[], testPath=None, - interactive=False, logfiles=True, - debuggerInfo=None): - """Run xpcshell tests. + def readManifest(self, manifest): + """Given a manifest file containing a list of test directories, + return a list of absolute paths to the directories contained within.""" + manifestdir = os.path.dirname(manifest) + testdirs = [] + try: + f = open(manifest, "r") + for line in f: + dir = line.rstrip() + path = os.path.join(manifestdir, dir) + if os.path.isdir(path): + testdirs.append(path) + f.close() + except: + pass # just eat exceptions + return testdirs - |xpcshell|, is the xpcshell executable to use to run the tests. - |xrePath|, if provided, is the path to the XRE to use. - |symbolsPath|, if provided is the path to a directory containing - breakpad symbols for processing crashes in tests. - |manifest|, if provided, is a file containing a list of - test directories to run. - |testdirs|, if provided, is a list of absolute paths of test directories. - No-manifest only option. - |testPath|, if provided, indicates a single path and/or test to run. - |interactive|, if set to True, indicates to provide an xpcshell prompt - instead of automatically executing the test. - |logfiles|, if set to False, indicates not to save output to log files. - Non-interactive only option. - |debuggerInfo|, if set, specifies the debugger and debugger arguments - that will be used to launch xpcshell. - """ + def runTests(self, xpcshell, xrePath=None, symbolsPath=None, + manifest=None, testdirs=[], testPath=None, + interactive=False, logfiles=True, + debuggerInfo=None): + """Run xpcshell tests. - if not testdirs and not manifest: - # nothing to test! - print >>sys.stderr, "Error: No test dirs or test manifest specified!" - return False + |xpcshell|, is the xpcshell executable to use to run the tests. + |xrePath|, if provided, is the path to the XRE to use. + |symbolsPath|, if provided is the path to a directory containing + breakpad symbols for processing crashes in tests. + |manifest|, if provided, is a file containing a list of + test directories to run. + |testdirs|, if provided, is a list of absolute paths of test directories. + No-manifest only option. + |testPath|, if provided, indicates a single path and/or test to run. + |interactive|, if set to True, indicates to provide an xpcshell prompt + instead of automatically executing the test. + |logfiles|, if set to False, indicates not to save output to log files. + Non-interactive only option. + |debuggerInfo|, if set, specifies the debugger and debugger arguments + that will be used to launch xpcshell. + """ - passCount = 0 - failCount = 0 + if not testdirs and not manifest: + # nothing to test! + print >>sys.stderr, "Error: No test dirs or test manifest specified!" + return False - testharnessdir = os.path.dirname(os.path.abspath(__file__)) - xpcshell = os.path.abspath(xpcshell) - # we assume that httpd.js lives in components/ relative to xpcshell - httpdJSPath = os.path.join(os.path.dirname(xpcshell), "components", "httpd.js").replace("\\", "/"); + passCount = 0 + failCount = 0 - env = dict(os.environ) - # Make assertions fatal - env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" - # Don't launch the crash reporter client - env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" + testharnessdir = os.path.dirname(os.path.abspath(__file__)) + xpcshell = os.path.abspath(xpcshell) + # we assume that httpd.js lives in components/ relative to xpcshell + httpdJSPath = os.path.join(os.path.dirname(xpcshell), "components", "httpd.js").replace("\\", "/"); - if xrePath is None: - xrePath = os.path.dirname(xpcshell) - else: - xrePath = os.path.abspath(xrePath) - if sys.platform == 'win32': - env["PATH"] = env["PATH"] + ";" + xrePath - elif sys.platform in ('os2emx', 'os2knix'): - os.environ["BEGINLIBPATH"] = xrePath + ";" + env["BEGINLIBPATH"] - os.environ["LIBPATHSTRICT"] = "T" - elif sys.platform == 'osx': - env["DYLD_LIBRARY_PATH"] = xrePath - else: # unix or linux? - env["LD_LIBRARY_PATH"] = xrePath + env = dict(os.environ) + # Make assertions fatal + env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" + # Don't launch the crash reporter client + env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" - # xpcsRunArgs: function to call to run the test. - # pStdout, pStderr: Parameter values for later |Popen()| call. - if interactive: - xpcsRunArgs = [ + if xrePath is None: + xrePath = os.path.dirname(xpcshell) + else: + xrePath = os.path.abspath(xrePath) + if sys.platform == 'win32': + env["PATH"] = env["PATH"] + ";" + xrePath + elif sys.platform in ('os2emx', 'os2knix'): + os.environ["BEGINLIBPATH"] = xrePath + ";" + env["BEGINLIBPATH"] + os.environ["LIBPATHSTRICT"] = "T" + elif sys.platform == 'osx': + env["DYLD_LIBRARY_PATH"] = xrePath + else: # unix or linux? + env["LD_LIBRARY_PATH"] = xrePath + + # xpcsRunArgs: function to call to run the test. + # pStdout, pStderr: Parameter values for later |Popen()| call. + if interactive: + xpcsRunArgs = [ '-e', 'print("To start the test, type |_execute_test();|.");', '-i'] - pStdout = None - pStderr = None - else: - xpcsRunArgs = ['-e', '_execute_test();'] - if (debuggerInfo and debuggerInfo["interactive"]): pStdout = None pStderr = None else: - if sys.platform == 'os2emx': + xpcsRunArgs = ['-e', '_execute_test();'] + if (debuggerInfo and debuggerInfo["interactive"]): pStdout = None + pStderr = None else: - pStdout = PIPE - pStderr = STDOUT + if sys.platform == 'os2emx': + pStdout = None + else: + pStdout = PIPE + pStderr = STDOUT - # has to be loaded by xpchell: it can't load itself. - xpcsCmd = [xpcshell, '-g', xrePath, '-j', '-s'] + \ - ['-e', 'const _HTTPD_JS_PATH = "%s";' % httpdJSPath, - '-f', os.path.join(testharnessdir, 'head.js')] + # has to be loaded by xpchell: it can't load itself. + xpcsCmd = [xpcshell, '-g', xrePath, '-j', '-s'] + \ + ['-e', 'const _HTTPD_JS_PATH = "%s";' % httpdJSPath, + '-f', os.path.join(testharnessdir, 'head.js')] - if debuggerInfo: - xpcsCmd = [debuggerInfo["path"]] + debuggerInfo["args"] + xpcsCmd + if debuggerInfo: + xpcsCmd = [debuggerInfo["path"]] + debuggerInfo["args"] + xpcsCmd - # |testPath| will be the optional path only, or |None|. - # |singleFile| will be the optional test only, or |None|. - singleFile = None - if testPath: - if testPath.endswith('.js'): - # Split into path and file. - if testPath.find('/') == -1: - # Test only. - singleFile = testPath - testPath = None + # |testPath| will be the optional path only, or |None|. + # |singleFile| will be the optional test only, or |None|. + singleFile = None + if testPath: + if testPath.endswith('.js'): + # Split into path and file. + if testPath.find('/') == -1: + # Test only. + singleFile = testPath + testPath = None + else: + # Both path and test. + # Reuse |testPath| temporarily. + testPath = testPath.rsplit('/', 1) + singleFile = testPath[1] + testPath = testPath[0] else: - # Both path and test. - # Reuse |testPath| temporarily. - testPath = testPath.rsplit('/', 1) - singleFile = testPath[1] - testPath = testPath[0] - else: - # Path only. - # Simply remove optional ending separator. - testPath = testPath.rstrip("/") + # Path only. + # Simply remove optional ending separator. + testPath = testPath.rstrip("/") - # Override testdirs. - if manifest is not None: - testdirs = readManifest(os.path.abspath(manifest)) + # Override testdirs. + if manifest is not None: + testdirs = self.readManifest(os.path.abspath(manifest)) - # Process each test directory individually. - for testdir in testdirs: - if testPath and not testdir.endswith(testPath): - continue - - testdir = os.path.abspath(testdir) - - # get the list of head and tail files from the directory - testHeadFiles = [] - for f in sorted(glob(os.path.join(testdir, "head_*.js"))): - if os.path.isfile(f): - testHeadFiles += [f] - testTailFiles = [] - # Tails are executed in the reverse order, to "match" heads order, - # as in "h1-h2-h3 then t3-t2-t1". - for f in reversed(sorted(glob(os.path.join(testdir, "tail_*.js")))): - if os.path.isfile(f): - testTailFiles += [f] - - # if a single test file was specified, we only want to execute that test - testfiles = sorted(glob(os.path.join(testdir, "test_*.js"))) - if singleFile: - if singleFile in [os.path.basename(x) for x in testfiles]: - testfiles = [os.path.join(testdir, singleFile)] - else: # not in this dir? skip it + # Process each test directory individually. + for testdir in testdirs: + if testPath and not testdir.endswith(testPath): continue - cmdH = ", ".join(['"' + f.replace('\\', '/') + '"' - for f in testHeadFiles]) - cmdT = ", ".join(['"' + f.replace('\\', '/') + '"' - for f in testTailFiles]) - cmdH = xpcsCmd + \ - ['-e', 'const _HEAD_FILES = [%s];' % cmdH] + \ - ['-e', 'const _TAIL_FILES = [%s];' % cmdT] + testdir = os.path.abspath(testdir) - # Now execute each test individually. - for test in testfiles: - # The test file will have to be loaded after the head files. - cmdT = ['-e', 'const _TEST_FILE = ["%s"];' % - os.path.join(testdir, test).replace('\\', '/')] + # get the list of head and tail files from the directory + testHeadFiles = [] + for f in sorted(glob(os.path.join(testdir, "head_*.js"))): + if os.path.isfile(f): + testHeadFiles += [f] + testTailFiles = [] + # Tails are executed in the reverse order, to "match" heads order, + # as in "h1-h2-h3 then t3-t2-t1". + for f in reversed(sorted(glob(os.path.join(testdir, "tail_*.js")))): + if os.path.isfile(f): + testTailFiles += [f] - # create a temp dir that the JS harness can stick a profile in - profileDir = None - try: - profileDir = mkdtemp() - env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir + # if a single test file was specified, we only want to execute that test + testfiles = sorted(glob(os.path.join(testdir, "test_*.js"))) + if singleFile: + if singleFile in [os.path.basename(x) for x in testfiles]: + testfiles = [os.path.join(testdir, singleFile)] + else: # not in this dir? skip it + continue - # Enable leaks (only) detection to its own log file. - leakLogFile = os.path.join(profileDir, "runxpcshelltests_leaks.log") - env["XPCOM_MEM_LEAK_LOG"] = leakLogFile + cmdH = ", ".join(['"' + f.replace('\\', '/') + '"' + for f in testHeadFiles]) + cmdT = ", ".join(['"' + f.replace('\\', '/') + '"' + for f in testTailFiles]) + cmdH = xpcsCmd + \ + ['-e', 'const _HEAD_FILES = [%s];' % cmdH] + \ + ['-e', 'const _TAIL_FILES = [%s];' % cmdT] - proc = Popen(cmdH + cmdT + xpcsRunArgs, - stdout=pStdout, stderr=pStderr, env=env, cwd=testdir) + # Now execute each test individually. + for test in testfiles: + # The test file will have to be loaded after the head files. + cmdT = ['-e', 'const _TEST_FILE = ["%s"];' % + os.path.join(testdir, test).replace('\\', '/')] - # allow user to kill hung subprocess with SIGINT w/o killing this script - # - don't move this line above Popen, or child will inherit the SIG_IGN - signal.signal(signal.SIGINT, signal.SIG_IGN) - # |stderr == None| as |pStderr| was either |None| or redirected to |stdout|. - stdout, stderr = proc.communicate() - signal.signal(signal.SIGINT, signal.SIG_DFL) + # create a temp dir that the JS harness can stick a profile in + profileDir = None + try: + profileDir = mkdtemp() + env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir - if interactive: - # Not sure what else to do here... - return True + # Enable leaks (only) detection to its own log file. + leakLogFile = os.path.join(profileDir, "runxpcshelltests_leaks.log") + env["XPCOM_MEM_LEAK_LOG"] = leakLogFile - if proc.returncode != 0 or (stdout and re.search("^TEST-UNEXPECTED-FAIL", stdout, re.MULTILINE)): - print """TEST-UNEXPECTED-FAIL | %s | test failed (with xpcshell return code: %d), see following log: + proc = Popen(cmdH + cmdT + xpcsRunArgs, + stdout=pStdout, stderr=pStderr, env=env, cwd=testdir) + + # allow user to kill hung subprocess with SIGINT w/o killing this script + # - don't move this line above Popen, or child will inherit the SIG_IGN + signal.signal(signal.SIGINT, signal.SIG_IGN) + # |stderr == None| as |pStderr| was either |None| or redirected to |stdout|. + stdout, stderr = proc.communicate() + signal.signal(signal.SIGINT, signal.SIG_DFL) + + if interactive: + # Not sure what else to do here... + return True + + if proc.returncode != 0 or (stdout and re.search("^TEST-UNEXPECTED-FAIL", stdout, re.MULTILINE)): + print """TEST-UNEXPECTED-FAIL | %s | test failed (with xpcshell return code: %d), see following log: >>>>>>> %s <<<<<<<""" % (test, proc.returncode, stdout) - checkForCrashes(testdir, symbolsPath, testName=test) - failCount += 1 - else: - print "TEST-PASS | %s | test passed" % test - passCount += 1 + checkForCrashes(testdir, symbolsPath, testName=test) + failCount += 1 + else: + print "TEST-PASS | %s | test passed" % test + passCount += 1 - dumpLeakLog(leakLogFile, True) + dumpLeakLog(leakLogFile, True) - if logfiles and stdout: - try: - f = open(test + ".log", "w") - f.write(stdout) + if logfiles and stdout: + try: + f = open(test + ".log", "w") + f.write(stdout) - if os.path.exists(leakLogFile): - leaks = open(leakLogFile, "r") - f.write(leaks.read()) - leaks.close() - finally: - if f: - f.close() - finally: - if profileDir: - shutil.rmtree(profileDir) + if os.path.exists(leakLogFile): + leaks = open(leakLogFile, "r") + f.write(leaks.read()) + leaks.close() + finally: + if f: + f.close() + finally: + if profileDir: + shutil.rmtree(profileDir) - if passCount == 0 and failCount == 0: - print "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | No tests run. Did you pass an invalid --test-path?" - failCount = 1 + if passCount == 0 and failCount == 0: + print "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | No tests run. Did you pass an invalid --test-path?" + failCount = 1 - print """INFO | Result summary: + print """INFO | Result summary: INFO | Passed: %d INFO | Failed: %d""" % (passCount, failCount) - return failCount == 0 + return failCount == 0 def main(): """Process command line arguments and call runTests() to do the real work.""" @@ -307,27 +311,29 @@ def main(): if len(args) < 2 and options.manifest is None or \ (len(args) < 1 and options.manifest is not None): - print >>sys.stderr, """Usage: %s - or: %s --manifest=test.manifest """ % (sys.argv[0], + print >>sys.stderr, """Usage: %s + or: %s --manifest=test.manifest """ % (sys.argv[0], sys.argv[0]) - sys.exit(1) + sys.exit(1) - debuggerInfo = getDebuggerInfo(oldcwd, options.debugger, options.debuggerArgs, + xpcsh = XPCShellTests() + debuggerInfo = getDebuggerInfo(xpcsh.oldcwd, options.debugger, options.debuggerArgs, options.debuggerInteractive); if options.interactive and not options.testPath: print >>sys.stderr, "Error: You must specify a test filename in interactive mode!" sys.exit(1) - if not runTests(args[0], - xrePath=options.xrePath, - symbolsPath=options.symbolsPath, - manifest=options.manifest, - testdirs=args[1:], - testPath=options.testPath, - interactive=options.interactive, - logfiles=options.logfiles, - debuggerInfo=debuggerInfo): + + if not xpcsh.runTests(args[0], + xrePath=options.xrePath, + symbolsPath=options.symbolsPath, + manifest=options.manifest, + testdirs=args[1:], + testPath=options.testPath, + interactive=options.interactive, + logfiles=options.logfiles, + debuggerInfo=debuggerInfo): sys.exit(1) if __name__ == '__main__':