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 leaks
= open(leakLogFile
, "r")
287 leakReport
= leaks
.read()
290 # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
291 # Only check whether an actual leak was reported.
292 if filter and not "0 TOTAL " in leakReport
:
295 # Simply copy the log.
296 log
.info(leakReport
.rstrip("\n"))
298 def processSingleLeakFile(leakLogFileName
, PID
, processType
, leakThreshold
):
299 """Process a single leak log, corresponding to the specified
300 process PID and type.
303 # Per-Inst Leaked Total Rem ...
304 # 0 TOTAL 17 192 419115886 2 ...
305 # 833 nsTimerImpl 60 120 24726 2 ...
306 lineRe
= re
.compile(r
"^\s*\d+\s+(?P<name>\S+)\s+"
307 r
"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
308 r
"-?\d+\s+(?P<numLeaked>-?\d+)")
311 if PID
and processType
:
312 processString
= "| %s process %s " % (processType
, PID
)
313 leaks
= open(leakLogFileName
, "r")
315 matches
= lineRe
.match(line
)
317 int(matches
.group("numLeaked")) == 0 and
318 matches
.group("name") != "TOTAL"):
320 log
.info(line
.rstrip())
323 leaks
= open(leakLogFileName
, "r")
325 crashedOnPurpose
= False
329 if line
.find("purposefully crash") > -1:
330 crashedOnPurpose
= True
331 matches
= lineRe
.match(line
)
334 name
= matches
.group("name")
335 size
= int(matches
.group("size"))
336 bytesLeaked
= int(matches
.group("bytesLeaked"))
337 numLeaked
= int(matches
.group("numLeaked"))
338 if size
< 0 or bytesLeaked
< 0 or numLeaked
< 0:
339 log
.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | negative leaks caught!" %
343 elif name
== "TOTAL":
346 if bytesLeaked
< 0 or bytesLeaked
> leakThreshold
:
347 prefix
= "TEST-UNEXPECTED-FAIL"
348 leakLog
= "TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | leaked" \
349 " %d bytes during test execution" % (processString
, bytesLeaked
)
350 elif bytesLeaked
> 0:
351 leakLog
= "TEST-PASS %s| automationutils.processLeakLog() | WARNING leaked" \
352 " %d bytes during test execution" % (processString
, bytesLeaked
)
354 leakLog
= "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
356 # Remind the threshold if it is not 0, which is the default/goal.
357 if leakThreshold
!= 0:
358 leakLog
+= " (threshold set at %d bytes)" % leakThreshold
359 # Log the information.
364 instance
= "instances"
365 rest
= " each (%s bytes total)" % matches
.group("bytesLeaked")
367 instance
= "instance"
371 # don't spam brief tinderbox logs with tons of leak output
373 log
.info("%(prefix)s %(process)s| automationutils.processLeakLog() | leaked %(numLeaked)d %(instance)s of %(name)s "
374 "with size %(size)s bytes%(rest)s" %
376 "process": processString
,
377 "numLeaked": numLeaked
,
378 "instance": instance
,
380 "size": matches
.group("size"),
384 log
.info("INFO | automationutils.processLeakLog() | process %s was " \
385 "deliberately crashed and thus has no leak log" % PID
)
387 log
.info("WARNING | automationutils.processLeakLog() | missing output line for total leaks!")
391 def processLeakLog(leakLogFile
, leakThreshold
= 0):
392 """Process the leak log, including separate leak logs created
395 Use this function if you want an additional PASS/FAIL summary.
396 It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
399 if not os
.path
.exists(leakLogFile
):
400 log
.info("WARNING | automationutils.processLeakLog() | refcount logging is off, so leaks can't be detected!")
403 (leakLogFileDir
, leakFileBase
) = os
.path
.split(leakLogFile
)
404 pidRegExp
= re
.compile(r
".*?_([a-z]*)_pid(\d*)$")
405 if leakFileBase
[-4:] == ".log":
406 leakFileBase
= leakFileBase
[:-4]
407 pidRegExp
= re
.compile(r
".*?_([a-z]*)_pid(\d*).log$")
409 for fileName
in os
.listdir(leakLogFileDir
):
410 if fileName
.find(leakFileBase
) != -1:
411 thisFile
= os
.path
.join(leakLogFileDir
, fileName
)
414 m
= pidRegExp
.search(fileName
)
416 processType
= m
.group(1)
417 processPID
= m
.group(2)
418 processSingleLeakFile(thisFile
, processPID
, processType
, leakThreshold
)
420 def replaceBackSlashes(input):
421 return input.replace('\\', '/')
423 def wrapCommand(cmd
):
425 If running on OS X 10.5 or older, wrap |cmd| so that it will
426 be executed as an i386 binary, in case it's a 32-bit/64-bit universal
429 if platform
.system() == "Darwin" and \
430 hasattr(platform
, 'mac_ver') and \
431 platform
.mac_ver()[0][:4] < '10.6':
432 return ["arch", "-arch", "i386"] + cmd
433 # otherwise just execute the command normally