Bug 575870 - Enable the firefox button on xp themed, classic, and aero basic. r=dao...
[mozilla-central.git] / build / automationutils.py
blobeaae49a3f18a8fb62063ca5dfe257efe5d9d18ba
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, 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",
54 # Map of debugging programs to information about them, like default arguments
55 # and whether or not they are interactive.
56 DEBUGGER_INFO = {
57 # gdb requires that you supply the '--args' flag in order to pass arguments
58 # after the executable name to the executable.
59 "gdb": {
60 "interactive": True,
61 "args": "-q --args"
64 # valgrind doesn't explain much about leaks unless you set the
65 # '--leak-check=full' flag.
66 "valgrind": {
67 "interactive": False,
68 "args": "--leak-check=full"
72 log = logging.getLogger()
74 def isURL(thing):
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
82 default = None,
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
106 if testName is None:
107 try:
108 testName = os.path.basename(sys._getframe(1).f_code.co_filename)
109 except:
110 testName = "unknown"
112 foundCrash = False
113 dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
114 for d in dumps:
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)
120 nullfd.close()
121 elif stackwalkCGI and symbolsPath and isURL(symbolsPath):
122 f = None
123 try:
124 f = open(d, "rb")
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
128 import urllib2
129 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()
134 finally:
135 if f:
136 f.close()
137 else:
138 if not symbolsPath:
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."
142 else:
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)
150 if dumpSavePath:
151 shutil.move(d, dumpSavePath)
152 print "Saved dump as %s" % os.path.join(dumpSavePath,
153 os.path.basename(d))
154 else:
155 os.remove(d)
156 extra = os.path.splitext(d)[0] + ".extra"
157 if os.path.exists(extra):
158 os.remove(extra)
159 foundCrash = True
161 return foundCrash
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):
172 return 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):
180 return newpath
181 return None
183 def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
185 debuggerInfo = None
187 if debugger:
188 debuggerPath = searchPath(directory, debugger)
189 if not debuggerPath:
190 print "Error: Path %s doesn't exist." % debugger
191 sys.exit(1)
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]
198 return default
200 debuggerInfo = {
201 "path": debuggerPath,
202 "interactive" : getDebuggerInfo("interactive", False),
203 "args": getDebuggerInfo("args", "").split()
206 if debuggerArgs:
207 debuggerInfo["args"] = debuggerArgs.split()
208 if debuggerInteractive:
209 debuggerInfo["interactive"] = debuggerInteractive
211 return debuggerInfo
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):
223 return
225 leaks = open(leakLogFile, "r")
226 leakReport = leaks.read()
227 leaks.close()
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:
232 return
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+)")
249 processString = ""
250 if PID and processType:
251 processString = "| %s process %s " % (processType, PID)
252 leaks = open(leakLogFileName, "r")
253 for line in leaks:
254 matches = lineRe.match(line)
255 if (matches and
256 int(matches.group("numLeaked")) == 0 and
257 matches.group("name") != "TOTAL"):
258 continue
259 log.info(line.rstrip())
260 leaks.close()
262 leaks = open(leakLogFileName, "r")
263 seenTotal = False
264 crashedOnPurpose = False
265 prefix = "TEST-PASS"
266 numObjects = 0
267 for line in leaks:
268 if line.find("purposefully crash") > -1:
269 crashedOnPurpose = True
270 matches = lineRe.match(line)
271 if not matches:
272 continue
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!" %
279 processString)
280 if name == "TOTAL":
281 seenTotal = True
282 elif name == "TOTAL":
283 seenTotal = True
284 # Check for leaks.
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)
292 else:
293 leakLog = "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
294 % processString
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.
299 log.info(leakLog)
300 else:
301 if numLeaked != 0:
302 if numLeaked > 1:
303 instance = "instances"
304 rest = " each (%s bytes total)" % matches.group("bytesLeaked")
305 else:
306 instance = "instance"
307 rest = ""
308 numObjects += 1
309 if numObjects > 5:
310 # don't spam brief tinderbox logs with tons of leak output
311 prefix = "TEST-INFO"
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" %
314 { "prefix": prefix,
315 "process": processString,
316 "numLeaked": numLeaked,
317 "instance": instance,
318 "name": name,
319 "size": matches.group("size"),
320 "rest": rest })
321 if not seenTotal:
322 if crashedOnPurpose:
323 log.info("INFO | automationutils.processLeakLog() | process %s was " \
324 "deliberately crashed and thus has no leak log" % PID)
325 else:
326 log.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | missing output line for total leaks!" %
327 processString)
328 leaks.close()
331 def processLeakLog(leakLogFile, leakThreshold = 0):
332 """Process the leak log, including separate leak logs created
333 by child processes.
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!")
341 return
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)
352 processPID = 0
353 processType = None
354 m = pidRegExp.search(fileName)
355 if m:
356 processType = m.group(1)
357 processPID = m.group(2)
358 processSingleLeakFile(thisFile, processPID, processType, leakThreshold)
360 def replaceBackSlashes(input):
361 return input.replace('\\', '/')