2 # ***** BEGIN LICENSE BLOCK *****
3 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 # The contents of this file are subject to the Mozilla Public License Version
6 # 1.1 (the "License"); you may not use this file except in compliance with
7 # the License. You may obtain a copy of the License at
8 # http://www.mozilla.org/MPL/
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 # for the specific language governing rights and limitations under the
15 # The Original Code is mozilla.org code.
17 # The Initial Developer of the Original Code is The Mozilla Foundation
18 # Portions created by the Initial Developer are Copyright (C) 2009
19 # the Initial Developer. All Rights Reserved.
22 # Serge Gautherie <sgautherie.bz@free.fr>
23 # Ted Mielczarek <ted.mielczarek@gmail.com>
25 # Alternatively, the contents of this file may be used under the terms of
26 # either the GNU General Public License Version 2 or later (the "GPL"), or
27 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 # in which case the provisions of the GPL or the LGPL are applicable instead
29 # of those above. If you wish to allow use of your version of this file only
30 # under the terms of either the GPL or the LGPL, and not to allow others to
31 # use your version of this file under the terms of the MPL, indicate your
32 # decision by deleting the provisions above and replace them with the notice
33 # and other provisions required by the GPL or the LGPL. If you do not delete
34 # the provisions above, a recipient may use your version of this file under
35 # the terms of any one of the MPL, the GPL or the LGPL.
37 # ***** END LICENSE BLOCK ***** */
39 import glob
, logging
, os
, shutil
, subprocess
, sys
41 from urlparse
import urlparse
54 # Map of debugging programs to information about them, like default arguments
55 # and whether or not they are interactive.
57 # gdb requires that you supply the '--args' flag in order to pass arguments
58 # after the executable name to the executable.
64 # valgrind doesn't explain much about leaks unless you set the
65 # '--leak-check=full' flag.
68 "args": "--leak-check=full"
72 log
= logging
.getLogger()
75 """Return True if |thing| looks like a URL."""
76 return urlparse(thing
).scheme
!= ''
78 def addCommonOptions(parser
, defaults
={}):
79 parser
.add_option("--xre-path",
80 action
= "store", type = "string", dest
= "xrePath",
81 # individual scripts will set a sane default
83 help = "absolute path to directory containing XRE (probably xulrunner)")
84 if 'SYMBOLS_PATH' not in defaults
:
85 defaults
['SYMBOLS_PATH'] = None
86 parser
.add_option("--symbols-path",
87 action
= "store", type = "string", dest
= "symbolsPath",
88 default
= defaults
['SYMBOLS_PATH'],
89 help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
90 parser
.add_option("--debugger",
91 action
= "store", dest
= "debugger",
92 help = "use the given debugger to launch the application")
93 parser
.add_option("--debugger-args",
94 action
= "store", dest
= "debuggerArgs",
95 help = "pass the given args to the debugger _before_ "
96 "the application on the command line")
97 parser
.add_option("--debugger-interactive",
98 action
= "store_true", dest
= "debuggerInteractive",
99 help = "prevents the test harness from redirecting "
100 "stdout and stderr for interactive debuggers")
102 def checkForCrashes(dumpDir
, symbolsPath
, testName
=None):
103 stackwalkPath
= os
.environ
.get('MINIDUMP_STACKWALK', None)
104 stackwalkCGI
= os
.environ
.get('MINIDUMP_STACKWALK_CGI', None)
105 # try to get the caller's filename if no test name is given
108 testName
= os
.path
.basename(sys
._getframe
(1).f_code
.co_filename
)
113 dumps
= glob
.glob(os
.path
.join(dumpDir
, '*.dmp'))
115 log
.info("PROCESS-CRASH | %s | application crashed (minidump found)", testName
)
116 if symbolsPath
and stackwalkPath
and os
.path
.exists(stackwalkPath
):
117 nullfd
= open(os
.devnull
, 'w')
118 # eat minidump_stackwalk errors
119 subprocess
.call([stackwalkPath
, d
, symbolsPath
], stderr
=nullfd
)
121 elif stackwalkCGI
and symbolsPath
and isURL(symbolsPath
):
125 sys
.path
.append(os
.path
.join(os
.path
.dirname(__file__
), "poster.zip"))
126 from poster
.encode
import multipart_encode
127 from poster
.streaminghttp
import register_openers
130 datagen
, headers
= multipart_encode({"minidump": f
,
131 "symbols": symbolsPath
})
132 request
= urllib2
.Request(stackwalkCGI
, datagen
, headers
)
133 print urllib2
.urlopen(request
).read()
139 print "No symbols path given, can't process dump."
140 if not stackwalkPath
and not stackwalkCGI
:
141 print "Neither MINIDUMP_STACKWALK nor MINIDUMP_STACKWALK_CGI is set, can't process dump."
143 if stackwalkPath
and not os
.path
.exists(stackwalkPath
):
144 print "MINIDUMP_STACKWALK binary not found: %s" % stackwalkPath
145 elif stackwalkCGI
and not isURL(stackwalkCGI
):
146 print "MINIDUMP_STACKWALK_CGI is not a URL: %s" % stackwalkCGI
147 elif symbolsPath
and not isURL(symbolsPath
):
148 print "symbolsPath is not a URL: %s" % symbolsPath
149 dumpSavePath
= os
.environ
.get('MINIDUMP_SAVE_PATH', None)
151 shutil
.move(d
, dumpSavePath
)
152 print "Saved dump as %s" % os
.path
.join(dumpSavePath
,
156 extra
= os
.path
.splitext(d
)[0] + ".extra"
157 if os
.path
.exists(extra
):
163 def getFullPath(directory
, path
):
164 "Get an absolute path relative to 'directory'."
165 return os
.path
.normpath(os
.path
.join(directory
, os
.path
.expanduser(path
)))
167 def searchPath(directory
, path
):
168 "Go one step beyond getFullPath and try the various folders in PATH"
169 # Try looking in the current working directory first.
170 newpath
= getFullPath(directory
, path
)
171 if os
.path
.isfile(newpath
):
174 # At this point we have to fail if a directory was given (to prevent cases
175 # like './gdb' from matching '/usr/bin/./gdb').
176 if not os
.path
.dirname(path
):
177 for dir in os
.environ
['PATH'].split(os
.pathsep
):
178 newpath
= os
.path
.join(dir, path
)
179 if os
.path
.isfile(newpath
):
183 def getDebuggerInfo(directory
, debugger
, debuggerArgs
, debuggerInteractive
= False):
188 debuggerPath
= searchPath(directory
, debugger
)
190 print "Error: Path %s doesn't exist." % debugger
193 debuggerName
= os
.path
.basename(debuggerPath
).lower()
195 def getDebuggerInfo(type, default
):
196 if debuggerName
in DEBUGGER_INFO
and type in DEBUGGER_INFO
[debuggerName
]:
197 return DEBUGGER_INFO
[debuggerName
][type]
201 "path": debuggerPath
,
202 "interactive" : getDebuggerInfo("interactive", False),
203 "args": getDebuggerInfo("args", "").split()
207 debuggerInfo
["args"] = debuggerArgs
.split()
208 if debuggerInteractive
:
209 debuggerInfo
["interactive"] = debuggerInteractive
214 def dumpLeakLog(leakLogFile
, filter = False):
215 """Process the leak log, without parsing it.
217 Use this function if you want the raw log only.
218 Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
221 # Don't warn (nor "info") if the log file is not there.
222 if not os
.path
.exists(leakLogFile
):
225 leaks
= open(leakLogFile
, "r")
226 leakReport
= leaks
.read()
229 # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
230 # Only check whether an actual leak was reported.
231 if filter and not "0 TOTAL " in leakReport
:
234 # Simply copy the log.
235 log
.info(leakReport
.rstrip("\n"))
237 def processSingleLeakFile(leakLogFileName
, PID
, processType
, leakThreshold
):
238 """Process a single leak log, corresponding to the specified
239 process PID and type.
242 # Per-Inst Leaked Total Rem ...
243 # 0 TOTAL 17 192 419115886 2 ...
244 # 833 nsTimerImpl 60 120 24726 2 ...
245 lineRe
= re
.compile(r
"^\s*\d+\s+(?P<name>\S+)\s+"
246 r
"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
247 r
"-?\d+\s+(?P<numLeaked>-?\d+)")
250 if PID
and processType
:
251 processString
= "| %s process %s " % (processType
, PID
)
252 leaks
= open(leakLogFileName
, "r")
254 matches
= lineRe
.match(line
)
256 int(matches
.group("numLeaked")) == 0 and
257 matches
.group("name") != "TOTAL"):
259 log
.info(line
.rstrip())
262 leaks
= open(leakLogFileName
, "r")
264 crashedOnPurpose
= False
268 if line
.find("purposefully crash") > -1:
269 crashedOnPurpose
= True
270 matches
= lineRe
.match(line
)
273 name
= matches
.group("name")
274 size
= int(matches
.group("size"))
275 bytesLeaked
= int(matches
.group("bytesLeaked"))
276 numLeaked
= int(matches
.group("numLeaked"))
277 if size
< 0 or bytesLeaked
< 0 or numLeaked
< 0:
278 log
.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | negative leaks caught!" %
282 elif name
== "TOTAL":
285 if bytesLeaked
< 0 or bytesLeaked
> leakThreshold
:
286 prefix
= "TEST-UNEXPECTED-FAIL"
287 leakLog
= "TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | leaked" \
288 " %d bytes during test execution" % (processString
, bytesLeaked
)
289 elif bytesLeaked
> 0:
290 leakLog
= "TEST-PASS %s| automationutils.processLeakLog() | WARNING leaked" \
291 " %d bytes during test execution" % (processString
, bytesLeaked
)
293 leakLog
= "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
295 # Remind the threshold if it is not 0, which is the default/goal.
296 if leakThreshold
!= 0:
297 leakLog
+= " (threshold set at %d bytes)" % leakThreshold
298 # Log the information.
303 instance
= "instances"
304 rest
= " each (%s bytes total)" % matches
.group("bytesLeaked")
306 instance
= "instance"
310 # don't spam brief tinderbox logs with tons of leak output
312 log
.info("%(prefix)s %(process)s| automationutils.processLeakLog() | leaked %(numLeaked)d %(instance)s of %(name)s "
313 "with size %(size)s bytes%(rest)s" %
315 "process": processString
,
316 "numLeaked": numLeaked
,
317 "instance": instance
,
319 "size": matches
.group("size"),
323 log
.info("INFO | automationutils.processLeakLog() | process %s was " \
324 "deliberately crashed and thus has no leak log" % PID
)
326 log
.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | missing output line for total leaks!" %
331 def processLeakLog(leakLogFile
, leakThreshold
= 0):
332 """Process the leak log, including separate leak logs created
335 Use this function if you want an additional PASS/FAIL summary.
336 It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
339 if not os
.path
.exists(leakLogFile
):
340 log
.info("WARNING | automationutils.processLeakLog() | refcount logging is off, so leaks can't be detected!")
343 (leakLogFileDir
, leakFileBase
) = os
.path
.split(leakLogFile
)
344 pidRegExp
= re
.compile(r
".*?_([a-z]*)_pid(\d*)$")
345 if leakFileBase
[-4:] == ".log":
346 leakFileBase
= leakFileBase
[:-4]
347 pidRegExp
= re
.compile(r
".*?_([a-z]*)_pid(\d*).log$")
349 for fileName
in os
.listdir(leakLogFileDir
):
350 if fileName
.find(leakFileBase
) != -1:
351 thisFile
= os
.path
.join(leakLogFileDir
, fileName
)
354 m
= pidRegExp
.search(fileName
)
356 processType
= m
.group(1)
357 processPID
= m
.group(2)
358 processSingleLeakFile(thisFile
, processPID
, processType
, leakThreshold
)
360 def replaceBackSlashes(input):
361 return input.replace('\\', '/')