build/automationutils.py

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 #
michael@0 2 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 # License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 5
michael@0 6 from __future__ import with_statement
michael@0 7 import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile
michael@0 8 import base64
michael@0 9 import re
michael@0 10 import os
michael@0 11 from urlparse import urlparse
michael@0 12 from operator import itemgetter
michael@0 13 import signal
michael@0 14
michael@0 15 try:
michael@0 16 import mozinfo
michael@0 17 except ImportError:
michael@0 18 # Stub out fake mozinfo since this is not importable on Android 4.0 Opt.
michael@0 19 # This should be fixed; see
michael@0 20 # https://bugzilla.mozilla.org/show_bug.cgi?id=650881
michael@0 21 mozinfo = type('mozinfo', (), dict(info={}))()
michael@0 22 mozinfo.isWin = mozinfo.isLinux = mozinfo.isUnix = mozinfo.isMac = False
michael@0 23
michael@0 24 # TODO! FILE: localautomation :/
michael@0 25 # mapping from would-be mozinfo attr <-> sys.platform
michael@0 26 mapping = {'isMac': ['mac', 'darwin'],
michael@0 27 'isLinux': ['linux', 'linux2'],
michael@0 28 'isWin': ['win32', 'win64'],
michael@0 29 }
michael@0 30 mapping = dict(sum([[(value, key) for value in values] for key, values in mapping.items()], []))
michael@0 31 attr = mapping.get(sys.platform)
michael@0 32 if attr:
michael@0 33 setattr(mozinfo, attr, True)
michael@0 34 if mozinfo.isLinux:
michael@0 35 mozinfo.isUnix = True
michael@0 36
michael@0 37 __all__ = [
michael@0 38 "ZipFileReader",
michael@0 39 "addCommonOptions",
michael@0 40 "dumpLeakLog",
michael@0 41 "isURL",
michael@0 42 "processLeakLog",
michael@0 43 "getDebuggerInfo",
michael@0 44 "DEBUGGER_INFO",
michael@0 45 "replaceBackSlashes",
michael@0 46 'KeyValueParseError',
michael@0 47 'parseKeyValue',
michael@0 48 'systemMemory',
michael@0 49 'environment',
michael@0 50 'dumpScreen',
michael@0 51 "ShutdownLeaks"
michael@0 52 ]
michael@0 53
michael@0 54 # Map of debugging programs to information about them, like default arguments
michael@0 55 # and whether or not they are interactive.
michael@0 56 DEBUGGER_INFO = {
michael@0 57 # gdb requires that you supply the '--args' flag in order to pass arguments
michael@0 58 # after the executable name to the executable.
michael@0 59 "gdb": {
michael@0 60 "interactive": True,
michael@0 61 "args": "-q --args"
michael@0 62 },
michael@0 63
michael@0 64 "cgdb": {
michael@0 65 "interactive": True,
michael@0 66 "args": "-q --args"
michael@0 67 },
michael@0 68
michael@0 69 "lldb": {
michael@0 70 "interactive": True,
michael@0 71 "args": "--",
michael@0 72 "requiresEscapedArgs": True
michael@0 73 },
michael@0 74
michael@0 75 # valgrind doesn't explain much about leaks unless you set the
michael@0 76 # '--leak-check=full' flag. But there are a lot of objects that are
michael@0 77 # semi-deliberately leaked, so we set '--show-possibly-lost=no' to avoid
michael@0 78 # uninteresting output from those objects. We set '--smc-check==all-non-file'
michael@0 79 # and '--vex-iropt-register-updates=allregs-at-mem-access' so that valgrind
michael@0 80 # deals properly with JIT'd JavaScript code.
michael@0 81 "valgrind": {
michael@0 82 "interactive": False,
michael@0 83 "args": " ".join(["--leak-check=full",
michael@0 84 "--show-possibly-lost=no",
michael@0 85 "--smc-check=all-non-file",
michael@0 86 "--vex-iropt-register-updates=allregs-at-mem-access"])
michael@0 87 }
michael@0 88 }
michael@0 89
michael@0 90 class ZipFileReader(object):
michael@0 91 """
michael@0 92 Class to read zip files in Python 2.5 and later. Limited to only what we
michael@0 93 actually use.
michael@0 94 """
michael@0 95
michael@0 96 def __init__(self, filename):
michael@0 97 self._zipfile = zipfile.ZipFile(filename, "r")
michael@0 98
michael@0 99 def __del__(self):
michael@0 100 self._zipfile.close()
michael@0 101
michael@0 102 def _getnormalizedpath(self, path):
michael@0 103 """
michael@0 104 Gets a normalized path from 'path' (or the current working directory if
michael@0 105 'path' is None). Also asserts that the path exists.
michael@0 106 """
michael@0 107 if path is None:
michael@0 108 path = os.curdir
michael@0 109 path = os.path.normpath(os.path.expanduser(path))
michael@0 110 assert os.path.isdir(path)
michael@0 111 return path
michael@0 112
michael@0 113 def _extractname(self, name, path):
michael@0 114 """
michael@0 115 Extracts a file with the given name from the zip file to the given path.
michael@0 116 Also creates any directories needed along the way.
michael@0 117 """
michael@0 118 filename = os.path.normpath(os.path.join(path, name))
michael@0 119 if name.endswith("/"):
michael@0 120 os.makedirs(filename)
michael@0 121 else:
michael@0 122 path = os.path.split(filename)[0]
michael@0 123 if not os.path.isdir(path):
michael@0 124 os.makedirs(path)
michael@0 125 with open(filename, "wb") as dest:
michael@0 126 dest.write(self._zipfile.read(name))
michael@0 127
michael@0 128 def namelist(self):
michael@0 129 return self._zipfile.namelist()
michael@0 130
michael@0 131 def read(self, name):
michael@0 132 return self._zipfile.read(name)
michael@0 133
michael@0 134 def extract(self, name, path = None):
michael@0 135 if hasattr(self._zipfile, "extract"):
michael@0 136 return self._zipfile.extract(name, path)
michael@0 137
michael@0 138 # This will throw if name is not part of the zip file.
michael@0 139 self._zipfile.getinfo(name)
michael@0 140
michael@0 141 self._extractname(name, self._getnormalizedpath(path))
michael@0 142
michael@0 143 def extractall(self, path = None):
michael@0 144 if hasattr(self._zipfile, "extractall"):
michael@0 145 return self._zipfile.extractall(path)
michael@0 146
michael@0 147 path = self._getnormalizedpath(path)
michael@0 148
michael@0 149 for name in self._zipfile.namelist():
michael@0 150 self._extractname(name, path)
michael@0 151
michael@0 152 log = logging.getLogger()
michael@0 153
michael@0 154 def isURL(thing):
michael@0 155 """Return True if |thing| looks like a URL."""
michael@0 156 # We want to download URLs like http://... but not Windows paths like c:\...
michael@0 157 return len(urlparse(thing).scheme) >= 2
michael@0 158
michael@0 159 # Python does not provide strsignal() even in the very latest 3.x.
michael@0 160 # This is a reasonable fake.
michael@0 161 def strsig(n):
michael@0 162 # Signal numbers run 0 through NSIG-1; an array with NSIG members
michael@0 163 # has exactly that many slots
michael@0 164 _sigtbl = [None]*signal.NSIG
michael@0 165 for k in dir(signal):
michael@0 166 if k.startswith("SIG") and not k.startswith("SIG_") and k != "SIGCLD" and k != "SIGPOLL":
michael@0 167 _sigtbl[getattr(signal, k)] = k
michael@0 168 # Realtime signals mostly have no names
michael@0 169 if hasattr(signal, "SIGRTMIN") and hasattr(signal, "SIGRTMAX"):
michael@0 170 for r in range(signal.SIGRTMIN+1, signal.SIGRTMAX+1):
michael@0 171 _sigtbl[r] = "SIGRTMIN+" + str(r - signal.SIGRTMIN)
michael@0 172 # Fill in any remaining gaps
michael@0 173 for i in range(signal.NSIG):
michael@0 174 if _sigtbl[i] is None:
michael@0 175 _sigtbl[i] = "unrecognized signal, number " + str(i)
michael@0 176 if n < 0 or n >= signal.NSIG:
michael@0 177 return "out-of-range signal, number "+str(n)
michael@0 178 return _sigtbl[n]
michael@0 179
michael@0 180 def printstatus(status, name = ""):
michael@0 181 # 'status' is the exit status
michael@0 182 if os.name != 'posix':
michael@0 183 # Windows error codes are easier to look up if printed in hexadecimal
michael@0 184 if status < 0:
michael@0 185 status += 2**32
michael@0 186 print "TEST-INFO | %s: exit status %x\n" % (name, status)
michael@0 187 elif os.WIFEXITED(status):
michael@0 188 print "TEST-INFO | %s: exit %d\n" % (name, os.WEXITSTATUS(status))
michael@0 189 elif os.WIFSIGNALED(status):
michael@0 190 # The python stdlib doesn't appear to have strsignal(), alas
michael@0 191 print "TEST-INFO | {}: killed by {}".format(name,strsig(os.WTERMSIG(status)))
michael@0 192 else:
michael@0 193 # This is probably a can't-happen condition on Unix, but let's be defensive
michael@0 194 print "TEST-INFO | %s: undecodable exit status %04x\n" % (name, status)
michael@0 195
michael@0 196 def addCommonOptions(parser, defaults={}):
michael@0 197 parser.add_option("--xre-path",
michael@0 198 action = "store", type = "string", dest = "xrePath",
michael@0 199 # individual scripts will set a sane default
michael@0 200 default = None,
michael@0 201 help = "absolute path to directory containing XRE (probably xulrunner)")
michael@0 202 if 'SYMBOLS_PATH' not in defaults:
michael@0 203 defaults['SYMBOLS_PATH'] = None
michael@0 204 parser.add_option("--symbols-path",
michael@0 205 action = "store", type = "string", dest = "symbolsPath",
michael@0 206 default = defaults['SYMBOLS_PATH'],
michael@0 207 help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
michael@0 208 parser.add_option("--debugger",
michael@0 209 action = "store", dest = "debugger",
michael@0 210 help = "use the given debugger to launch the application")
michael@0 211 parser.add_option("--debugger-args",
michael@0 212 action = "store", dest = "debuggerArgs",
michael@0 213 help = "pass the given args to the debugger _before_ "
michael@0 214 "the application on the command line")
michael@0 215 parser.add_option("--debugger-interactive",
michael@0 216 action = "store_true", dest = "debuggerInteractive",
michael@0 217 help = "prevents the test harness from redirecting "
michael@0 218 "stdout and stderr for interactive debuggers")
michael@0 219
michael@0 220 def getFullPath(directory, path):
michael@0 221 "Get an absolute path relative to 'directory'."
michael@0 222 return os.path.normpath(os.path.join(directory, os.path.expanduser(path)))
michael@0 223
michael@0 224 def searchPath(directory, path):
michael@0 225 "Go one step beyond getFullPath and try the various folders in PATH"
michael@0 226 # Try looking in the current working directory first.
michael@0 227 newpath = getFullPath(directory, path)
michael@0 228 if os.path.isfile(newpath):
michael@0 229 return newpath
michael@0 230
michael@0 231 # At this point we have to fail if a directory was given (to prevent cases
michael@0 232 # like './gdb' from matching '/usr/bin/./gdb').
michael@0 233 if not os.path.dirname(path):
michael@0 234 for dir in os.environ['PATH'].split(os.pathsep):
michael@0 235 newpath = os.path.join(dir, path)
michael@0 236 if os.path.isfile(newpath):
michael@0 237 return newpath
michael@0 238 return None
michael@0 239
michael@0 240 def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
michael@0 241
michael@0 242 debuggerInfo = None
michael@0 243
michael@0 244 if debugger:
michael@0 245 debuggerPath = searchPath(directory, debugger)
michael@0 246 if not debuggerPath:
michael@0 247 print "Error: Path %s doesn't exist." % debugger
michael@0 248 sys.exit(1)
michael@0 249
michael@0 250 debuggerName = os.path.basename(debuggerPath).lower()
michael@0 251
michael@0 252 def getDebuggerInfo(type, default):
michael@0 253 if debuggerName in DEBUGGER_INFO and type in DEBUGGER_INFO[debuggerName]:
michael@0 254 return DEBUGGER_INFO[debuggerName][type]
michael@0 255 return default
michael@0 256
michael@0 257 debuggerInfo = {
michael@0 258 "path": debuggerPath,
michael@0 259 "interactive" : getDebuggerInfo("interactive", False),
michael@0 260 "args": getDebuggerInfo("args", "").split(),
michael@0 261 "requiresEscapedArgs": getDebuggerInfo("requiresEscapedArgs", False)
michael@0 262 }
michael@0 263
michael@0 264 if debuggerArgs:
michael@0 265 debuggerInfo["args"] = debuggerArgs.split()
michael@0 266 if debuggerInteractive:
michael@0 267 debuggerInfo["interactive"] = debuggerInteractive
michael@0 268
michael@0 269 return debuggerInfo
michael@0 270
michael@0 271
michael@0 272 def dumpLeakLog(leakLogFile, filter = False):
michael@0 273 """Process the leak log, without parsing it.
michael@0 274
michael@0 275 Use this function if you want the raw log only.
michael@0 276 Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
michael@0 277 """
michael@0 278
michael@0 279 # Don't warn (nor "info") if the log file is not there.
michael@0 280 if not os.path.exists(leakLogFile):
michael@0 281 return
michael@0 282
michael@0 283 with open(leakLogFile, "r") as leaks:
michael@0 284 leakReport = leaks.read()
michael@0 285
michael@0 286 # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
michael@0 287 # Only check whether an actual leak was reported.
michael@0 288 if filter and not "0 TOTAL " in leakReport:
michael@0 289 return
michael@0 290
michael@0 291 # Simply copy the log.
michael@0 292 log.info(leakReport.rstrip("\n"))
michael@0 293
michael@0 294 def processSingleLeakFile(leakLogFileName, processType, leakThreshold):
michael@0 295 """Process a single leak log.
michael@0 296 """
michael@0 297
michael@0 298 # Per-Inst Leaked Total Rem ...
michael@0 299 # 0 TOTAL 17 192 419115886 2 ...
michael@0 300 # 833 nsTimerImpl 60 120 24726 2 ...
michael@0 301 lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+"
michael@0 302 r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
michael@0 303 r"-?\d+\s+(?P<numLeaked>-?\d+)")
michael@0 304
michael@0 305 processString = ""
michael@0 306 if processType:
michael@0 307 # eg 'plugin'
michael@0 308 processString = " %s process:" % processType
michael@0 309
michael@0 310 crashedOnPurpose = False
michael@0 311 totalBytesLeaked = None
michael@0 312 leakAnalysis = []
michael@0 313 leakedObjectNames = []
michael@0 314 with open(leakLogFileName, "r") as leaks:
michael@0 315 for line in leaks:
michael@0 316 if line.find("purposefully crash") > -1:
michael@0 317 crashedOnPurpose = True
michael@0 318 matches = lineRe.match(line)
michael@0 319 if not matches:
michael@0 320 # eg: the leak table header row
michael@0 321 log.info(line.rstrip())
michael@0 322 continue
michael@0 323 name = matches.group("name")
michael@0 324 size = int(matches.group("size"))
michael@0 325 bytesLeaked = int(matches.group("bytesLeaked"))
michael@0 326 numLeaked = int(matches.group("numLeaked"))
michael@0 327 # Output the raw line from the leak log table if it is the TOTAL row,
michael@0 328 # or is for an object row that has been leaked.
michael@0 329 if numLeaked != 0 or name == "TOTAL":
michael@0 330 log.info(line.rstrip())
michael@0 331 # Analyse the leak log, but output later or it will interrupt the leak table
michael@0 332 if name == "TOTAL":
michael@0 333 totalBytesLeaked = bytesLeaked
michael@0 334 if size < 0 or bytesLeaked < 0 or numLeaked < 0:
michael@0 335 leakAnalysis.append("TEST-UNEXPECTED-FAIL | leakcheck |%s negative leaks caught!"
michael@0 336 % processString)
michael@0 337 continue
michael@0 338 if name != "TOTAL" and numLeaked != 0:
michael@0 339 leakedObjectNames.append(name)
michael@0 340 leakAnalysis.append("TEST-INFO | leakcheck |%s leaked %d %s (%s bytes)"
michael@0 341 % (processString, numLeaked, name, bytesLeaked))
michael@0 342 log.info('\n'.join(leakAnalysis))
michael@0 343
michael@0 344 if totalBytesLeaked is None:
michael@0 345 # We didn't see a line with name 'TOTAL'
michael@0 346 if crashedOnPurpose:
michael@0 347 log.info("TEST-INFO | leakcheck |%s deliberate crash and thus no leak log"
michael@0 348 % processString)
michael@0 349 else:
michael@0 350 # TODO: This should be a TEST-UNEXPECTED-FAIL, but was changed to a warning
michael@0 351 # due to too many intermittent failures (see bug 831223).
michael@0 352 log.info("WARNING | leakcheck |%s missing output line for total leaks!"
michael@0 353 % processString)
michael@0 354 return
michael@0 355
michael@0 356 if totalBytesLeaked == 0:
michael@0 357 log.info("TEST-PASS | leakcheck |%s no leaks detected!" % processString)
michael@0 358 return
michael@0 359
michael@0 360 # totalBytesLeaked was seen and is non-zero.
michael@0 361 if totalBytesLeaked > leakThreshold:
michael@0 362 # Fail the run if we're over the threshold (which defaults to 0)
michael@0 363 prefix = "TEST-UNEXPECTED-FAIL"
michael@0 364 else:
michael@0 365 prefix = "WARNING"
michael@0 366 # Create a comma delimited string of the first N leaked objects found,
michael@0 367 # to aid with bug summary matching in TBPL. Note: The order of the objects
michael@0 368 # had no significance (they're sorted alphabetically).
michael@0 369 maxSummaryObjects = 5
michael@0 370 leakedObjectSummary = ', '.join(leakedObjectNames[:maxSummaryObjects])
michael@0 371 if len(leakedObjectNames) > maxSummaryObjects:
michael@0 372 leakedObjectSummary += ', ...'
michael@0 373 log.info("%s | leakcheck |%s %d bytes leaked (%s)"
michael@0 374 % (prefix, processString, totalBytesLeaked, leakedObjectSummary))
michael@0 375
michael@0 376 def processLeakLog(leakLogFile, leakThreshold = 0):
michael@0 377 """Process the leak log, including separate leak logs created
michael@0 378 by child processes.
michael@0 379
michael@0 380 Use this function if you want an additional PASS/FAIL summary.
michael@0 381 It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
michael@0 382 """
michael@0 383
michael@0 384 if not os.path.exists(leakLogFile):
michael@0 385 log.info("WARNING | leakcheck | refcount logging is off, so leaks can't be detected!")
michael@0 386 return
michael@0 387
michael@0 388 if leakThreshold != 0:
michael@0 389 log.info("TEST-INFO | leakcheck | threshold set at %d bytes" % leakThreshold)
michael@0 390
michael@0 391 (leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
michael@0 392 fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*$")
michael@0 393 if leakFileBase[-4:] == ".log":
michael@0 394 leakFileBase = leakFileBase[:-4]
michael@0 395 fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*.log$")
michael@0 396
michael@0 397 for fileName in os.listdir(leakLogFileDir):
michael@0 398 if fileName.find(leakFileBase) != -1:
michael@0 399 thisFile = os.path.join(leakLogFileDir, fileName)
michael@0 400 processType = None
michael@0 401 m = fileNameRegExp.search(fileName)
michael@0 402 if m:
michael@0 403 processType = m.group(1)
michael@0 404 processSingleLeakFile(thisFile, processType, leakThreshold)
michael@0 405
michael@0 406 def replaceBackSlashes(input):
michael@0 407 return input.replace('\\', '/')
michael@0 408
michael@0 409 class KeyValueParseError(Exception):
michael@0 410 """error when parsing strings of serialized key-values"""
michael@0 411 def __init__(self, msg, errors=()):
michael@0 412 self.errors = errors
michael@0 413 Exception.__init__(self, msg)
michael@0 414
michael@0 415 def parseKeyValue(strings, separator='=', context='key, value: '):
michael@0 416 """
michael@0 417 parse string-serialized key-value pairs in the form of
michael@0 418 `key = value`. Returns a list of 2-tuples.
michael@0 419 Note that whitespace is not stripped.
michael@0 420 """
michael@0 421
michael@0 422 # syntax check
michael@0 423 missing = [string for string in strings if separator not in string]
michael@0 424 if missing:
michael@0 425 raise KeyValueParseError("Error: syntax error in %s" % (context,
michael@0 426 ','.join(missing)),
michael@0 427 errors=missing)
michael@0 428 return [string.split(separator, 1) for string in strings]
michael@0 429
michael@0 430 def systemMemory():
michael@0 431 """
michael@0 432 Returns total system memory in kilobytes.
michael@0 433 Works only on unix-like platforms where `free` is in the path.
michael@0 434 """
michael@0 435 return int(os.popen("free").readlines()[1].split()[1])
michael@0 436
michael@0 437 def environment(xrePath, env=None, crashreporter=True, debugger=False, dmdPath=None):
michael@0 438 """populate OS environment variables for mochitest"""
michael@0 439
michael@0 440 env = os.environ.copy() if env is None else env
michael@0 441
michael@0 442 assert os.path.isabs(xrePath)
michael@0 443
michael@0 444 ldLibraryPath = xrePath
michael@0 445
michael@0 446 envVar = None
michael@0 447 dmdLibrary = None
michael@0 448 preloadEnvVar = None
michael@0 449 if mozinfo.isUnix:
michael@0 450 envVar = "LD_LIBRARY_PATH"
michael@0 451 env['MOZILLA_FIVE_HOME'] = xrePath
michael@0 452 dmdLibrary = "libdmd.so"
michael@0 453 preloadEnvVar = "LD_PRELOAD"
michael@0 454 elif mozinfo.isMac:
michael@0 455 envVar = "DYLD_LIBRARY_PATH"
michael@0 456 dmdLibrary = "libdmd.dylib"
michael@0 457 preloadEnvVar = "DYLD_INSERT_LIBRARIES"
michael@0 458 elif mozinfo.isWin:
michael@0 459 envVar = "PATH"
michael@0 460 dmdLibrary = "dmd.dll"
michael@0 461 preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB"
michael@0 462 if envVar:
michael@0 463 envValue = ((env.get(envVar), str(ldLibraryPath))
michael@0 464 if mozinfo.isWin
michael@0 465 else (ldLibraryPath, dmdPath, env.get(envVar)))
michael@0 466 env[envVar] = os.path.pathsep.join([path for path in envValue if path])
michael@0 467
michael@0 468 if dmdPath and dmdLibrary and preloadEnvVar:
michael@0 469 env['DMD'] = '1'
michael@0 470 env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary)
michael@0 471
michael@0 472 # crashreporter
michael@0 473 env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
michael@0 474 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
michael@0 475 env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
michael@0 476
michael@0 477 if crashreporter and not debugger:
michael@0 478 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
michael@0 479 env['MOZ_CRASHREPORTER'] = '1'
michael@0 480 else:
michael@0 481 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
michael@0 482
michael@0 483 # Crash on non-local network connections.
michael@0 484 env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
michael@0 485
michael@0 486 # Set WebRTC logging in case it is not set yet
michael@0 487 env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:5,datachannel:5')
michael@0 488 env.setdefault('R_LOG_LEVEL', '6')
michael@0 489 env.setdefault('R_LOG_DESTINATION', 'stderr')
michael@0 490 env.setdefault('R_LOG_VERBOSE', '1')
michael@0 491
michael@0 492 # ASan specific environment stuff
michael@0 493 asan = bool(mozinfo.info.get("asan"))
michael@0 494 if asan and (mozinfo.isLinux or mozinfo.isMac):
michael@0 495 try:
michael@0 496 # Symbolizer support
michael@0 497 llvmsym = os.path.join(xrePath, "llvm-symbolizer")
michael@0 498 if os.path.isfile(llvmsym):
michael@0 499 env["ASAN_SYMBOLIZER_PATH"] = llvmsym
michael@0 500 log.info("ASan using symbolizer at %s", llvmsym)
michael@0 501
michael@0 502 totalMemory = systemMemory()
michael@0 503
michael@0 504 # Only 4 GB RAM or less available? Use custom ASan options to reduce
michael@0 505 # the amount of resources required to do the tests. Standard options
michael@0 506 # will otherwise lead to OOM conditions on the current test slaves.
michael@0 507 message = "INFO | runtests.py | ASan running in %s configuration"
michael@0 508 if totalMemory <= 1024 * 1024 * 4:
michael@0 509 message = message % 'low-memory'
michael@0 510 env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5"
michael@0 511 else:
michael@0 512 message = message % 'default memory'
michael@0 513 except OSError,err:
michael@0 514 log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror)
michael@0 515 except:
michael@0 516 log.info("Failed determine available memory, disabling ASan low-memory configuration")
michael@0 517 else:
michael@0 518 log.info(message)
michael@0 519
michael@0 520 return env
michael@0 521
michael@0 522 def dumpScreen(utilityPath):
michael@0 523 """dumps a screenshot of the entire screen to a directory specified by
michael@0 524 the MOZ_UPLOAD_DIR environment variable"""
michael@0 525 import mozfile
michael@0 526
michael@0 527 # Need to figure out which OS-dependent tool to use
michael@0 528 if mozinfo.isUnix:
michael@0 529 utility = [os.path.join(utilityPath, "screentopng")]
michael@0 530 utilityname = "screentopng"
michael@0 531 elif mozinfo.isMac:
michael@0 532 utility = ['/usr/sbin/screencapture', '-C', '-x', '-t', 'png']
michael@0 533 utilityname = "screencapture"
michael@0 534 elif mozinfo.isWin:
michael@0 535 utility = [os.path.join(utilityPath, "screenshot.exe")]
michael@0 536 utilityname = "screenshot"
michael@0 537
michael@0 538 # Get dir where to write the screenshot file
michael@0 539 parent_dir = os.environ.get('MOZ_UPLOAD_DIR', None)
michael@0 540 if not parent_dir:
michael@0 541 log.info('Failed to retrieve MOZ_UPLOAD_DIR env var')
michael@0 542 return
michael@0 543
michael@0 544 # Run the capture
michael@0 545 try:
michael@0 546 tmpfd, imgfilename = tempfile.mkstemp(prefix='mozilla-test-fail-screenshot_', suffix='.png', dir=parent_dir)
michael@0 547 os.close(tmpfd)
michael@0 548 returncode = subprocess.call(utility + [imgfilename])
michael@0 549 printstatus(returncode, utilityname)
michael@0 550 except OSError, err:
michael@0 551 log.info("Failed to start %s for screenshot: %s",
michael@0 552 utility[0], err.strerror)
michael@0 553 return
michael@0 554
michael@0 555 class ShutdownLeaks(object):
michael@0 556 """
michael@0 557 Parses the mochitest run log when running a debug build, assigns all leaked
michael@0 558 DOM windows (that are still around after test suite shutdown, despite running
michael@0 559 the GC) to the tests that created them and prints leak statistics.
michael@0 560 """
michael@0 561
michael@0 562 def __init__(self, logger):
michael@0 563 self.logger = logger
michael@0 564 self.tests = []
michael@0 565 self.leakedWindows = {}
michael@0 566 self.leakedDocShells = set()
michael@0 567 self.currentTest = None
michael@0 568 self.seenShutdown = False
michael@0 569
michael@0 570 def log(self, line):
michael@0 571 if line[2:11] == "DOMWINDOW":
michael@0 572 self._logWindow(line)
michael@0 573 elif line[2:10] == "DOCSHELL":
michael@0 574 self._logDocShell(line)
michael@0 575 elif line.startswith("TEST-START"):
michael@0 576 fileName = line.split(" ")[-1].strip().replace("chrome://mochitests/content/browser/", "")
michael@0 577 self.currentTest = {"fileName": fileName, "windows": set(), "docShells": set()}
michael@0 578 elif line.startswith("INFO TEST-END"):
michael@0 579 # don't track a test if no windows or docShells leaked
michael@0 580 if self.currentTest and (self.currentTest["windows"] or self.currentTest["docShells"]):
michael@0 581 self.tests.append(self.currentTest)
michael@0 582 self.currentTest = None
michael@0 583 elif line.startswith("INFO TEST-START | Shutdown"):
michael@0 584 self.seenShutdown = True
michael@0 585
michael@0 586 def process(self):
michael@0 587 for test in self._parseLeakingTests():
michael@0 588 for url, count in self._zipLeakedWindows(test["leakedWindows"]):
michael@0 589 self.logger("TEST-UNEXPECTED-FAIL | %s | leaked %d window(s) until shutdown [url = %s]", test["fileName"], count, url)
michael@0 590
michael@0 591 if test["leakedDocShells"]:
michael@0 592 self.logger("TEST-UNEXPECTED-FAIL | %s | leaked %d docShell(s) until shutdown", test["fileName"], len(test["leakedDocShells"]))
michael@0 593
michael@0 594 def _logWindow(self, line):
michael@0 595 created = line[:2] == "++"
michael@0 596 pid = self._parseValue(line, "pid")
michael@0 597 serial = self._parseValue(line, "serial")
michael@0 598
michael@0 599 # log line has invalid format
michael@0 600 if not pid or not serial:
michael@0 601 self.logger("TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>", line)
michael@0 602 return
michael@0 603
michael@0 604 key = pid + "." + serial
michael@0 605
michael@0 606 if self.currentTest:
michael@0 607 windows = self.currentTest["windows"]
michael@0 608 if created:
michael@0 609 windows.add(key)
michael@0 610 else:
michael@0 611 windows.discard(key)
michael@0 612 elif self.seenShutdown and not created:
michael@0 613 self.leakedWindows[key] = self._parseValue(line, "url")
michael@0 614
michael@0 615 def _logDocShell(self, line):
michael@0 616 created = line[:2] == "++"
michael@0 617 pid = self._parseValue(line, "pid")
michael@0 618 id = self._parseValue(line, "id")
michael@0 619
michael@0 620 # log line has invalid format
michael@0 621 if not pid or not id:
michael@0 622 self.logger("TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>", line)
michael@0 623 return
michael@0 624
michael@0 625 key = pid + "." + id
michael@0 626
michael@0 627 if self.currentTest:
michael@0 628 docShells = self.currentTest["docShells"]
michael@0 629 if created:
michael@0 630 docShells.add(key)
michael@0 631 else:
michael@0 632 docShells.discard(key)
michael@0 633 elif self.seenShutdown and not created:
michael@0 634 self.leakedDocShells.add(key)
michael@0 635
michael@0 636 def _parseValue(self, line, name):
michael@0 637 match = re.search("\[%s = (.+?)\]" % name, line)
michael@0 638 if match:
michael@0 639 return match.group(1)
michael@0 640 return None
michael@0 641
michael@0 642 def _parseLeakingTests(self):
michael@0 643 leakingTests = []
michael@0 644
michael@0 645 for test in self.tests:
michael@0 646 test["leakedWindows"] = [self.leakedWindows[id] for id in test["windows"] if id in self.leakedWindows]
michael@0 647 test["leakedDocShells"] = [id for id in test["docShells"] if id in self.leakedDocShells]
michael@0 648 test["leakCount"] = len(test["leakedWindows"]) + len(test["leakedDocShells"])
michael@0 649
michael@0 650 if test["leakCount"]:
michael@0 651 leakingTests.append(test)
michael@0 652
michael@0 653 return sorted(leakingTests, key=itemgetter("leakCount"), reverse=True)
michael@0 654
michael@0 655 def _zipLeakedWindows(self, leakedWindows):
michael@0 656 counts = []
michael@0 657 counted = set()
michael@0 658
michael@0 659 for url in leakedWindows:
michael@0 660 if not url in counted:
michael@0 661 counts.append((url, leakedWindows.count(url)))
michael@0 662 counted.add(url)
michael@0 663
michael@0 664 return sorted(counts, key=itemgetter(1), reverse=True)

mercurial