2
# This Source Code Form is subject to the terms of the Mozilla Public
3
# License, v. 2.0. If a copy of the MPL was not distributed with this
4
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
from __future__ import with_statement
7
import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile
9
from urlparse import urlparse
24
# Map of debugging programs to information about them, like default arguments
25
# and whether or not they are interactive.
27
# gdb requires that you supply the '--args' flag in order to pass arguments
28
# after the executable name to the executable.
34
# valgrind doesn't explain much about leaks unless you set the
35
# '--leak-check=full' flag.
38
"args": "--leak-check=full"
42
class ZipFileReader(object):
44
Class to read zip files in Python 2.5 and later. Limited to only what we
48
def __init__(self, filename):
49
self._zipfile = zipfile.ZipFile(filename, "r")
54
def _getnormalizedpath(self, path):
56
Gets a normalized path from 'path' (or the current working directory if
57
'path' is None). Also asserts that the path exists.
61
path = os.path.normpath(os.path.expanduser(path))
62
assert os.path.isdir(path)
65
def _extractname(self, name, path):
67
Extracts a file with the given name from the zip file to the given path.
68
Also creates any directories needed along the way.
70
filename = os.path.normpath(os.path.join(path, name))
71
if name.endswith("/"):
74
path = os.path.split(filename)[0]
75
if not os.path.isdir(path):
77
with open(filename, "wb") as dest:
78
dest.write(self._zipfile.read(name))
81
return self._zipfile.namelist()
84
return self._zipfile.read(name)
86
def extract(self, name, path = None):
87
if hasattr(self._zipfile, "extract"):
88
return self._zipfile.extract(name, path)
90
# This will throw if name is not part of the zip file.
91
self._zipfile.getinfo(name)
93
self._extractname(name, self._getnormalizedpath(path))
95
def extractall(self, path = None):
96
if hasattr(self._zipfile, "extractall"):
97
return self._zipfile.extractall(path)
99
path = self._getnormalizedpath(path)
101
for name in self._zipfile.namelist():
102
self._extractname(name, path)
104
log = logging.getLogger()
107
"""Return True if |thing| looks like a URL."""
108
# We want to download URLs like http://... but not Windows paths like c:\...
109
return len(urlparse(thing).scheme) >= 2
111
def addCommonOptions(parser, defaults={}):
112
parser.add_option("--xre-path",
113
action = "store", type = "string", dest = "xrePath",
114
# individual scripts will set a sane default
116
help = "absolute path to directory containing XRE (probably xulrunner)")
117
if 'SYMBOLS_PATH' not in defaults:
118
defaults['SYMBOLS_PATH'] = None
119
parser.add_option("--symbols-path",
120
action = "store", type = "string", dest = "symbolsPath",
121
default = defaults['SYMBOLS_PATH'],
122
help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
123
parser.add_option("--debugger",
124
action = "store", dest = "debugger",
125
help = "use the given debugger to launch the application")
126
parser.add_option("--debugger-args",
127
action = "store", dest = "debuggerArgs",
128
help = "pass the given args to the debugger _before_ "
129
"the application on the command line")
130
parser.add_option("--debugger-interactive",
131
action = "store_true", dest = "debuggerInteractive",
132
help = "prevents the test harness from redirecting "
133
"stdout and stderr for interactive debuggers")
135
def checkForCrashes(dumpDir, symbolsPath, testName=None):
136
stackwalkPath = os.environ.get('MINIDUMP_STACKWALK', None)
137
# try to get the caller's filename if no test name is given
140
testName = os.path.basename(sys._getframe(1).f_code.co_filename)
144
# Check preconditions
145
dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
150
removeSymbolsPath = False
152
# If our symbols are at a remote URL, download them now
153
if symbolsPath and isURL(symbolsPath):
154
print "Downloading symbols from: " + symbolsPath
155
removeSymbolsPath = True
156
# Get the symbols and write them to a temporary zipfile
157
data = urllib2.urlopen(symbolsPath)
158
symbolsFile = tempfile.TemporaryFile()
159
symbolsFile.write(data.read())
160
# extract symbols to a temporary directory (which we'll delete after
161
# processing all crashes)
162
symbolsPath = tempfile.mkdtemp()
163
zfile = ZipFileReader(symbolsFile)
164
zfile.extractall(symbolsPath)
168
stackwalkOutput.append("Crash dump filename: " + d)
170
if symbolsPath and stackwalkPath and os.path.exists(stackwalkPath):
171
# run minidump stackwalk
172
p = subprocess.Popen([stackwalkPath, d, symbolsPath],
173
stdout=subprocess.PIPE,
174
stderr=subprocess.PIPE)
175
(out, err) = p.communicate()
177
# minidump_stackwalk is chatty, so ignore stderr when it succeeds.
178
stackwalkOutput.append(out)
179
# The top frame of the crash is always the line after "Thread N (crashed)"
182
# 0 libnss3.so!nssCertificate_Destroy [certificate.c : 102 + 0x0]
183
# 0 mozjs.dll!js::GlobalObject::getDebuggers() [GlobalObject.cpp:89df18f9b6da : 580 + 0x0]
184
# 0 libxul.so!void js::gc::MarkInternal<JSObject>(JSTracer*, JSObject**) [Marking.cpp : 92 + 0x28]
185
lines = out.splitlines()
186
for i, line in enumerate(lines):
187
if "(crashed)" in line:
188
match = re.search(r"^ 0 (?:.*!)?(?:void )?([^\[]+)", lines[i+1])
190
topFrame = "@ %s" % match.group(1).strip()
193
stackwalkOutput.append("stderr from minidump_stackwalk:")
194
stackwalkOutput.append(err)
195
if p.returncode != 0:
196
stackwalkOutput.append("minidump_stackwalk exited with return code %d" % p.returncode)
199
stackwalkOutput.append("No symbols path given, can't process dump.")
200
if not stackwalkPath:
201
stackwalkOutput.append("MINIDUMP_STACKWALK not set, can't process dump.")
202
elif stackwalkPath and not os.path.exists(stackwalkPath):
203
stackwalkOutput.append("MINIDUMP_STACKWALK binary not found: %s" % stackwalkPath)
205
topFrame = "Unknown top frame"
206
log.info("PROCESS-CRASH | %s | application crashed [%s]", testName, topFrame)
207
print '\n'.join(stackwalkOutput)
208
dumpSavePath = os.environ.get('MINIDUMP_SAVE_PATH', None)
210
shutil.move(d, dumpSavePath)
211
print "Saved dump as %s" % os.path.join(dumpSavePath,
215
extra = os.path.splitext(d)[0] + ".extra"
216
if os.path.exists(extra):
219
if removeSymbolsPath:
220
shutil.rmtree(symbolsPath)
224
def getFullPath(directory, path):
225
"Get an absolute path relative to 'directory'."
226
return os.path.normpath(os.path.join(directory, os.path.expanduser(path)))
228
def searchPath(directory, path):
229
"Go one step beyond getFullPath and try the various folders in PATH"
230
# Try looking in the current working directory first.
231
newpath = getFullPath(directory, path)
232
if os.path.isfile(newpath):
235
# At this point we have to fail if a directory was given (to prevent cases
236
# like './gdb' from matching '/usr/bin/./gdb').
237
if not os.path.dirname(path):
238
for dir in os.environ['PATH'].split(os.pathsep):
239
newpath = os.path.join(dir, path)
240
if os.path.isfile(newpath):
244
def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
249
debuggerPath = searchPath(directory, debugger)
251
print "Error: Path %s doesn't exist." % debugger
254
debuggerName = os.path.basename(debuggerPath).lower()
256
def getDebuggerInfo(type, default):
257
if debuggerName in DEBUGGER_INFO and type in DEBUGGER_INFO[debuggerName]:
258
return DEBUGGER_INFO[debuggerName][type]
262
"path": debuggerPath,
263
"interactive" : getDebuggerInfo("interactive", False),
264
"args": getDebuggerInfo("args", "").split()
268
debuggerInfo["args"] = debuggerArgs.split()
269
if debuggerInteractive:
270
debuggerInfo["interactive"] = debuggerInteractive
275
def dumpLeakLog(leakLogFile, filter = False):
276
"""Process the leak log, without parsing it.
278
Use this function if you want the raw log only.
279
Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
282
# Don't warn (nor "info") if the log file is not there.
283
if not os.path.exists(leakLogFile):
286
with open(leakLogFile, "r") as leaks:
287
leakReport = leaks.read()
289
# Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
290
# Only check whether an actual leak was reported.
291
if filter and not "0 TOTAL " in leakReport:
294
# Simply copy the log.
295
log.info(leakReport.rstrip("\n"))
297
def processSingleLeakFile(leakLogFileName, processType, leakThreshold):
298
"""Process a single leak log.
301
# Per-Inst Leaked Total Rem ...
302
# 0 TOTAL 17 192 419115886 2 ...
303
# 833 nsTimerImpl 60 120 24726 2 ...
304
lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+"
305
r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
306
r"-?\d+\s+(?P<numLeaked>-?\d+)")
311
processString = " %s process:" % processType
313
crashedOnPurpose = False
314
totalBytesLeaked = None
316
leakedObjectNames = []
317
with open(leakLogFileName, "r") as leaks:
319
if line.find("purposefully crash") > -1:
320
crashedOnPurpose = True
321
matches = lineRe.match(line)
323
# eg: the leak table header row
324
log.info(line.rstrip())
326
name = matches.group("name")
327
size = int(matches.group("size"))
328
bytesLeaked = int(matches.group("bytesLeaked"))
329
numLeaked = int(matches.group("numLeaked"))
330
# Output the raw line from the leak log table if it is the TOTAL row,
331
# or is for an object row that has been leaked.
332
if numLeaked != 0 or name == "TOTAL":
333
log.info(line.rstrip())
334
# Analyse the leak log, but output later or it will interrupt the leak table
336
totalBytesLeaked = bytesLeaked
337
if size < 0 or bytesLeaked < 0 or numLeaked < 0:
338
leakAnalysis.append("TEST-UNEXPECTED-FAIL | leakcheck |%s negative leaks caught!"
341
if name != "TOTAL" and numLeaked != 0:
342
leakedObjectNames.append(name)
343
leakAnalysis.append("TEST-INFO | leakcheck |%s leaked %d %s (%s bytes)"
344
% (processString, numLeaked, name, bytesLeaked))
345
log.info('\n'.join(leakAnalysis))
347
if totalBytesLeaked is None:
348
# We didn't see a line with name 'TOTAL'
350
log.info("TEST-INFO | leakcheck |%s deliberate crash and thus no leak log"
353
# TODO: This should be a TEST-UNEXPECTED-FAIL, but was changed to a warning
354
# due to too many intermittent failures (see bug 831223).
355
log.info("WARNING | leakcheck |%s missing output line for total leaks!"
359
if totalBytesLeaked == 0:
360
log.info("TEST-PASS | leakcheck |%s no leaks detected!" % processString)
363
# totalBytesLeaked was seen and is non-zero.
364
if totalBytesLeaked > leakThreshold:
365
# Fail the run if we're over the threshold (which defaults to 0)
366
prefix = "TEST-UNEXPECTED-FAIL"
369
# Create a comma delimited string of the first N leaked objects found,
370
# to aid with bug summary matching in TBPL. Note: The order of the objects
371
# had no significance (they're sorted alphabetically).
372
maxSummaryObjects = 5
373
leakedObjectSummary = ', '.join(leakedObjectNames[:maxSummaryObjects])
374
if len(leakedObjectNames) > maxSummaryObjects:
375
leakedObjectSummary += ', ...'
376
log.info("%s | leakcheck |%s %d bytes leaked (%s)"
377
% (prefix, processString, totalBytesLeaked, leakedObjectSummary))
379
def processLeakLog(leakLogFile, leakThreshold = 0):
380
"""Process the leak log, including separate leak logs created
383
Use this function if you want an additional PASS/FAIL summary.
384
It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
387
if not os.path.exists(leakLogFile):
388
log.info("WARNING | leakcheck | refcount logging is off, so leaks can't be detected!")
391
if leakThreshold != 0:
392
log.info("TEST-INFO | leakcheck | threshold set at %d bytes" % leakThreshold)
394
(leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
395
fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*$")
396
if leakFileBase[-4:] == ".log":
397
leakFileBase = leakFileBase[:-4]
398
fileNameRegExp = re.compile(r".*?_([a-z]*)_pid\d*.log$")
400
for fileName in os.listdir(leakLogFileDir):
401
if fileName.find(leakFileBase) != -1:
402
thisFile = os.path.join(leakLogFileDir, fileName)
404
m = fileNameRegExp.search(fileName)
406
processType = m.group(1)
407
processSingleLeakFile(thisFile, processType, leakThreshold)
409
def replaceBackSlashes(input):
410
return input.replace('\\', '/')
412
def wrapCommand(cmd):
414
If running on OS X 10.5 or older, wrap |cmd| so that it will
415
be executed as an i386 binary, in case it's a 32-bit/64-bit universal
418
if platform.system() == "Darwin" and \
419
hasattr(platform, 'mac_ver') and \
420
platform.mac_ver()[0][:4] < '10.6':
421
return ["arch", "-arch", "i386"] + cmd
422
# otherwise just execute the command normally