1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/build/automationutils.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,664 @@ 1.4 +# 1.5 +# This Source Code Form is subject to the terms of the Mozilla Public 1.6 +# License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.8 + 1.9 +from __future__ import with_statement 1.10 +import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile 1.11 +import base64 1.12 +import re 1.13 +import os 1.14 +from urlparse import urlparse 1.15 +from operator import itemgetter 1.16 +import signal 1.17 + 1.18 +try: 1.19 + import mozinfo 1.20 +except ImportError: 1.21 + # Stub out fake mozinfo since this is not importable on Android 4.0 Opt. 1.22 + # This should be fixed; see 1.23 + # https://bugzilla.mozilla.org/show_bug.cgi?id=650881 1.24 + mozinfo = type('mozinfo', (), dict(info={}))() 1.25 + mozinfo.isWin = mozinfo.isLinux = mozinfo.isUnix = mozinfo.isMac = False 1.26 + 1.27 + # TODO! FILE: localautomation :/ 1.28 + # mapping from would-be mozinfo attr <-> sys.platform 1.29 + mapping = {'isMac': ['mac', 'darwin'], 1.30 + 'isLinux': ['linux', 'linux2'], 1.31 + 'isWin': ['win32', 'win64'], 1.32 + } 1.33 + mapping = dict(sum([[(value, key) for value in values] for key, values in mapping.items()], [])) 1.34 + attr = mapping.get(sys.platform) 1.35 + if attr: 1.36 + setattr(mozinfo, attr, True) 1.37 + if mozinfo.isLinux: 1.38 + mozinfo.isUnix = True 1.39 + 1.40 +__all__ = [ 1.41 + "ZipFileReader", 1.42 + "addCommonOptions", 1.43 + "dumpLeakLog", 1.44 + "isURL", 1.45 + "processLeakLog", 1.46 + "getDebuggerInfo", 1.47 + "DEBUGGER_INFO", 1.48 + "replaceBackSlashes", 1.49 + 'KeyValueParseError', 1.50 + 'parseKeyValue', 1.51 + 'systemMemory', 1.52 + 'environment', 1.53 + 'dumpScreen', 1.54 + "ShutdownLeaks" 1.55 + ] 1.56 + 1.57 +# Map of debugging programs to information about them, like default arguments 1.58 +# and whether or not they are interactive. 1.59 +DEBUGGER_INFO = { 1.60 + # gdb requires that you supply the '--args' flag in order to pass arguments 1.61 + # after the executable name to the executable. 1.62 + "gdb": { 1.63 + "interactive": True, 1.64 + "args": "-q --args" 1.65 + }, 1.66 + 1.67 + "cgdb": { 1.68 + "interactive": True, 1.69 + "args": "-q --args" 1.70 + }, 1.71 + 1.72 + "lldb": { 1.73 + "interactive": True, 1.74 + "args": "--", 1.75 + "requiresEscapedArgs": True 1.76 + }, 1.77 + 1.78 + # valgrind doesn't explain much about leaks unless you set the 1.79 + # '--leak-check=full' flag. But there are a lot of objects that are 1.80 + # semi-deliberately leaked, so we set '--show-possibly-lost=no' to avoid 1.81 + # uninteresting output from those objects. We set '--smc-check==all-non-file' 1.82 + # and '--vex-iropt-register-updates=allregs-at-mem-access' so that valgrind 1.83 + # deals properly with JIT'd JavaScript code. 1.84 + "valgrind": { 1.85 + "interactive": False, 1.86 + "args": " ".join(["--leak-check=full", 1.87 + "--show-possibly-lost=no", 1.88 + "--smc-check=all-non-file", 1.89 + "--vex-iropt-register-updates=allregs-at-mem-access"]) 1.90 + } 1.91 +} 1.92 + 1.93 +class ZipFileReader(object): 1.94 + """ 1.95 + Class to read zip files in Python 2.5 and later. Limited to only what we 1.96 + actually use. 1.97 + """ 1.98 + 1.99 + def __init__(self, filename): 1.100 + self._zipfile = zipfile.ZipFile(filename, "r") 1.101 + 1.102 + def __del__(self): 1.103 + self._zipfile.close() 1.104 + 1.105 + def _getnormalizedpath(self, path): 1.106 + """ 1.107 + Gets a normalized path from 'path' (or the current working directory if 1.108 + 'path' is None). Also asserts that the path exists. 1.109 + """ 1.110 + if path is None: 1.111 + path = os.curdir 1.112 + path = os.path.normpath(os.path.expanduser(path)) 1.113 + assert os.path.isdir(path) 1.114 + return path 1.115 + 1.116 + def _extractname(self, name, path): 1.117 + """ 1.118 + Extracts a file with the given name from the zip file to the given path. 1.119 + Also creates any directories needed along the way. 1.120 + """ 1.121 + filename = os.path.normpath(os.path.join(path, name)) 1.122 + if name.endswith("/"): 1.123 + os.makedirs(filename) 1.124 + else: 1.125 + path = os.path.split(filename)[0] 1.126 + if not os.path.isdir(path): 1.127 + os.makedirs(path) 1.128 + with open(filename, "wb") as dest: 1.129 + dest.write(self._zipfile.read(name)) 1.130 + 1.131 + def namelist(self): 1.132 + return self._zipfile.namelist() 1.133 + 1.134 + def read(self, name): 1.135 + return self._zipfile.read(name) 1.136 + 1.137 + def extract(self, name, path = None): 1.138 + if hasattr(self._zipfile, "extract"): 1.139 + return self._zipfile.extract(name, path) 1.140 + 1.141 + # This will throw if name is not part of the zip file. 1.142 + self._zipfile.getinfo(name) 1.143 + 1.144 + self._extractname(name, self._getnormalizedpath(path)) 1.145 + 1.146 + def extractall(self, path = None): 1.147 + if hasattr(self._zipfile, "extractall"): 1.148 + return self._zipfile.extractall(path) 1.149 + 1.150 + path = self._getnormalizedpath(path) 1.151 + 1.152 + for name in self._zipfile.namelist(): 1.153 + self._extractname(name, path) 1.154 + 1.155 +log = logging.getLogger() 1.156 + 1.157 +def isURL(thing): 1.158 + """Return True if |thing| looks like a URL.""" 1.159 + # We want to download URLs like http://... but not Windows paths like c:\... 1.160 + return len(urlparse(thing).scheme) >= 2 1.161 + 1.162 +# Python does not provide strsignal() even in the very latest 3.x. 1.163 +# This is a reasonable fake. 1.164 +def strsig(n): 1.165 + # Signal numbers run 0 through NSIG-1; an array with NSIG members 1.166 + # has exactly that many slots 1.167 + _sigtbl = [None]*signal.NSIG 1.168 + for k in dir(signal): 1.169 + if k.startswith("SIG") and not k.startswith("SIG_") and k != "SIGCLD" and k != "SIGPOLL": 1.170 + _sigtbl[getattr(signal, k)] = k 1.171 + # Realtime signals mostly have no names 1.172 + if hasattr(signal, "SIGRTMIN") and hasattr(signal, "SIGRTMAX"): 1.173 + for r in range(signal.SIGRTMIN+1, signal.SIGRTMAX+1): 1.174 + _sigtbl[r] = "SIGRTMIN+" + str(r - signal.SIGRTMIN) 1.175 + # Fill in any remaining gaps 1.176 + for i in range(signal.NSIG): 1.177 + if _sigtbl[i] is None: 1.178 + _sigtbl[i] = "unrecognized signal, number " + str(i) 1.179 + if n < 0 or n >= signal.NSIG: 1.180 + return "out-of-range signal, number "+str(n) 1.181 + return _sigtbl[n] 1.182 + 1.183 +def printstatus(status, name = ""): 1.184 + # 'status' is the exit status 1.185 + if os.name != 'posix': 1.186 + # Windows error codes are easier to look up if printed in hexadecimal 1.187 + if status < 0: 1.188 + status += 2**32 1.189 + print "TEST-INFO | %s: exit status %x\n" % (name, status) 1.190 + elif os.WIFEXITED(status): 1.191 + print "TEST-INFO | %s: exit %d\n" % (name, os.WEXITSTATUS(status)) 1.192 + elif os.WIFSIGNALED(status): 1.193 + # The python stdlib doesn't appear to have strsignal(), alas 1.194 + print "TEST-INFO | {}: killed by {}".format(name,strsig(os.WTERMSIG(status))) 1.195 + else: 1.196 + # This is probably a can't-happen condition on Unix, but let's be defensive 1.197 + print "TEST-INFO | %s: undecodable exit status %04x\n" % (name, status) 1.198 + 1.199 +def addCommonOptions(parser, defaults={}): 1.200 + parser.add_option("--xre-path", 1.201 + action = "store", type = "string", dest = "xrePath", 1.202 + # individual scripts will set a sane default 1.203 + default = None, 1.204 + help = "absolute path to directory containing XRE (probably xulrunner)") 1.205 + if 'SYMBOLS_PATH' not in defaults: 1.206 + defaults['SYMBOLS_PATH'] = None 1.207 + parser.add_option("--symbols-path", 1.208 + action = "store", type = "string", dest = "symbolsPath", 1.209 + default = defaults['SYMBOLS_PATH'], 1.210 + help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols") 1.211 + parser.add_option("--debugger", 1.212 + action = "store", dest = "debugger", 1.213 + help = "use the given debugger to launch the application") 1.214 + parser.add_option("--debugger-args", 1.215 + action = "store", dest = "debuggerArgs", 1.216 + help = "pass the given args to the debugger _before_ " 1.217 + "the application on the command line") 1.218 + parser.add_option("--debugger-interactive", 1.219 + action = "store_true", dest = "debuggerInteractive", 1.220 + help = "prevents the test harness from redirecting " 1.221 + "stdout and stderr for interactive debuggers") 1.222 + 1.223 +def getFullPath(directory, path): 1.224 + "Get an absolute path relative to 'directory'." 1.225 + return os.path.normpath(os.path.join(directory, os.path.expanduser(path))) 1.226 + 1.227 +def searchPath(directory, path): 1.228 + "Go one step beyond getFullPath and try the various folders in PATH" 1.229 + # Try looking in the current working directory first. 1.230 + newpath = getFullPath(directory, path) 1.231 + if os.path.isfile(newpath): 1.232 + return newpath 1.233 + 1.234 + # At this point we have to fail if a directory was given (to prevent cases 1.235 + # like './gdb' from matching '/usr/bin/./gdb'). 1.236 + if not os.path.dirname(path): 1.237 + for dir in os.environ['PATH'].split(os.pathsep): 1.238 + newpath = os.path.join(dir, path) 1.239 + if os.path.isfile(newpath): 1.240 + return newpath 1.241 + return None 1.242 + 1.243 +def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False): 1.244 + 1.245 + debuggerInfo = None 1.246 + 1.247 + if debugger: 1.248 + debuggerPath = searchPath(directory, debugger) 1.249 + if not debuggerPath: 1.250 + print "Error: Path %s doesn't exist." % debugger 1.251 + sys.exit(1) 1.252 + 1.253 + debuggerName = os.path.basename(debuggerPath).lower() 1.254 + 1.255 + def getDebuggerInfo(type, default): 1.256 + if debuggerName in DEBUGGER_INFO and type in DEBUGGER_INFO[debuggerName]: 1.257 + return DEBUGGER_INFO[debuggerName][type] 1.258 + return default 1.259 + 1.260 + debuggerInfo = { 1.261 + "path": debuggerPath, 1.262 + "interactive" : getDebuggerInfo("interactive", False), 1.263 + "args": getDebuggerInfo("args", "").split(), 1.264 + "requiresEscapedArgs": getDebuggerInfo("requiresEscapedArgs", False) 1.265 + } 1.266 + 1.267 + if debuggerArgs: 1.268 + debuggerInfo["args"] = debuggerArgs.split() 1.269 + if debuggerInteractive: 1.270 + debuggerInfo["interactive"] = debuggerInteractive 1.271 + 1.272 + return debuggerInfo 1.273 + 1.274 + 1.275 +def dumpLeakLog(leakLogFile, filter = False): 1.276 + """Process the leak log, without parsing it. 1.277 + 1.278 + Use this function if you want the raw log only. 1.279 + Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable. 1.280 + """ 1.281 + 1.282 + # Don't warn (nor "info") if the log file is not there. 1.283 + if not os.path.exists(leakLogFile): 1.284 + return 1.285 + 1.286 + with open(leakLogFile, "r") as leaks: 1.287 + leakReport = leaks.read() 1.288 + 1.289 + # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out. 1.290 + # Only check whether an actual leak was reported. 1.291 + if filter and not "0 TOTAL " in leakReport: 1.292 + return 1.293 + 1.294 + # Simply copy the log. 1.295 + log.info(leakReport.rstrip("\n")) 1.296 + 1.297 +def processSingleLeakFile(leakLogFileName, processType, leakThreshold): 1.298 + """Process a single leak log. 1.299 + """ 1.300 + 1.301 + # Per-Inst Leaked Total Rem ... 1.302 + # 0 TOTAL 17 192 419115886 2 ... 1.303 + # 833 nsTimerImpl 60 120 24726 2 ... 1.304 + lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+" 1.305 + r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+" 1.306 + r"-?\d+\s+(?P<numLeaked>-?\d+)") 1.307 + 1.308 + processString = "" 1.309 + if processType: 1.310 + # eg 'plugin' 1.311 + processString = " %s process:" % processType 1.312 + 1.313 + crashedOnPurpose = False 1.314 + totalBytesLeaked = None 1.315 + leakAnalysis = [] 1.316 + leakedObjectNames = [] 1.317 + with open(leakLogFileName, "r") as leaks: 1.318 + for line in leaks: 1.319 + if line.find("purposefully crash") > -1: 1.320 + crashedOnPurpose = True 1.321 + matches = lineRe.match(line) 1.322 + if not matches: 1.323 + # eg: the leak table header row 1.324 + log.info(line.rstrip()) 1.325 + continue 1.326 + name = matches.group("name") 1.327 + size = int(matches.group("size")) 1.328 + bytesLeaked = int(matches.group("bytesLeaked")) 1.329 + numLeaked = int(matches.group("numLeaked")) 1.330 + # Output the raw line from the leak log table if it is the TOTAL row, 1.331 + # or is for an object row that has been leaked. 1.332 + if numLeaked != 0 or name == "TOTAL": 1.333 + log.info(line.rstrip()) 1.334 + # Analyse the leak log, but output later or it will interrupt the leak table 1.335 + if name == "TOTAL": 1.336 + totalBytesLeaked = bytesLeaked 1.337 + if size < 0 or bytesLeaked < 0 or numLeaked < 0: 1.338 + leakAnalysis.append("TEST-UNEXPECTED-FAIL | leakcheck |%s negative leaks caught!" 1.339 + % processString) 1.340 + continue 1.341 + if name != "TOTAL" and numLeaked != 0: 1.342 + leakedObjectNames.append(name) 1.343 + leakAnalysis.append("TEST-INFO | leakcheck |%s leaked %d %s (%s bytes)" 1.344 + % (processString, numLeaked, name, bytesLeaked)) 1.345 + log.info('\n'.join(leakAnalysis)) 1.346 + 1.347 + if totalBytesLeaked is None: 1.348 + # We didn't see a line with name 'TOTAL' 1.349 + if crashedOnPurpose: 1.350 + log.info("TEST-INFO | leakcheck |%s deliberate crash and thus no leak log" 1.351 + % processString) 1.352 + else: 1.353 + # TODO: This should be a TEST-UNEXPECTED-FAIL, but was changed to a warning 1.354 + # due to too many intermittent failures (see bug 831223). 1.355 + log.info("WARNING | leakcheck |%s missing output line for total leaks!" 1.356 + % processString) 1.357 + return 1.358 + 1.359 + if totalBytesLeaked == 0: 1.360 + log.info("TEST-PASS | leakcheck |%s no leaks detected!" % processString) 1.361 + return 1.362 + 1.363 + # totalBytesLeaked was seen and is non-zero. 1.364 + if totalBytesLeaked > leakThreshold: 1.365 + # Fail the run if we're over the threshold (which defaults to 0) 1.366 + prefix = "TEST-UNEXPECTED-FAIL" 1.367 + else: 1.368 + prefix = "WARNING" 1.369 + # Create a comma delimited string of the first N leaked objects found, 1.370 + # to aid with bug summary matching in TBPL. Note: The order of the objects 1.371 + # had no significance (they're sorted alphabetically). 1.372 + maxSummaryObjects = 5 1.373 + leakedObjectSummary = ', '.join(leakedObjectNames[:maxSummaryObjects]) 1.374 + if len(leakedObjectNames) > maxSummaryObjects: 1.375 + leakedObjectSummary += ', ...' 1.376 + log.info("%s | leakcheck |%s %d bytes leaked (%s)" 1.377 + % (prefix, processString, totalBytesLeaked, leakedObjectSummary)) 1.378 + 1.379 +def processLeakLog(leakLogFile, leakThreshold = 0): 1.380 + """Process the leak log, including separate leak logs created 1.381 + by child processes. 1.382 + 1.383 + Use this function if you want an additional PASS/FAIL summary. 1.384 + It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable. 1.385 + """ 1.386 + 1.387 + if not os.path.exists(leakLogFile): 1.388 + log.info("WARNING | leakcheck | refcount logging is off, so leaks can't be detected!") 1.389 + return 1.390 + 1.391 + if leakThreshold != 0: 1.392 + log.info("TEST-INFO | leakcheck | threshold set at %d bytes" % leakThreshold) 1.393 + 1.394 + (leakLogFileDir, leakFileBase) = os.path.split(leakLogFile) 1.395 + fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*$") 1.396 + if leakFileBase[-4:] == ".log": 1.397 + leakFileBase = leakFileBase[:-4] 1.398 + fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*.log$") 1.399 + 1.400 + for fileName in os.listdir(leakLogFileDir): 1.401 + if fileName.find(leakFileBase) != -1: 1.402 + thisFile = os.path.join(leakLogFileDir, fileName) 1.403 + processType = None 1.404 + m = fileNameRegExp.search(fileName) 1.405 + if m: 1.406 + processType = m.group(1) 1.407 + processSingleLeakFile(thisFile, processType, leakThreshold) 1.408 + 1.409 +def replaceBackSlashes(input): 1.410 + return input.replace('\\', '/') 1.411 + 1.412 +class KeyValueParseError(Exception): 1.413 + """error when parsing strings of serialized key-values""" 1.414 + def __init__(self, msg, errors=()): 1.415 + self.errors = errors 1.416 + Exception.__init__(self, msg) 1.417 + 1.418 +def parseKeyValue(strings, separator='=', context='key, value: '): 1.419 + """ 1.420 + parse string-serialized key-value pairs in the form of 1.421 + `key = value`. Returns a list of 2-tuples. 1.422 + Note that whitespace is not stripped. 1.423 + """ 1.424 + 1.425 + # syntax check 1.426 + missing = [string for string in strings if separator not in string] 1.427 + if missing: 1.428 + raise KeyValueParseError("Error: syntax error in %s" % (context, 1.429 + ','.join(missing)), 1.430 + errors=missing) 1.431 + return [string.split(separator, 1) for string in strings] 1.432 + 1.433 +def systemMemory(): 1.434 + """ 1.435 + Returns total system memory in kilobytes. 1.436 + Works only on unix-like platforms where `free` is in the path. 1.437 + """ 1.438 + return int(os.popen("free").readlines()[1].split()[1]) 1.439 + 1.440 +def environment(xrePath, env=None, crashreporter=True, debugger=False, dmdPath=None): 1.441 + """populate OS environment variables for mochitest""" 1.442 + 1.443 + env = os.environ.copy() if env is None else env 1.444 + 1.445 + assert os.path.isabs(xrePath) 1.446 + 1.447 + ldLibraryPath = xrePath 1.448 + 1.449 + envVar = None 1.450 + dmdLibrary = None 1.451 + preloadEnvVar = None 1.452 + if mozinfo.isUnix: 1.453 + envVar = "LD_LIBRARY_PATH" 1.454 + env['MOZILLA_FIVE_HOME'] = xrePath 1.455 + dmdLibrary = "libdmd.so" 1.456 + preloadEnvVar = "LD_PRELOAD" 1.457 + elif mozinfo.isMac: 1.458 + envVar = "DYLD_LIBRARY_PATH" 1.459 + dmdLibrary = "libdmd.dylib" 1.460 + preloadEnvVar = "DYLD_INSERT_LIBRARIES" 1.461 + elif mozinfo.isWin: 1.462 + envVar = "PATH" 1.463 + dmdLibrary = "dmd.dll" 1.464 + preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB" 1.465 + if envVar: 1.466 + envValue = ((env.get(envVar), str(ldLibraryPath)) 1.467 + if mozinfo.isWin 1.468 + else (ldLibraryPath, dmdPath, env.get(envVar))) 1.469 + env[envVar] = os.path.pathsep.join([path for path in envValue if path]) 1.470 + 1.471 + if dmdPath and dmdLibrary and preloadEnvVar: 1.472 + env['DMD'] = '1' 1.473 + env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary) 1.474 + 1.475 + # crashreporter 1.476 + env['GNOME_DISABLE_CRASH_DIALOG'] = '1' 1.477 + env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1' 1.478 + env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1' 1.479 + 1.480 + if crashreporter and not debugger: 1.481 + env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' 1.482 + env['MOZ_CRASHREPORTER'] = '1' 1.483 + else: 1.484 + env['MOZ_CRASHREPORTER_DISABLE'] = '1' 1.485 + 1.486 + # Crash on non-local network connections. 1.487 + env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1' 1.488 + 1.489 + # Set WebRTC logging in case it is not set yet 1.490 + env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:5,datachannel:5') 1.491 + env.setdefault('R_LOG_LEVEL', '6') 1.492 + env.setdefault('R_LOG_DESTINATION', 'stderr') 1.493 + env.setdefault('R_LOG_VERBOSE', '1') 1.494 + 1.495 + # ASan specific environment stuff 1.496 + asan = bool(mozinfo.info.get("asan")) 1.497 + if asan and (mozinfo.isLinux or mozinfo.isMac): 1.498 + try: 1.499 + # Symbolizer support 1.500 + llvmsym = os.path.join(xrePath, "llvm-symbolizer") 1.501 + if os.path.isfile(llvmsym): 1.502 + env["ASAN_SYMBOLIZER_PATH"] = llvmsym 1.503 + log.info("ASan using symbolizer at %s", llvmsym) 1.504 + 1.505 + totalMemory = systemMemory() 1.506 + 1.507 + # Only 4 GB RAM or less available? Use custom ASan options to reduce 1.508 + # the amount of resources required to do the tests. Standard options 1.509 + # will otherwise lead to OOM conditions on the current test slaves. 1.510 + message = "INFO | runtests.py | ASan running in %s configuration" 1.511 + if totalMemory <= 1024 * 1024 * 4: 1.512 + message = message % 'low-memory' 1.513 + env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5" 1.514 + else: 1.515 + message = message % 'default memory' 1.516 + except OSError,err: 1.517 + log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror) 1.518 + except: 1.519 + log.info("Failed determine available memory, disabling ASan low-memory configuration") 1.520 + else: 1.521 + log.info(message) 1.522 + 1.523 + return env 1.524 + 1.525 +def dumpScreen(utilityPath): 1.526 + """dumps a screenshot of the entire screen to a directory specified by 1.527 + the MOZ_UPLOAD_DIR environment variable""" 1.528 + import mozfile 1.529 + 1.530 + # Need to figure out which OS-dependent tool to use 1.531 + if mozinfo.isUnix: 1.532 + utility = [os.path.join(utilityPath, "screentopng")] 1.533 + utilityname = "screentopng" 1.534 + elif mozinfo.isMac: 1.535 + utility = ['/usr/sbin/screencapture', '-C', '-x', '-t', 'png'] 1.536 + utilityname = "screencapture" 1.537 + elif mozinfo.isWin: 1.538 + utility = [os.path.join(utilityPath, "screenshot.exe")] 1.539 + utilityname = "screenshot" 1.540 + 1.541 + # Get dir where to write the screenshot file 1.542 + parent_dir = os.environ.get('MOZ_UPLOAD_DIR', None) 1.543 + if not parent_dir: 1.544 + log.info('Failed to retrieve MOZ_UPLOAD_DIR env var') 1.545 + return 1.546 + 1.547 + # Run the capture 1.548 + try: 1.549 + tmpfd, imgfilename = tempfile.mkstemp(prefix='mozilla-test-fail-screenshot_', suffix='.png', dir=parent_dir) 1.550 + os.close(tmpfd) 1.551 + returncode = subprocess.call(utility + [imgfilename]) 1.552 + printstatus(returncode, utilityname) 1.553 + except OSError, err: 1.554 + log.info("Failed to start %s for screenshot: %s", 1.555 + utility[0], err.strerror) 1.556 + return 1.557 + 1.558 +class ShutdownLeaks(object): 1.559 + """ 1.560 + Parses the mochitest run log when running a debug build, assigns all leaked 1.561 + DOM windows (that are still around after test suite shutdown, despite running 1.562 + the GC) to the tests that created them and prints leak statistics. 1.563 + """ 1.564 + 1.565 + def __init__(self, logger): 1.566 + self.logger = logger 1.567 + self.tests = [] 1.568 + self.leakedWindows = {} 1.569 + self.leakedDocShells = set() 1.570 + self.currentTest = None 1.571 + self.seenShutdown = False 1.572 + 1.573 + def log(self, line): 1.574 + if line[2:11] == "DOMWINDOW": 1.575 + self._logWindow(line) 1.576 + elif line[2:10] == "DOCSHELL": 1.577 + self._logDocShell(line) 1.578 + elif line.startswith("TEST-START"): 1.579 + fileName = line.split(" ")[-1].strip().replace("chrome://mochitests/content/browser/", "") 1.580 + self.currentTest = {"fileName": fileName, "windows": set(), "docShells": set()} 1.581 + elif line.startswith("INFO TEST-END"): 1.582 + # don't track a test if no windows or docShells leaked 1.583 + if self.currentTest and (self.currentTest["windows"] or self.currentTest["docShells"]): 1.584 + self.tests.append(self.currentTest) 1.585 + self.currentTest = None 1.586 + elif line.startswith("INFO TEST-START | Shutdown"): 1.587 + self.seenShutdown = True 1.588 + 1.589 + def process(self): 1.590 + for test in self._parseLeakingTests(): 1.591 + for url, count in self._zipLeakedWindows(test["leakedWindows"]): 1.592 + self.logger("TEST-UNEXPECTED-FAIL | %s | leaked %d window(s) until shutdown [url = %s]", test["fileName"], count, url) 1.593 + 1.594 + if test["leakedDocShells"]: 1.595 + self.logger("TEST-UNEXPECTED-FAIL | %s | leaked %d docShell(s) until shutdown", test["fileName"], len(test["leakedDocShells"])) 1.596 + 1.597 + def _logWindow(self, line): 1.598 + created = line[:2] == "++" 1.599 + pid = self._parseValue(line, "pid") 1.600 + serial = self._parseValue(line, "serial") 1.601 + 1.602 + # log line has invalid format 1.603 + if not pid or not serial: 1.604 + self.logger("TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>", line) 1.605 + return 1.606 + 1.607 + key = pid + "." + serial 1.608 + 1.609 + if self.currentTest: 1.610 + windows = self.currentTest["windows"] 1.611 + if created: 1.612 + windows.add(key) 1.613 + else: 1.614 + windows.discard(key) 1.615 + elif self.seenShutdown and not created: 1.616 + self.leakedWindows[key] = self._parseValue(line, "url") 1.617 + 1.618 + def _logDocShell(self, line): 1.619 + created = line[:2] == "++" 1.620 + pid = self._parseValue(line, "pid") 1.621 + id = self._parseValue(line, "id") 1.622 + 1.623 + # log line has invalid format 1.624 + if not pid or not id: 1.625 + self.logger("TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>", line) 1.626 + return 1.627 + 1.628 + key = pid + "." + id 1.629 + 1.630 + if self.currentTest: 1.631 + docShells = self.currentTest["docShells"] 1.632 + if created: 1.633 + docShells.add(key) 1.634 + else: 1.635 + docShells.discard(key) 1.636 + elif self.seenShutdown and not created: 1.637 + self.leakedDocShells.add(key) 1.638 + 1.639 + def _parseValue(self, line, name): 1.640 + match = re.search("\[%s = (.+?)\]" % name, line) 1.641 + if match: 1.642 + return match.group(1) 1.643 + return None 1.644 + 1.645 + def _parseLeakingTests(self): 1.646 + leakingTests = [] 1.647 + 1.648 + for test in self.tests: 1.649 + test["leakedWindows"] = [self.leakedWindows[id] for id in test["windows"] if id in self.leakedWindows] 1.650 + test["leakedDocShells"] = [id for id in test["docShells"] if id in self.leakedDocShells] 1.651 + test["leakCount"] = len(test["leakedWindows"]) + len(test["leakedDocShells"]) 1.652 + 1.653 + if test["leakCount"]: 1.654 + leakingTests.append(test) 1.655 + 1.656 + return sorted(leakingTests, key=itemgetter("leakCount"), reverse=True) 1.657 + 1.658 + def _zipLeakedWindows(self, leakedWindows): 1.659 + counts = [] 1.660 + counted = set() 1.661 + 1.662 + for url in leakedWindows: 1.663 + if not url in counted: 1.664 + counts.append((url, leakedWindows.count(url))) 1.665 + counted.add(url) 1.666 + 1.667 + return sorted(counts, key=itemgetter(1), reverse=True)