Bug 550643 - Minor improvements to parsemark.py. r=jorendorff.
[mozilla-central.git] / build / automationutils.py
blobd2ad9bea0cdfffe418c1d8ad904b0db7616fac50
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
42 __all__ = [
43 "addCommonOptions",
44 "checkForCrashes",
45 "dumpLeakLog",
46 "processLeakLog",
47 "getDebuggerInfo",
48 "DEBUGGER_INFO",
51 # Map of debugging programs to information about them, like default arguments
52 # and whether or not they are interactive.
53 DEBUGGER_INFO = {
54 # gdb requires that you supply the '--args' flag in order to pass arguments
55 # after the executable name to the executable.
56 "gdb": {
57 "interactive": True,
58 "args": "-q --args"
61 # valgrind doesn't explain much about leaks unless you set the
62 # '--leak-check=full' flag.
63 "valgrind": {
64 "interactive": False,
65 "args": "--leak-check=full"
69 log = logging.getLogger()
71 def addCommonOptions(parser, defaults={}):
72 parser.add_option("--xre-path",
73 action = "store", type = "string", dest = "xrePath",
74 # individual scripts will set a sane default
75 default = None,
76 help = "absolute path to directory containing XRE (probably xulrunner)")
77 if 'SYMBOLS_PATH' not in defaults:
78 defaults['SYMBOLS_PATH'] = None
79 parser.add_option("--symbols-path",
80 action = "store", type = "string", dest = "symbolsPath",
81 default = defaults['SYMBOLS_PATH'],
82 help = "absolute path to directory containing breakpad symbols")
83 parser.add_option("--debugger",
84 action = "store", dest = "debugger",
85 help = "use the given debugger to launch the application")
86 parser.add_option("--debugger-args",
87 action = "store", dest = "debuggerArgs",
88 help = "pass the given args to the debugger _before_ "
89 "the application on the command line")
90 parser.add_option("--debugger-interactive",
91 action = "store_true", dest = "debuggerInteractive",
92 help = "prevents the test harness from redirecting "
93 "stdout and stderr for interactive debuggers")
95 def checkForCrashes(dumpDir, symbolsPath, testName=None):
96 stackwalkPath = os.environ.get('MINIDUMP_STACKWALK', None)
97 # try to get the caller's filename if no test name is given
98 if testName is None:
99 try:
100 testName = os.path.basename(sys._getframe(1).f_code.co_filename)
101 except:
102 testName = "unknown"
104 foundCrash = False
105 dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
106 for d in dumps:
107 log.info("PROCESS-CRASH | %s | application crashed (minidump found)", testName)
108 if symbolsPath and stackwalkPath and os.path.exists(stackwalkPath):
109 nullfd = open(os.devnull, 'w')
110 # eat minidump_stackwalk errors
111 subprocess.call([stackwalkPath, d, symbolsPath], stderr=nullfd)
112 nullfd.close()
113 else:
114 if not symbolsPath:
115 print "No symbols path given, can't process dump."
116 if not stackwalkPath:
117 print "MINIDUMP_STACKWALK not set, can't process dump."
118 else:
119 if not os.path.exists(stackwalkPath):
120 print "MINIDUMP_STACKWALK binary not found: %s" % stackwalkPath
121 dumpSavePath = os.environ.get('MINIDUMP_SAVE_PATH', None)
122 if dumpSavePath:
123 shutil.move(d, dumpSavePath)
124 print "Saved dump as %s" % os.path.join(dumpSavePath,
125 os.path.basename(d))
126 else:
127 os.remove(d)
128 extra = os.path.splitext(d)[0] + ".extra"
129 if os.path.exists(extra):
130 os.remove(extra)
131 foundCrash = True
133 return foundCrash
135 def getFullPath(directory, path):
136 "Get an absolute path relative to 'directory'."
137 return os.path.normpath(os.path.join(directory, os.path.expanduser(path)))
139 def searchPath(directory, path):
140 "Go one step beyond getFullPath and try the various folders in PATH"
141 # Try looking in the current working directory first.
142 newpath = getFullPath(directory, path)
143 if os.path.isfile(newpath):
144 return newpath
146 # At this point we have to fail if a directory was given (to prevent cases
147 # like './gdb' from matching '/usr/bin/./gdb').
148 if not os.path.dirname(path):
149 for dir in os.environ['PATH'].split(os.pathsep):
150 newpath = os.path.join(dir, path)
151 if os.path.isfile(newpath):
152 return newpath
153 return None
155 def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
157 debuggerInfo = None
159 if debugger:
160 debuggerPath = searchPath(directory, debugger)
161 if not debuggerPath:
162 print "Error: Path %s doesn't exist." % debugger
163 sys.exit(1)
165 debuggerName = os.path.basename(debuggerPath).lower()
167 def getDebuggerInfo(type, default):
168 if debuggerName in DEBUGGER_INFO and type in DEBUGGER_INFO[debuggerName]:
169 return DEBUGGER_INFO[debuggerName][type]
170 return default
172 debuggerInfo = {
173 "path": debuggerPath,
174 "interactive" : getDebuggerInfo("interactive", False),
175 "args": getDebuggerInfo("args", "").split()
178 if debuggerArgs:
179 debuggerInfo["args"] = debuggerArgs.split()
180 if debuggerInteractive:
181 debuggerInfo["interactive"] = debuggerInteractive
183 return debuggerInfo
186 def dumpLeakLog(leakLogFile, filter = False):
187 """Process the leak log, without parsing it.
189 Use this function if you want the raw log only.
190 Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
193 # Don't warn (nor "info") if the log file is not there.
194 if not os.path.exists(leakLogFile):
195 return
197 leaks = open(leakLogFile, "r")
198 leakReport = leaks.read()
199 leaks.close()
201 # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
202 # Only check whether an actual leak was reported.
203 if filter and not "0 TOTAL " in leakReport:
204 return
206 # Simply copy the log.
207 log.info(leakReport.rstrip("\n"))
209 def processSingleLeakFile(leakLogFileName, PID, processType, leakThreshold):
210 """Process a single leak log, corresponding to the specified
211 process PID and type.
214 # Per-Inst Leaked Total Rem ...
215 # 0 TOTAL 17 192 419115886 2 ...
216 # 833 nsTimerImpl 60 120 24726 2 ...
217 lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+"
218 r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
219 r"-?\d+\s+(?P<numLeaked>-?\d+)")
221 processString = ""
222 if PID and processType:
223 processString = "| %s process %s " % (processType, PID)
224 leaks = open(leakLogFileName, "r")
225 for line in leaks:
226 matches = lineRe.match(line)
227 if (matches and
228 int(matches.group("numLeaked")) == 0 and
229 matches.group("name") != "TOTAL"):
230 continue
231 log.info(line.rstrip())
232 leaks.close()
234 leaks = open(leakLogFileName, "r")
235 seenTotal = False
236 crashedOnPurpose = False
237 prefix = "TEST-PASS"
238 for line in leaks:
239 if line.find("purposefully crash") > -1:
240 crashedOnPurpose = True
241 matches = lineRe.match(line)
242 if not matches:
243 continue
244 name = matches.group("name")
245 size = int(matches.group("size"))
246 bytesLeaked = int(matches.group("bytesLeaked"))
247 numLeaked = int(matches.group("numLeaked"))
248 if size < 0 or bytesLeaked < 0 or numLeaked < 0:
249 log.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | negative leaks caught!" %
250 processString)
251 if name == "TOTAL":
252 seenTotal = True
253 elif name == "TOTAL":
254 seenTotal = True
255 # Check for leaks.
256 if bytesLeaked < 0 or bytesLeaked > leakThreshold:
257 prefix = "TEST-UNEXPECTED-FAIL"
258 leakLog = "TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | leaked" \
259 " %d bytes during test execution" % (processString, bytesLeaked)
260 elif bytesLeaked > 0:
261 leakLog = "TEST-PASS %s| automationutils.processLeakLog() | WARNING leaked" \
262 " %d bytes during test execution" % (processString, bytesLeaked)
263 else:
264 leakLog = "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
265 % processString
266 # Remind the threshold if it is not 0, which is the default/goal.
267 if leakThreshold != 0:
268 leakLog += " (threshold set at %d bytes)" % leakThreshold
269 # Log the information.
270 log.info(leakLog)
271 else:
272 if numLeaked != 0:
273 if numLeaked > 1:
274 instance = "instances"
275 rest = " each (%s bytes total)" % matches.group("bytesLeaked")
276 else:
277 instance = "instance"
278 rest = ""
279 log.info("%(prefix)s %(process)s| automationutils.processLeakLog() | leaked %(numLeaked)d %(instance)s of %(name)s "
280 "with size %(size)s bytes%(rest)s" %
281 { "prefix": prefix,
282 "process": processString,
283 "numLeaked": numLeaked,
284 "instance": instance,
285 "name": name,
286 "size": matches.group("size"),
287 "rest": rest })
288 if not seenTotal:
289 if crashedOnPurpose:
290 log.info("INFO | automationutils.processLeakLog() | process %s was " \
291 "deliberately crashed and thus has no leak log" % PID)
292 else:
293 log.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | missing output line for total leaks!" %
294 processString)
295 leaks.close()
298 def processLeakLog(leakLogFile, leakThreshold = 0):
299 """Process the leak log, including separate leak logs created
300 by child processes.
302 Use this function if you want an additional PASS/FAIL summary.
303 It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
306 if not os.path.exists(leakLogFile):
307 log.info("WARNING | automationutils.processLeakLog() | refcount logging is off, so leaks can't be detected!")
308 return
310 (leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
311 pidRegExp = re.compile(r".*?_([a-z]*)_pid(\d*)$")
312 if leakFileBase[-4:] == ".log":
313 leakFileBase = leakFileBase[:-4]
314 pidRegExp = re.compile(r".*?_([a-z]*)_pid(\d*).log$")
316 for fileName in os.listdir(leakLogFileDir):
317 if fileName.find(leakFileBase) != -1:
318 thisFile = os.path.join(leakLogFileDir, fileName)
319 processPID = 0
320 processType = None
321 m = pidRegExp.search(fileName)
322 if m:
323 processType = m.group(1)
324 processPID = m.group(2)
325 processSingleLeakFile(thisFile, processPID, processType, leakThreshold)