Bug 845676 - Remove most of the assertion annotations in the content/media mochitests...
[gecko.git] / build / automationutils.py
blobd108792ac5690329bd5138098fd6b56723d67b05
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 from __future__ import with_statement
7 import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile
8 import re
9 from urlparse import urlparse
11 __all__ = [
12 "ZipFileReader",
13 "addCommonOptions",
14 "checkForCrashes",
15 "dumpLeakLog",
16 "isURL",
17 "processLeakLog",
18 "getDebuggerInfo",
19 "DEBUGGER_INFO",
20 "replaceBackSlashes",
21 "wrapCommand",
24 # Map of debugging programs to information about them, like default arguments
25 # and whether or not they are interactive.
26 DEBUGGER_INFO = {
27 # gdb requires that you supply the '--args' flag in order to pass arguments
28 # after the executable name to the executable.
29 "gdb": {
30 "interactive": True,
31 "args": "-q --args"
34 # valgrind doesn't explain much about leaks unless you set the
35 # '--leak-check=full' flag.
36 "valgrind": {
37 "interactive": False,
38 "args": "--leak-check=full"
42 class ZipFileReader(object):
43 """
44 Class to read zip files in Python 2.5 and later. Limited to only what we
45 actually use.
46 """
48 def __init__(self, filename):
49 self._zipfile = zipfile.ZipFile(filename, "r")
51 def __del__(self):
52 self._zipfile.close()
54 def _getnormalizedpath(self, path):
55 """
56 Gets a normalized path from 'path' (or the current working directory if
57 'path' is None). Also asserts that the path exists.
58 """
59 if path is None:
60 path = os.curdir
61 path = os.path.normpath(os.path.expanduser(path))
62 assert os.path.isdir(path)
63 return path
65 def _extractname(self, name, path):
66 """
67 Extracts a file with the given name from the zip file to the given path.
68 Also creates any directories needed along the way.
69 """
70 filename = os.path.normpath(os.path.join(path, name))
71 if name.endswith("/"):
72 os.makedirs(filename)
73 else:
74 path = os.path.split(filename)[0]
75 if not os.path.isdir(path):
76 os.makedirs(path)
77 with open(filename, "wb") as dest:
78 dest.write(self._zipfile.read(name))
80 def namelist(self):
81 return self._zipfile.namelist()
83 def read(self, name):
84 return self._zipfile.read(name)
86 def extract(self, name, path = None):
87 if hasattr(self._zipfile, "extract"):
88 return self._zipfile.extract(name, path)
90 # This will throw if name is not part of the zip file.
91 self._zipfile.getinfo(name)
93 self._extractname(name, self._getnormalizedpath(path))
95 def extractall(self, path = None):
96 if hasattr(self._zipfile, "extractall"):
97 return self._zipfile.extractall(path)
99 path = self._getnormalizedpath(path)
101 for name in self._zipfile.namelist():
102 self._extractname(name, path)
104 log = logging.getLogger()
106 def isURL(thing):
107 """Return True if |thing| looks like a URL."""
108 # We want to download URLs like http://... but not Windows paths like c:\...
109 return len(urlparse(thing).scheme) >= 2
111 def addCommonOptions(parser, defaults={}):
112 parser.add_option("--xre-path",
113 action = "store", type = "string", dest = "xrePath",
114 # individual scripts will set a sane default
115 default = None,
116 help = "absolute path to directory containing XRE (probably xulrunner)")
117 if 'SYMBOLS_PATH' not in defaults:
118 defaults['SYMBOLS_PATH'] = None
119 parser.add_option("--symbols-path",
120 action = "store", type = "string", dest = "symbolsPath",
121 default = defaults['SYMBOLS_PATH'],
122 help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
123 parser.add_option("--debugger",
124 action = "store", dest = "debugger",
125 help = "use the given debugger to launch the application")
126 parser.add_option("--debugger-args",
127 action = "store", dest = "debuggerArgs",
128 help = "pass the given args to the debugger _before_ "
129 "the application on the command line")
130 parser.add_option("--debugger-interactive",
131 action = "store_true", dest = "debuggerInteractive",
132 help = "prevents the test harness from redirecting "
133 "stdout and stderr for interactive debuggers")
135 def checkForCrashes(dumpDir, symbolsPath, testName=None):
136 stackwalkPath = os.environ.get('MINIDUMP_STACKWALK', None)
137 # try to get the caller's filename if no test name is given
138 if testName is None:
139 try:
140 testName = os.path.basename(sys._getframe(1).f_code.co_filename)
141 except:
142 testName = "unknown"
144 # Check preconditions
145 dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
146 if len(dumps) == 0:
147 return False
149 try:
150 removeSymbolsPath = False
152 # If our symbols are at a remote URL, download them now
153 if symbolsPath and isURL(symbolsPath):
154 print "Downloading symbols from: " + symbolsPath
155 removeSymbolsPath = True
156 # Get the symbols and write them to a temporary zipfile
157 data = urllib2.urlopen(symbolsPath)
158 symbolsFile = tempfile.TemporaryFile()
159 symbolsFile.write(data.read())
160 # extract symbols to a temporary directory (which we'll delete after
161 # processing all crashes)
162 symbolsPath = tempfile.mkdtemp()
163 zfile = ZipFileReader(symbolsFile)
164 zfile.extractall(symbolsPath)
166 for d in dumps:
167 stackwalkOutput = []
168 stackwalkOutput.append("Crash dump filename: " + d)
169 topFrame = None
170 if symbolsPath and stackwalkPath and os.path.exists(stackwalkPath):
171 # run minidump stackwalk
172 p = subprocess.Popen([stackwalkPath, d, symbolsPath],
173 stdout=subprocess.PIPE,
174 stderr=subprocess.PIPE)
175 (out, err) = p.communicate()
176 if len(out) > 3:
177 # minidump_stackwalk is chatty, so ignore stderr when it succeeds.
178 stackwalkOutput.append(out)
179 # The top frame of the crash is always the line after "Thread N (crashed)"
180 # Examples:
181 # 0 libc.so + 0xa888
182 # 0 libnss3.so!nssCertificate_Destroy [certificate.c : 102 + 0x0]
183 # 0 mozjs.dll!js::GlobalObject::getDebuggers() [GlobalObject.cpp:89df18f9b6da : 580 + 0x0]
184 # 0 libxul.so!void js::gc::MarkInternal<JSObject>(JSTracer*, JSObject**) [Marking.cpp : 92 + 0x28]
185 lines = out.splitlines()
186 for i, line in enumerate(lines):
187 if "(crashed)" in line:
188 match = re.search(r"^ 0 (?:.*!)?(?:void )?([^\[]+)", lines[i+1])
189 if match:
190 topFrame = "@ %s" % match.group(1).strip()
191 break
192 else:
193 stackwalkOutput.append("stderr from minidump_stackwalk:")
194 stackwalkOutput.append(err)
195 if p.returncode != 0:
196 stackwalkOutput.append("minidump_stackwalk exited with return code %d" % p.returncode)
197 else:
198 if not symbolsPath:
199 stackwalkOutput.append("No symbols path given, can't process dump.")
200 if not stackwalkPath:
201 stackwalkOutput.append("MINIDUMP_STACKWALK not set, can't process dump.")
202 elif stackwalkPath and not os.path.exists(stackwalkPath):
203 stackwalkOutput.append("MINIDUMP_STACKWALK binary not found: %s" % stackwalkPath)
204 if not topFrame:
205 topFrame = "Unknown top frame"
206 log.info("PROCESS-CRASH | %s | application crashed [%s]", testName, topFrame)
207 print '\n'.join(stackwalkOutput)
208 dumpSavePath = os.environ.get('MINIDUMP_SAVE_PATH', None)
209 if dumpSavePath:
210 shutil.move(d, dumpSavePath)
211 print "Saved dump as %s" % os.path.join(dumpSavePath,
212 os.path.basename(d))
213 else:
214 os.remove(d)
215 extra = os.path.splitext(d)[0] + ".extra"
216 if os.path.exists(extra):
217 os.remove(extra)
218 finally:
219 if removeSymbolsPath:
220 shutil.rmtree(symbolsPath)
222 return True
224 def getFullPath(directory, path):
225 "Get an absolute path relative to 'directory'."
226 return os.path.normpath(os.path.join(directory, os.path.expanduser(path)))
228 def searchPath(directory, path):
229 "Go one step beyond getFullPath and try the various folders in PATH"
230 # Try looking in the current working directory first.
231 newpath = getFullPath(directory, path)
232 if os.path.isfile(newpath):
233 return newpath
235 # At this point we have to fail if a directory was given (to prevent cases
236 # like './gdb' from matching '/usr/bin/./gdb').
237 if not os.path.dirname(path):
238 for dir in os.environ['PATH'].split(os.pathsep):
239 newpath = os.path.join(dir, path)
240 if os.path.isfile(newpath):
241 return newpath
242 return None
244 def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
246 debuggerInfo = None
248 if debugger:
249 debuggerPath = searchPath(directory, debugger)
250 if not debuggerPath:
251 print "Error: Path %s doesn't exist." % debugger
252 sys.exit(1)
254 debuggerName = os.path.basename(debuggerPath).lower()
256 def getDebuggerInfo(type, default):
257 if debuggerName in DEBUGGER_INFO and type in DEBUGGER_INFO[debuggerName]:
258 return DEBUGGER_INFO[debuggerName][type]
259 return default
261 debuggerInfo = {
262 "path": debuggerPath,
263 "interactive" : getDebuggerInfo("interactive", False),
264 "args": getDebuggerInfo("args", "").split()
267 if debuggerArgs:
268 debuggerInfo["args"] = debuggerArgs.split()
269 if debuggerInteractive:
270 debuggerInfo["interactive"] = debuggerInteractive
272 return debuggerInfo
275 def dumpLeakLog(leakLogFile, filter = False):
276 """Process the leak log, without parsing it.
278 Use this function if you want the raw log only.
279 Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
282 # Don't warn (nor "info") if the log file is not there.
283 if not os.path.exists(leakLogFile):
284 return
286 leaks = open(leakLogFile, "r")
287 leakReport = leaks.read()
288 leaks.close()
290 # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
291 # Only check whether an actual leak was reported.
292 if filter and not "0 TOTAL " in leakReport:
293 return
295 # Simply copy the log.
296 log.info(leakReport.rstrip("\n"))
298 def processSingleLeakFile(leakLogFileName, PID, processType, leakThreshold):
299 """Process a single leak log, corresponding to the specified
300 process PID and type.
303 # Per-Inst Leaked Total Rem ...
304 # 0 TOTAL 17 192 419115886 2 ...
305 # 833 nsTimerImpl 60 120 24726 2 ...
306 lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+"
307 r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
308 r"-?\d+\s+(?P<numLeaked>-?\d+)")
310 processString = ""
311 if PID and processType:
312 processString = "| %s process %s " % (processType, PID)
313 leaks = open(leakLogFileName, "r")
314 for line in leaks:
315 matches = lineRe.match(line)
316 if (matches and
317 int(matches.group("numLeaked")) == 0 and
318 matches.group("name") != "TOTAL"):
319 continue
320 log.info(line.rstrip())
321 leaks.close()
323 leaks = open(leakLogFileName, "r")
324 seenTotal = False
325 crashedOnPurpose = False
326 prefix = "TEST-PASS"
327 numObjects = 0
328 for line in leaks:
329 if line.find("purposefully crash") > -1:
330 crashedOnPurpose = True
331 matches = lineRe.match(line)
332 if not matches:
333 continue
334 name = matches.group("name")
335 size = int(matches.group("size"))
336 bytesLeaked = int(matches.group("bytesLeaked"))
337 numLeaked = int(matches.group("numLeaked"))
338 if size < 0 or bytesLeaked < 0 or numLeaked < 0:
339 log.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | negative leaks caught!" %
340 processString)
341 if name == "TOTAL":
342 seenTotal = True
343 elif name == "TOTAL":
344 seenTotal = True
345 # Check for leaks.
346 if bytesLeaked < 0 or bytesLeaked > leakThreshold:
347 prefix = "TEST-UNEXPECTED-FAIL"
348 leakLog = "TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | leaked" \
349 " %d bytes during test execution" % (processString, bytesLeaked)
350 elif bytesLeaked > 0:
351 leakLog = "TEST-PASS %s| automationutils.processLeakLog() | WARNING leaked" \
352 " %d bytes during test execution" % (processString, bytesLeaked)
353 else:
354 leakLog = "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
355 % processString
356 # Remind the threshold if it is not 0, which is the default/goal.
357 if leakThreshold != 0:
358 leakLog += " (threshold set at %d bytes)" % leakThreshold
359 # Log the information.
360 log.info(leakLog)
361 else:
362 if numLeaked != 0:
363 if numLeaked > 1:
364 instance = "instances"
365 rest = " each (%s bytes total)" % matches.group("bytesLeaked")
366 else:
367 instance = "instance"
368 rest = ""
369 numObjects += 1
370 if numObjects > 5:
371 # don't spam brief tinderbox logs with tons of leak output
372 prefix = "TEST-INFO"
373 log.info("%(prefix)s %(process)s| automationutils.processLeakLog() | leaked %(numLeaked)d %(instance)s of %(name)s "
374 "with size %(size)s bytes%(rest)s" %
375 { "prefix": prefix,
376 "process": processString,
377 "numLeaked": numLeaked,
378 "instance": instance,
379 "name": name,
380 "size": matches.group("size"),
381 "rest": rest })
382 if not seenTotal:
383 if crashedOnPurpose:
384 log.info("INFO | automationutils.processLeakLog() | process %s was " \
385 "deliberately crashed and thus has no leak log" % PID)
386 else:
387 log.info("WARNING | automationutils.processLeakLog() | missing output line for total leaks!")
388 leaks.close()
391 def processLeakLog(leakLogFile, leakThreshold = 0):
392 """Process the leak log, including separate leak logs created
393 by child processes.
395 Use this function if you want an additional PASS/FAIL summary.
396 It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
399 if not os.path.exists(leakLogFile):
400 log.info("WARNING | automationutils.processLeakLog() | refcount logging is off, so leaks can't be detected!")
401 return
403 (leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
404 pidRegExp = re.compile(r".*?_([a-z]*)_pid(\d*)$")
405 if leakFileBase[-4:] == ".log":
406 leakFileBase = leakFileBase[:-4]
407 pidRegExp = re.compile(r".*?_([a-z]*)_pid(\d*).log$")
409 for fileName in os.listdir(leakLogFileDir):
410 if fileName.find(leakFileBase) != -1:
411 thisFile = os.path.join(leakLogFileDir, fileName)
412 processPID = 0
413 processType = None
414 m = pidRegExp.search(fileName)
415 if m:
416 processType = m.group(1)
417 processPID = m.group(2)
418 processSingleLeakFile(thisFile, processPID, processType, leakThreshold)
420 def replaceBackSlashes(input):
421 return input.replace('\\', '/')
423 def wrapCommand(cmd):
425 If running on OS X 10.5 or older, wrap |cmd| so that it will
426 be executed as an i386 binary, in case it's a 32-bit/64-bit universal
427 binary.
429 if platform.system() == "Darwin" and \
430 hasattr(platform, 'mac_ver') and \
431 platform.mac_ver()[0][:4] < '10.6':
432 return ["arch", "-arch", "i386"] + cmd
433 # otherwise just execute the command normally
434 return cmd