Home | History | Annotate | Download | only in heapcheck
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Valgrind-style suppressions for heapchecker reports.
      7 
      8 Suppressions are defined as follows:
      9 
     10 # optional one-line comments anywhere in the suppressions file.
     11 {
     12   Toolname:Errortype
     13   Short description of the error.
     14   fun:function_name
     15   fun:wildcarded_fun*_name
     16   # an ellipsis wildcards zero or more functions in a stack.
     17   ...
     18   fun:some_other_function_name
     19 }
     20 
     21 Note that only a 'fun:' prefix is allowed, i.e. we can't suppress objects and
     22 source files.
     23 
     24 If ran from the command line, suppressions.py does a self-test of the
     25 Suppression class.
     26 """
     27 
     28 import re
     29 
     30 ELLIPSIS = '...'
     31 
     32 
     33 class Suppression(object):
     34   """This class represents a single stack trace suppression.
     35 
     36   Attributes:
     37     type: A string representing the error type, e.g. Heapcheck:Leak.
     38     description: A string representing the error description.
     39   """
     40 
     41   def __init__(self, kind, description, stack):
     42     """Inits Suppression.
     43 
     44     stack is a list of function names and/or wildcards.
     45 
     46     Args:
     47       kind:
     48       description: Same as class attributes.
     49       stack: A list of strings.
     50     """
     51     self.type = kind
     52     self.description = description
     53     self._stack = stack
     54     re_line = ''
     55     re_bucket = ''
     56     for line in stack:
     57       if line == ELLIPSIS:
     58         re_line += re.escape(re_bucket)
     59         re_bucket = ''
     60         re_line += '(.*\n)*'
     61       else:
     62         for char in line:
     63           if char == '*':
     64             re_line += re.escape(re_bucket)
     65             re_bucket = ''
     66             re_line += '.*'
     67           else:  # there can't be any '\*'s in a stack trace
     68             re_bucket += char
     69         re_line += re.escape(re_bucket)
     70         re_bucket = ''
     71         re_line += '\n'
     72     self._re = re.compile(re_line, re.MULTILINE)
     73 
     74   def Match(self, report):
     75     """Returns bool indicating whether the suppression matches the given report.
     76 
     77     Args:
     78       report: list of strings (function names).
     79     Returns:
     80       True if the suppression is not empty and matches the report.
     81     """
     82     if not self._stack:
     83       return False
     84     if self._re.match('\n'.join(report) + '\n'):
     85       return True
     86     else:
     87       return False
     88 
     89 
     90 class SuppressionError(Exception):
     91   def __init__(self, filename, line, report=''):
     92     Exception.__init__(self, filename, line, report)
     93     self._file = filename
     94     self._line = line
     95     self._report = report
     96 
     97   def __str__(self):
     98     return 'Error reading suppressions from "%s" (line %d): %s.' % (
     99         self._file, self._line, self._report)
    100 
    101 
    102 def ReadSuppressionsFromFile(filename):
    103   """Given a file, returns a list of suppressions."""
    104   input_file = file(filename, 'r')
    105   result = []
    106   cur_descr = ''
    107   cur_type = ''
    108   cur_stack = []
    109   nline = 0
    110   try:
    111     for line in input_file:
    112       nline += 1
    113       line = line.strip()
    114       if line.startswith('#'):
    115         continue
    116       elif line.startswith('{'):
    117         pass
    118       elif line.startswith('}'):
    119         result.append(Suppression(cur_type, cur_descr, cur_stack))
    120         cur_descr = ''
    121         cur_type = ''
    122         cur_stack = []
    123       elif not cur_descr:
    124         cur_descr = line
    125         continue
    126       elif not cur_type:
    127         cur_type = line
    128         continue
    129       elif line.startswith('fun:'):
    130         line = line[4:]
    131         cur_stack.append(line.strip())
    132       elif line.startswith(ELLIPSIS):
    133         cur_stack.append(ELLIPSIS)
    134       else:
    135         raise SuppressionError(filename, nline,
    136                                '"fun:function_name" or "..." expected')
    137   except SuppressionError:
    138     input_file.close()
    139     raise
    140   return result
    141 
    142 
    143 def MatchTest():
    144   """Tests the Suppression.Match() capabilities."""
    145 
    146   def GenSupp(*lines):
    147     return Suppression('', '', list(lines))
    148   empty = GenSupp()
    149   assert not empty.Match([])
    150   assert not empty.Match(['foo', 'bar'])
    151   asterisk = GenSupp('*bar')
    152   assert asterisk.Match(['foobar', 'foobaz'])
    153   assert not asterisk.Match(['foobaz', 'foobar'])
    154   ellipsis = GenSupp('...', 'foo')
    155   assert ellipsis.Match(['foo', 'bar'])
    156   assert ellipsis.Match(['bar', 'baz', 'foo'])
    157   assert not ellipsis.Match(['bar', 'baz', 'bah'])
    158   mixed = GenSupp('...', 'foo*', 'function')
    159   assert mixed.Match(['foobar', 'foobaz', 'function'])
    160   assert not mixed.Match(['foobar', 'blah', 'function'])
    161   at_and_dollar = GenSupp('foo@GLIBC', 'bar@NOCANCEL')
    162   assert at_and_dollar.Match(['foo@GLIBC', 'bar@NOCANCEL'])
    163   re_chars = GenSupp('.*')
    164   assert re_chars.Match(['.foobar'])
    165   assert not re_chars.Match(['foobar'])
    166   print 'PASS'
    167 
    168 
    169 if __name__ == '__main__':
    170   MatchTest()
    171