~ubuntu-branches/ubuntu/quantal/enigmail/quantal-security

« back to all changes in this revision

Viewing changes to build/automationutils.py

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-u3g8nmwa0pdwagwc
Tags: 2:1.5.2-0ubuntu0.12.10.1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#
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/.
5
 
 
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
10
 
 
11
 
__all__ = [
12
 
  "ZipFileReader",
13
 
  "addCommonOptions",
14
 
  "checkForCrashes",
15
 
  "dumpLeakLog",
16
 
  "isURL",
17
 
  "processLeakLog",
18
 
  "getDebuggerInfo",
19
 
  "DEBUGGER_INFO",
20
 
  "replaceBackSlashes",
21
 
  "wrapCommand",
22
 
  ]
23
 
 
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"
32
 
  },
33
 
 
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"
39
 
  }
40
 
}
41
 
 
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
 
  """
47
 
 
48
 
  def __init__(self, filename):
49
 
    self._zipfile = zipfile.ZipFile(filename, "r")
50
 
 
51
 
  def __del__(self):
52
 
    self._zipfile.close()
53
 
 
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
64
 
 
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))
79
 
 
80
 
  def namelist(self):
81
 
    return self._zipfile.namelist()
82
 
 
83
 
  def read(self, name):
84
 
    return self._zipfile.read(name)
85
 
 
86
 
  def extract(self, name, path = None):
87
 
    if hasattr(self._zipfile, "extract"):
88
 
      return self._zipfile.extract(name, path)
89
 
 
90
 
    # This will throw if name is not part of the zip file.
91
 
    self._zipfile.getinfo(name)
92
 
 
93
 
    self._extractname(name, self._getnormalizedpath(path))
94
 
 
95
 
  def extractall(self, path = None):
96
 
    if hasattr(self._zipfile, "extractall"):
97
 
      return self._zipfile.extractall(path)
98
 
 
99
 
    path = self._getnormalizedpath(path)
100
 
 
101
 
    for name in self._zipfile.namelist():
102
 
      self._extractname(name, path)
103
 
 
104
 
log = logging.getLogger()
105
 
 
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
110
 
 
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")
134
 
 
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"
143
 
 
144
 
  # Check preconditions
145
 
  dumps = glob.glob(os.path.join(dumpDir, '*.dmp'))
146
 
  if len(dumps) == 0:
147
 
    return False
148
 
 
149
 
  foundCrash = False
150
 
  removeSymbolsPath = False
151
 
 
152
 
  # If our symbols are at a remote URL, download them now
153
 
  if 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)
165
 
 
166
 
  try:
167
 
    for d in dumps:
168
 
      log.info("PROCESS-CRASH | %s | application crashed (minidump found)", testName)
169
 
      print "Crash dump filename: " + d
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
 
          print out
179
 
        else:
180
 
          print "stderr from minidump_stackwalk:"
181
 
          print err
182
 
        if p.returncode != 0:
183
 
          print "minidump_stackwalk exited with return code %d" % p.returncode
184
 
      else:
185
 
        if not symbolsPath:
186
 
          print "No symbols path given, can't process dump."
187
 
        if not stackwalkPath:
188
 
          print "MINIDUMP_STACKWALK not set, can't process dump."
189
 
        elif stackwalkPath and not os.path.exists(stackwalkPath):
190
 
          print "MINIDUMP_STACKWALK binary not found: %s" % stackwalkPath
191
 
      dumpSavePath = os.environ.get('MINIDUMP_SAVE_PATH', None)
192
 
      if dumpSavePath:
193
 
        shutil.move(d, dumpSavePath)
194
 
        print "Saved dump as %s" % os.path.join(dumpSavePath,
195
 
                                                os.path.basename(d))
196
 
      else:
197
 
        os.remove(d)
198
 
      extra = os.path.splitext(d)[0] + ".extra"
199
 
      if os.path.exists(extra):
200
 
        os.remove(extra)
201
 
      foundCrash = True
202
 
  finally:
203
 
    if removeSymbolsPath:
204
 
      shutil.rmtree(symbolsPath)
205
 
 
206
 
  return foundCrash
207
 
 
208
 
def getFullPath(directory, path):
209
 
  "Get an absolute path relative to 'directory'."
210
 
  return os.path.normpath(os.path.join(directory, os.path.expanduser(path)))
211
 
 
212
 
def searchPath(directory, path):
213
 
  "Go one step beyond getFullPath and try the various folders in PATH"
214
 
  # Try looking in the current working directory first.
215
 
  newpath = getFullPath(directory, path)
216
 
  if os.path.isfile(newpath):
217
 
    return newpath
218
 
 
219
 
  # At this point we have to fail if a directory was given (to prevent cases
220
 
  # like './gdb' from matching '/usr/bin/./gdb').
221
 
  if not os.path.dirname(path):
222
 
    for dir in os.environ['PATH'].split(os.pathsep):
223
 
      newpath = os.path.join(dir, path)
224
 
      if os.path.isfile(newpath):
225
 
        return newpath
226
 
  return None
227
 
 
228
 
def getDebuggerInfo(directory, debugger, debuggerArgs, debuggerInteractive = False):
229
 
 
230
 
  debuggerInfo = None
231
 
 
232
 
  if debugger:
233
 
    debuggerPath = searchPath(directory, debugger)
234
 
    if not debuggerPath:
235
 
      print "Error: Path %s doesn't exist." % debugger
236
 
      sys.exit(1)
237
 
 
238
 
    debuggerName = os.path.basename(debuggerPath).lower()
239
 
 
240
 
    def getDebuggerInfo(type, default):
241
 
      if debuggerName in DEBUGGER_INFO and type in DEBUGGER_INFO[debuggerName]:
242
 
        return DEBUGGER_INFO[debuggerName][type]
243
 
      return default
244
 
 
245
 
    debuggerInfo = {
246
 
      "path": debuggerPath,
247
 
      "interactive" : getDebuggerInfo("interactive", False),
248
 
      "args": getDebuggerInfo("args", "").split()
249
 
    }
250
 
 
251
 
    if debuggerArgs:
252
 
      debuggerInfo["args"] = debuggerArgs.split()
253
 
    if debuggerInteractive:
254
 
      debuggerInfo["interactive"] = debuggerInteractive
255
 
 
256
 
  return debuggerInfo
257
 
 
258
 
 
259
 
def dumpLeakLog(leakLogFile, filter = False):
260
 
  """Process the leak log, without parsing it.
