build/automationutils.py

changeset 0
6474c204b198
     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)

mercurial