Bug 627938: Fix nsGlobalChromeWindow cleanup. (r=smaug, a=jst)
[mozilla-central.git] / build / automationutils.py
blob3dd694a0acb346f48099fe6d04038fc4a6db0596
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
13 # License.
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.
21 # Contributor(s):
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
40 import re
41 from urlparse import urlparse
43 __all__ = [
44 "addCommonOptions",
45 "checkForCrashes",
46 "dumpLeakLog",
47 "isURL",
48 "processLeakLog",
49 "getDebuggerInfo",
50 "DEBUGGER_INFO",
51 "replaceBackSlashes",
52 "wrapCommand",
55 # Map of debugging programs to information about them, like default arguments
56 # and whether or not they are interactive.
57 DEBUGGER_INFO = {
58 # gdb requires that you supply the '--args' flag in order to pass arguments
59 # after the executable name to the executable.
60 "gdb": {
61 "interactive": True,
62 "args": "-q --args"
65 # valgrind doesn't explain much about leaks unless you set the
66 # '--leak-check=full' flag.
67 "valgrind": {
68 "interactive": False,
69 "args": "--leak-check=full"
73 log = logging.getLogger()
75 def isURL(thing):
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
83 default = None,
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
107 if testName is None:
108 try:
109 testName = os.path.basename(sys._getframe(1).f_code.co_filename)
110 except:
111 testName = "unknown"
113 foundCrash = False
114 dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
115 for d in dumps:
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()
122 if len(out) > 3:
123 # minidump_stackwalk is chatty, so ignore stderr when it succeeds.
124 print out
125 else:
126 print "stderr from minidump_stackwalk:"
127 print err
128 if p.returncode != 0:
129 print "minidump_stackwalk exited with return code %d" % p.returncode
130 elif stackwalkCGI and symbolsPath and isURL(symbolsPath):
131 f = None
132 try:
133 f = open(d, "rb")
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
137 import urllib2
138 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()
143 if len(result) > 3:
144 print result
145 else:
146 print "stackwalkCGI returned nothing."
147 finally:
148 if f:
149 f.close()
150 else:
151 if not symbolsPath:
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."
155 else:
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)
163 if dumpSavePath:
164 shutil.move(d, dumpSavePath)
165 print "Saved dump as %s" % os.path.join(dumpSavePath,
166 os.path.basename(d))
167 else:
168 os.remove(d)
169 extra = os.path.splitext(d)[0] + ".extra"
170 if os.path.exists(extra):
171 os.remove(extra)
172 foundCrash = True
174 return foundCrash
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):
185 return 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):
193 return newpath
194 return None
196 def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
198 debuggerInfo = None
200 if debugger:
201 debuggerPath = searchPath(directory, debugger)
202 if not debuggerPath:
203 print "Error: Path %s doesn't exist." % debugger
204 sys.exit(1)
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]
211 return default
213 debuggerInfo = {
214 "path": debuggerPath,
215 "interactive" : getDebuggerInfo("interactive", False),
216 "args": getDebuggerInfo("args", "").split()
219 if debuggerArgs:
220 debuggerInfo["args"] = debuggerArgs.split()
221 if debuggerInteractive:
222 debuggerInfo["interactive"] = debuggerInteractive
224 return debuggerInfo
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):
236 return
238 leaks = open(leakLogFile, "r")
239 leakReport = leaks.read()
240 leaks.close()
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:
245 return
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+)")
262 processString = ""
263 if PID and processType:
264 processString = "| %s process %s " % (processType, PID)
265 leaks = open(leakLogFileName, "r")
266 for line in leaks:
267 matches = lineRe.match(line)
268 if (matches and
269 int(matches.group("numLeaked")) == 0 and
270 matches.group("name") != "TOTAL"):
271 continue
272 log.info(line.rstrip())
273 leaks.close()
275 leaks = open(leakLogFileName, "r")
276 seenTotal = False
277 crashedOnPurpose = False
278 prefix = "TEST-PASS"
279 numObjects = 0
280 for line in leaks:
281 if line.find("purposefully crash") > -1:
282 crashedOnPurpose = True
283 matches = lineRe.match(line)
284 if not matches:
285 continue
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!" %
292 processString)
293 if name == "TOTAL":
294 seenTotal = True
295 elif name == "TOTAL":
296 seenTotal = True
297 # Check for leaks.
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)
305 else:
306 leakLog = "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
307 % processString
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.
312 log.info(leakLog)
313 else:
314 if numLeaked != 0:
315 if numLeaked > 1:
316 instance = "instances"
317 rest = " each (%s bytes total)" % matches.group("bytesLeaked")
318 else:
319 instance = "instance"
320 rest = ""
321 numObjects += 1
322 if numObjects > 5:
323 # don't spam brief tinderbox logs with tons of leak output
324 prefix = "TEST-INFO"
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" %
327 { "prefix": prefix,
328 "process": processString,
329 "numLeaked": numLeaked,
330 "instance": instance,
331 "name": name,
332 "size": matches.group("size"),
333 "rest": rest })
334 if not seenTotal:
335 if crashedOnPurpose:
336 log.info("INFO | automationutils.processLeakLog() | process %s was " \
337 "deliberately crashed and thus has no leak log" % PID)
338 else:
339 log.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | missing output line for total leaks!" %
340 processString)
341 leaks.close()
344 def processLeakLog(leakLogFile, leakThreshold = 0):
345 """Process the leak log, including separate leak logs created
346 by child processes.
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!")
354 return
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)
365 processPID = 0
366 processType = None
367 m = pidRegExp.search(fileName)
368 if m:
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
380 binary.
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
387 return cmd