261
 
 
262
 
  Use this function if you want the raw log only.
263
 
  Use it preferably with the |XPCOM_MEM_LEAK_LOG| environment variable.
264
 
  """
265
 
 
266
 
  # Don't warn (nor "info") if the log file is not there.
267
 
  if not os.path.exists(leakLogFile):
268
 
    return
269
 
 
270
 
  leaks = open(leakLogFile, "r")
271
 
  leakReport = leaks.read()
272
 
  leaks.close()
273
 
 
274
 
  # Only |XPCOM_MEM_LEAK_LOG| reports can be actually filtered out.
275
 
  # Only check whether an actual leak was reported.
276
 
  if filter and not "0 TOTAL " in leakReport:
277
 
    return
278
 
 
279
 
  # Simply copy the log.
280
 
  log.info(leakReport.rstrip("\n"))
281
 
 
282
 
def processSingleLeakFile(leakLogFileName, PID, processType, leakThreshold):
283
 
  """Process a single leak log, corresponding to the specified
284
 
  process PID and type.
285
 
  """
286
 
 
287
 
  #                  Per-Inst  Leaked      Total  Rem ...
288
 
  #   0 TOTAL              17     192  419115886    2 ...
289
 
  # 833 nsTimerImpl        60     120      24726    2 ...
290
 
  lineRe = re.compile(r"^\s*\d+\s+(?P<name>\S+)\s+"
291
 
                      r"(?P<size>-?\d+)\s+(?P<bytesLeaked>-?\d+)\s+"
292
 
                      r"-?\d+\s+(?P<numLeaked>-?\d+)")
293
 
 
294
 
  processString = ""
295
 
  if PID and processType:
296
 
    processString = "| %s process %s " % (processType, PID)
297
 
  leaks = open(leakLogFileName, "r")
298
 
  for line in leaks:
299
 
    matches = lineRe.match(line)
300
 
    if (matches and
301
 
        int(matches.group("numLeaked")) == 0 and
302
 
        matches.group("name") != "TOTAL"):
303
 
      continue
304
 
    log.info(line.rstrip())
305
 
  leaks.close()
306
 
 
307
 
  leaks = open(leakLogFileName, "r")
308
 
  seenTotal = False
309
 
  crashedOnPurpose = False
310
 
  prefix = "TEST-PASS"
311
 
  numObjects = 0
312
 
  for line in leaks:
313
 
    if line.find("purposefully crash") > -1:
314
 
      crashedOnPurpose = True
315
 
    matches = lineRe.match(line)
316
 
    if not matches:
317
 
      continue
318
 
    name = matches.group("name")
319
 
    size = int(matches.group("size"))
320
 
    bytesLeaked = int(matches.group("bytesLeaked"))
321
 
    numLeaked = int(matches.group("numLeaked"))
322
 
    if size < 0 or bytesLeaked < 0 or numLeaked < 0:
323
 
      log.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | negative leaks caught!" %
324
 
               processString)
325
 
      if name == "TOTAL":
326
 
        seenTotal = True
327
 
    elif name == "TOTAL":
328
 
      seenTotal = True
329
 
      # Check for leaks.
330
 
      if bytesLeaked < 0 or bytesLeaked > leakThreshold:
331
 
        prefix = "TEST-UNEXPECTED-FAIL"
332
 
        leakLog = "TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | leaked" \
333
 
                  " %d bytes during test execution" % (processString, bytesLeaked)
334
 
      elif bytesLeaked > 0:
335
 
        leakLog = "TEST-PASS %s| automationutils.processLeakLog() | WARNING leaked" \
336
 
                  " %d bytes during test execution" % (processString, bytesLeaked)
337
 
      else:
338
 
        leakLog = "TEST-PASS %s| automationutils.processLeakLog() | no leaks detected!" \
339
 
                  % processString
340
 
      # Remind the threshold if it is not 0, which is the default/goal.
341
 
      if leakThreshold != 0:
342
 
        leakLog += " (threshold set at %d bytes)" % leakThreshold
343
 
      # Log the information.
344
 
      log.info(leakLog)
345
 
    else:
346
 
      if numLeaked != 0:
347
 
        if numLeaked > 1:
348
 
          instance = "instances"
349
 
          rest = " each (%s bytes total)" % matches.group("bytesLeaked")
350
 
        else:
351
 
          instance = "instance"
352
 
          rest = ""
353
 
        numObjects += 1
354
 
        if numObjects > 5:
355
 
          # don't spam brief tinderbox logs with tons of leak output
356
 
          prefix = "TEST-INFO"
357
 
        log.info("%(prefix)s %(process)s| automationutils.processLeakLog() | leaked %(numLeaked)d %(instance)s of %(name)s "
358
 
                 "with size %(size)s bytes%(rest)s" %
359
 
                 { "prefix": prefix,
360
 
                   "process": processString,
361
 
                   "numLeaked": numLeaked,
362
 
                   "instance": instance,
363
 
                   "name": name,
364
 
                   "size": matches.group("size"),
365
 
                   "rest": rest })
366
 
  if not seenTotal:
367
 
    if crashedOnPurpose:
368
 
      log.info("INFO | automationutils.processLeakLog() | process %s was " \
369
 
               "deliberately crashed and thus has no leak log" % PID)
370
 
    else:
371
 
      log.info("TEST-UNEXPECTED-FAIL %s| automationutils.processLeakLog() | missing output line for total leaks!" %
372
 
             processString)
373
 
  leaks.close()
374
 
 
375
 
 
376
 
def processLeakLog(leakLogFile, leakThreshold = 0):
377
 
  """Process the leak log, including separate leak logs created
