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
, platform
, shutil
, subprocess
, sys
41 from urlparse
import urlparse
55 # Map of debugging programs to information about them, like default arguments
56 # and whether or not they are interactive.
58 # gdb requires that you supply the '--args' flag in order to pass arguments
59 # after the executable name to the executable.
65 # valgrind doesn't explain much about leaks unless you set the
66 # '--leak-check=full' flag.
69 "args": "--leak-check=full"
73 log
= logging
.getLogger()
76 """Return True if |thing| looks like a URL."""
77 return urlparse(thing
).scheme
!= ''
79 def addCommonOptions(parser
, defaults
={}):
80 parser
.add_option("--xre-path",
81 action
= "store", type = "string", dest
= "xrePath",
82 # individual scripts will set a sane default
84 help = "absolute path to directory containing XRE (probably xulrunner)")
85 if 'SYMBOLS_PATH' not in defaults
:
86 defaults
['SYMBOLS_PATH'] = None
87 parser
.add_option("--symbols-path",
88 action
= "store", type = "string", dest
= "symbolsPath",
89 default
= defaults
['SYMBOLS_PATH'],
90 help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
91 parser
.add_option("--debugger",
92 action
= "store", dest
= "debugger",
93 help = "use the given debugger to launch the application")
94 parser
.add_option("--debugger-args",
95 action
= "store", dest
= "debuggerArgs",
96 help = "pass the given args to the debugger _before_ "
97 "the application on the command line")
98 parser
.add_option("--debugger-interactive",
99 action
= "store_true", dest
= "debuggerInteractive",
100 help = "prevents the test harness from redirecting "
101 "stdout and stderr for interactive debuggers")
103 def checkForCrashes(dumpDir
, symbolsPath
, testName
=None):
104 stackwalkPath
= os
.environ
.get('MINIDUMP_STACKWALK', None)
105 stackwalkCGI
= os
.environ
.get('MINIDUMP_STACKWALK_CGI', None)
106 # try to get the caller's filename if no test name is given
109 testName
= os
.path
.basename(sys
._getframe
(1).f_code
.co_filename
)
114 dumps
= glob
.glob(os
.path
.join(dumpDir
, '*.dmp'))
116 log
.info("PROCESS-CRASH | %s | application crashed (minidump found)", testName
)
117 if symbolsPath
and stackwalkPath
and os
.path
.exists(stackwalkPath
):
118 p
= subprocess
.Popen([stackwalkPath
, d
, symbolsPath
],
119 stdout
=subprocess
.PIPE
,
120 stderr
=subprocess
.PIPE
)
121 (out
, err
) = p
.communicate()
123 # minidump_stackwalk is chatty, so ignore stderr when it succeeds.
126 print "stderr from minidump_stackwalk:"
128 if p
.returncode
!= 0:
129 print "minidump_stackwalk exited with return code %d" % p
.returncode
130 elif stackwalkCGI
and symbolsPath
and isURL(symbolsPath
):
134 sys
.path
.append(os
.path
.join(os
.path
.dirname(__file__
), "poster.zip"))
135 from poster
.encode
import multipart_encode
136 from poster
.streaminghttp
import register_openers
139 datagen
, headers
= multipart_encode({"minidump": f
,
140 "symbols": symbolsPath
})
141 request
= urllib2
.Request(stackwalkCGI
, datagen
, headers
)
142 result
= urllib2
.urlopen(request
).read()
146 print "stackwalkCGI returned nothing."
152 print "No symbols path given, can't process dump."
153 if not stackwalkPath
and not stackwalkCGI
:
154 print "Neither MINIDUMP_STACKWALK nor MINIDUMP_STACKWALK_CGI is set, can't process dump."
156 if stackwalkPath
and not os
.path
.exists(stackwalkPath
):
157 print "MINIDUMP_STACKWALK binary not found: %s" % stackwalkPath
158 elif stackwalkCGI
and not isURL(stackwalkCGI
):
159 print "MINIDUMP_STACKWALK_CGI is not a URL: %s" % stackwalkCGI
160 elif symbolsPath
and not isURL(symbolsPath
):
161 print "symbolsPath is not a URL: %s" % symbolsPath
162 dumpSavePath
= os
.environ
.get('MINIDUMP_SAVE_PATH', None)
164 shutil
.move(d
, dumpSavePath
)
165 print "Saved dump as %s" % os
.path
.join(dumpSavePath
,
169 extra
= os
.path
.splitext(d
)[0] + ".extra"
170 if os
.path
.exists(extra
):
176 def getFullPath(directory
, path
):
177 "Get an absolute path relative to 'directory'."
178 return os
.path
.normpath(os
.path
.join(directory
, os
.path
.expanduser(path
)))
180 def searchPath(directory
, path
):
181 "Go one step beyond getFullPath and try the various folders in PATH"
182 # Try looking in the current working directory first.
183 newpath
= getFullPath(directory
, path
)
184 if os
.path
.isfile(newpath
):
187 # At this point we have to fail if a directory was given (to prevent cases
188 # like './gdb' from matching '/usr/bin/./gdb').
189 if not os
.path
.dirname(path
):
190 for dir in os
.environ
['PATH'].split(os
.pathsep
):
191 newpath
= os
.path
.join(dir, path
)
192 if os
.path
.isfile(newpath
):
196 def getDebuggerInfo(directory
, debugger
, debuggerArgs
, debuggerInteractive
= False):
201 debuggerPath
= searchPath(directory
, debugger
)
203 print "Error: Path %s doesn't exist." % debugger
206 debuggerName
= os
.path
.basename(debuggerPath
).lower()
208 def getDebuggerInfo(type, default
):
209 if debuggerName
in DEBUGGER_INFO
and type in DEBUGGER_INFO
[debuggerName
]:
210 return DEBUGGER_INFO
[debuggerName
][type]
214 "path": debuggerPath
,
215 "interactive" : getDebuggerInfo("interactive", False),
216 "args": getDebuggerInfo("args", "").split()
220 debuggerInfo
["args"] = debuggerArgs
.split()
221 if debuggerInteractive
:
222 debuggerInfo
["interactive"] = debuggerInteractive
227 def dumpLeakLog(leakLogFile
, filter = False):
228 """Process the leak log, without parsing it.
230 Use this function if you want the raw log only.
231 Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
234 # Don't warn (nor "info") if the log file is not there.
235 if not os
.path
.exists(leakLogFile
):
238 leaks
= open(leakLogFile
, "r")
239 leakReport
= leaks
.read()
242 # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
243 # Only check whether an actual leak was reported.
244 if filter and not "0 TOTAL " in leakReport
:
247 # Simply copy the log.
248 log
.info(leakReport
.rstrip("\n"))
250 def processSingleLeakFile(leakLogFileName
, PID
, processType
, leakThreshold
):
251 """Process a single leak log, corresponding to the specified
252 process PID and type.
255 # Per-Inst Leaked Total Rem ...
256 # 0 TOTAL 17 192 419115886 2 ...
257 # 833 nsTimerImpl 60 120 24726 2 ...
258 lineRe
= re
.compile(r
"^\s*\d+\s+(?P<name>\S+)\s+"
259 r
"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
260 r
"-?\d+\s+(?P<numLeaked>-?\d+)")
263 if PID
and processType
:
264 processString
= "| %s process %s " % (processType
, PID
)
265 leaks
= open(leakLogFileName
, "r")
267 matches
= lineRe
.match(line
)
269 int(matches
.group("numLeaked")) == 0 and
270 matches
.group("name") != "TOTAL"):
272 log
.info(line
.rstrip())
275 leaks
= open(leakLogFileName
, "r")
277 crashedOnPurpose
= False
281 if line
.find("purposefully crash") > -1:
282 crashedOnPurpose
= True
283 matches
= lineRe
.match(line
)
286 name
= matches
.group("name")
287 size
= int(matches
.group("size"))
288 bytesLeaked
= int(matches
.group("bytesLeaked"))
289 numLeaked
= int(matches
.group("numLeaked"))
290 if size
< 0 or bytesLeaked
< 0 or numLeaked
< 0:
291 log
.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | negative leaks caught!" %
295 elif name
== "TOTAL":
298 if bytesLeaked
< 0 or bytesLeaked
> leakThreshold
:
299 prefix
= "TEST-UNEXPECTED-FAIL"
300 leakLog
= "TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | leaked" \
301 " %d bytes during test execution" % (processString
, bytesLeaked
)
302 elif bytesLeaked
> 0:
303 leakLog
= "TEST-PASS %s| automationutils.processLeakLog() | WARNING leaked" \
304 " %d bytes during test execution" % (processString
, bytesLeaked
)
306 leakLog
= "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
308 # Remind the threshold if it is not 0, which is the default/goal.
309 if leakThreshold
!= 0:
310 leakLog
+= " (threshold set at %d bytes)" % leakThreshold
311 # Log the information.
316 instance
= "instances"
317 rest
= " each (%s bytes total)" % matches
.group("bytesLeaked")
319 instance
= "instance"
323 # don't spam brief tinderbox logs with tons of leak output
325 log
.info("%(prefix)s %(process)s| automationutils.processLeakLog() | leaked %(numLeaked)d %(instance)s of %(name)s "
326 "with size %(size)s bytes%(rest)s" %
328 "process": processString
,
329 "numLeaked": numLeaked
,
330 "instance": instance
,
332 "size": matches
.group("size"),
336 log
.info("INFO | automationutils.processLeakLog() | process %s was " \
337 "deliberately crashed and thus has no leak log" % PID
)
339 log
.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | missing output line for total leaks!" %
344 def processLeakLog(leakLogFile
, leakThreshold
= 0):
345 """Process the leak log, including separate leak logs created
348 Use this function if you want an additional PASS/FAIL summary.
349 It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
352 if not os
.path
.exists(leakLogFile
):
353 log
.info("WARNING | automationutils.processLeakLog() | refcount logging is off, so leaks can't be detected!")
356 (leakLogFileDir
, leakFileBase
) = os
.path
.split(leakLogFile
)
357 pidRegExp
= re
.compile(r
".*?_([a-z]*)_pid(\d*)$")
358 if leakFileBase
[-4:] == ".log":
359 leakFileBase
= leakFileBase
[:-4]
360 pidRegExp
= re
.compile(r
".*?_([a-z]*)_pid(\d*).log$")
362 for fileName
in os
.listdir(leakLogFileDir
):
363 if fileName
.find(leakFileBase
) != -1:
364 thisFile
= os
.path
.join(leakLogFileDir
, fileName
)
367 m
= pidRegExp
.search(fileName
)
369 processType
= m
.group(1)
370 processPID
= m
.group(2)
371 processSingleLeakFile(thisFile
, processPID
, processType
, leakThreshold
)
373 def replaceBackSlashes(input):
374 return input.replace('\\', '/')
376 def wrapCommand(cmd
):
378 If running on OS X 10.5 or older, wrap |cmd| so that it will
379 be executed as an i386 binary, in case it's a 32-bit/64-bit universal
382 if platform
.system() == "Darwin" and \
383 hasattr(platform
, 'mac_ver') and \
384 platform
.mac_ver()[0][:4] < '10.6':
385 return ["arch", "-arch", "i386"] + cmd
386 # otherwise just execute the command normally