summaryrefslogtreecommitdiffstats
path: root/tools/heapcheck/heapcheck_test.py
blob: 86fa3782950111ae17d282650cc27156695e22d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Wrapper for running the test under heapchecker and analyzing the output."""

import datetime
import logging
import os
import re

import common
import path_utils
import suppressions


class HeapcheckWrapper(object):
  TMP_FILE = 'heapcheck.log'
  SANITY_TEST_SUPPRESSION = "Heapcheck sanity test"
  LEAK_REPORT_RE = re.compile(
     'Leak of ([0-9]*) bytes in ([0-9]*) objects allocated from:')
  STACK_LINE_RE = re.compile('\s*@\s*(?:0x)?[0-9a-fA-F]+\s*([^\n]*)')
  BORING_CALLERS = common.BoringCallers(mangled=False, use_re_wildcards=True)

  def __init__(self, supp_files):
    self._mode = 'strict'
    self._timeout = 1800
    self._nocleanup_on_exit = False
    self._suppressions = []
    for fname in supp_files:
      self._suppressions.extend(suppressions.ReadSuppressionsFromFile(fname))
    if os.path.exists(self.TMP_FILE):
      os.remove(self.TMP_FILE)

  def PutEnvAndLog(self, env_name, env_value):
    """Sets the env var |env_name| to |env_value| and writes to logging.info.
    """
    os.putenv(env_name, env_value)
    logging.info('export %s=%s', env_name, env_value)

  def Execute(self):
    """Executes the app to be tested."""
    logging.info('starting execution...')
    proc = ['sh', path_utils.ScriptDir() + '/heapcheck_std.sh']
    proc += self._args
    self.PutEnvAndLog('G_SLICE', 'always-malloc')
    self.PutEnvAndLog('NSS_DISABLE_ARENA_FREE_LIST', '1')
    self.PutEnvAndLog('NSS_DISABLE_UNLOAD', '1')
    self.PutEnvAndLog('GTEST_DEATH_TEST_USE_FORK', '1')
    self.PutEnvAndLog('HEAPCHECK', self._mode)
    self.PutEnvAndLog('HEAP_CHECK_ERROR_EXIT_CODE', '0')
    self.PutEnvAndLog('HEAP_CHECK_MAX_LEAKS', '-1')
    self.PutEnvAndLog('KEEP_SHADOW_STACKS', '1')
    self.PutEnvAndLog('PPROF_PATH',
        path_utils.ScriptDir() +
        '/../../third_party/tcmalloc/chromium/src/pprof')
    self.PutEnvAndLog('LD_LIBRARY_PATH',
                      '/usr/lib/debug/:/usr/lib32/debug/')
    # CHROME_DEVEL_SANDBOX causes problems with heapcheck
    self.PutEnvAndLog('CHROME_DEVEL_SANDBOX', '');

    return common.RunSubprocess(proc, self._timeout)

  def Analyze(self, log_lines, check_sanity=False):
    """Analyzes the app's output and applies suppressions to the reports.

    Analyze() searches the logs for leak reports and tries to apply
    suppressions to them. Unsuppressed reports and other log messages are
    dumped as is.

    If |check_sanity| is True, the list of suppressed reports is searched for a
    report starting with SANITY_TEST_SUPPRESSION. If there isn't one, Analyze
    returns 2 regardless of the unsuppressed reports.

    Args:
      log_lines:      An iterator over the app's log lines.
      check_sanity:   A flag that determines whether we should check the tool's
                      sanity.
    Returns:
      2, if the sanity check fails,
      1, if unsuppressed reports remain in the output and the sanity check
      passes,
      0, if all the errors are suppressed and the sanity check passes.
    """
    return_code = 0
    # leak signature: [number of bytes, number of objects]
    cur_leak_signature = None
    cur_stack = []
    cur_report = []
    reported_hashes = {}
    # Statistics grouped by suppression description:
    # [hit count, bytes, objects].
    used_suppressions = {}
    for line in log_lines:
      line = line.rstrip()  # remove the trailing \n
      match = self.STACK_LINE_RE.match(line)
      if match:
        cur_stack.append(match.groups()[0])
        cur_report.append(line)
        continue
      else:
        if cur_stack:
          # Try to find the suppression that applies to the current leak stack.
          description = ''
          for supp in self._suppressions:
            if supp.Match(cur_stack):
              cur_stack = []
              description = supp.description
              break
          if cur_stack:
            if not cur_leak_signature:
              print 'Missing leak signature for the following stack: '
              for frame in cur_stack:
                print '   ' + frame
              print 'Aborting...'
              return 3

            # Drop boring callers from the stack to get less redundant info
            # and fewer unique reports.
            found_boring = False
            for i in range(1, len(cur_stack)):
              for j in self.BORING_CALLERS:
                if re.match(j, cur_stack[i]):
                  cur_stack = cur_stack[:i]
                  cur_report = cur_report[:i]
                  found_boring = True
                  break
              if found_boring:
                break

            error_hash = hash("".join(cur_stack)) & 0xffffffffffffffff
            if error_hash not in reported_hashes:
              reported_hashes[error_hash] = 1
              # Print the report and set the return code to 1.
              print ('Leak of %d bytes in %d objects allocated from:'
                     % tuple(cur_leak_signature))
              print '\n'.join(cur_report)
              return_code = 1
              # Generate the suppression iff the stack contains more than one
              # frame (otherwise it's likely to be broken)
              if len(cur_stack) > 1 or found_boring:
                print '\nSuppression (error hash=#%016X#):\n{'  % (error_hash)
                print '   <insert_a_suppression_name_here>'
                print '   Heapcheck:Leak'
                for frame in cur_stack:
                  print '   fun:' + frame
                print '}\n\n'
              else:
                print ('This stack may be broken due to omitted frame pointers.'
                       ' It is not recommended to suppress it.\n')
          else:
            # Update the suppressions histogram.
            if description in used_suppressions:
              hits, bytes, objects = used_suppressions[description]
              hits += 1
              bytes += cur_leak_signature[0]
              objects += cur_leak_signature[1]
              used_suppressions[description] = [hits, bytes, objects]
            else:
              used_suppressions[description] = [1] + cur_leak_signature
        cur_stack = []
        cur_report = []
        cur_leak_signature = None
        match = self.LEAK_REPORT_RE.match(line)
        if match:
          cur_leak_signature = map(int, match.groups())
        else:
          print line
    # Print the list of suppressions used.
    is_sane = False
    if used_suppressions:
      print
      print '-----------------------------------------------------'
      print 'Suppressions used:'
      print '   count    bytes  objects name'
      histo = {}
      for description in used_suppressions:
        if description.startswith(HeapcheckWrapper.SANITY_TEST_SUPPRESSION):
          is_sane = True
        hits, bytes, objects = used_suppressions[description]
        line = '%8d %8d %8d %s' % (hits, bytes, objects, description)
        if hits in histo:
          histo[hits].append(line)
        else:
          histo[hits] = [line]
      keys = histo.keys()
      keys.sort()
      for count in keys:
        for line in histo[count]:
          print line
      print '-----------------------------------------------------'
    if check_sanity and not is_sane:
      logging.error("Sanity check failed")
      return 2
    else:
      return return_code

  def RunTestsAndAnalyze(self, check_sanity):
    exec_retcode = self.Execute()
    log_file = file(self.TMP_FILE, 'r')
    analyze_retcode = self.Analyze(log_file, check_sanity)
    log_file.close()

    if analyze_retcode:
      logging.error("Analyze failed.")
      return analyze_retcode

    if exec_retcode:
      logging.error("Test execution failed.")
      return exec_retcode
    else:
      logging.info("Test execution completed successfully.")

    return 0

  def Main(self, args, check_sanity=False):
    self._args = args
    start = datetime.datetime.now()
    retcode = -1
    retcode = self.RunTestsAndAnalyze(check_sanity)
    end = datetime.datetime.now()
    seconds = (end - start).seconds
    hours = seconds / 3600
    seconds %= 3600
    minutes = seconds / 60
    seconds %= 60
    logging.info('elapsed time: %02d:%02d:%02d', hours, minutes, seconds)
    logging.info('For more information on the Heapcheck bot see '
                 'http://dev.chromium.org/developers/how-tos/'
                 'using-the-heap-leak-checker')
    return retcode


def RunTool(args, supp_files, module):
  tool = HeapcheckWrapper(supp_files)
  MODULES_TO_SANITY_CHECK = ["base"]
  check_sanity = module in MODULES_TO_SANITY_CHECK
  return tool.Main(args[1:], check_sanity)