378
 
  by child processes.
379
 
 
380
 
  Use this function if you want an additional PASS/FAIL summary.
381
 
  It must be used with the |XPCOM_MEM_BLOAT_LOG| environment variable.
382
 
  """
383
 
 
384
 
  if not os.path.exists(leakLogFile):
385
 
    log.info("WARNING | automationutils.processLeakLog() | refcount logging is off, so leaks can't be detected!")
386
 
    return
387
 
 
388
 
  (leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
389
 
  pidRegExp = re.compile(r".*?_([a-z]*)_pid(\d*)$")
390
 
  if leakFileBase[-4:] == ".log":
391
 
    leakFileBase = leakFileBase[:-4]
392
 
    pidRegExp = re.compile(r".*?_([a-z]*)_pid(\d*).log$")
393
 
 
394
 
  for fileName in os.listdir(leakLogFileDir):
395
 
    if fileName.find(leakFileBase) != -1:
396
 
      thisFile = os.path.join(leakLogFileDir, fileName)
397
 
      processPID = 0
398
 
      processType = None
399
 
      m = pidRegExp.search(fileName)
400
 
      if m:
401
 
        processType = m.group(1)
402
 
        processPID = m.group(2)
403
 
      processSingleLeakFile(thisFile, processPID, processType, leakThreshold)
404
 
 
405
 
def replaceBackSlashes(input):
406
 
  return input.replace('\\', '/')
407
 
 
408
 
def wrapCommand(cmd):
409
 
  """
410
 
  If running on OS X 10.5 or older, wrap |cmd| so that it will
411
 
  be executed as an i386 binary, in case it's a 32-bit/64-bit universal
412
 
  binary.
413
 
  """
414
 
  if platform.system() == "Darwin" and \
415
 
     hasattr(platform, 'mac_ver') and \
416
 
     platform.mac_ver()[0][:4] < '10.6':
417
 
    return ["arch", "-arch", "i386"] + cmd
418
 
  # otherwise just execute the command normally
419
 
  return cmd