- add sources.
[platform/framework/web/crosswalk.git] / src / tools / heapcheck / heapcheck_test.py
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Wrapper for running the test under heapchecker and analyzing the output."""
6
7 import datetime
8 import logging
9 import os
10 import re
11
12 import common
13 import path_utils
14 import suppressions
15
16
17 class HeapcheckWrapper(object):
18   TMP_FILE = 'heapcheck.log'
19   SANITY_TEST_SUPPRESSION = "Heapcheck sanity test"
20   LEAK_REPORT_RE = re.compile(
21      'Leak of ([0-9]*) bytes in ([0-9]*) objects allocated from:')
22   # Workaround for http://crbug.com/132867, see below.
23   HOOKED_ALLOCATOR_RE = re.compile(
24      'Hooked allocator frame not found, returning empty trace')
25   STACK_LINE_RE = re.compile('\s*@\s*(?:0x)?[0-9a-fA-F]+\s*([^\n]*)')
26   BORING_CALLERS = common.BoringCallers(mangled=False, use_re_wildcards=True)
27
28   def __init__(self, supp_files):
29     self._mode = 'strict'
30     self._timeout = 3600
31     self._nocleanup_on_exit = False
32     self._suppressions = []
33     for fname in supp_files:
34       self._suppressions.extend(suppressions.ReadSuppressionsFromFile(fname))
35     if os.path.exists(self.TMP_FILE):
36       os.remove(self.TMP_FILE)
37
38   def PutEnvAndLog(self, env_name, env_value):
39     """Sets the env var |env_name| to |env_value| and writes to logging.info.
40     """
41     os.putenv(env_name, env_value)
42     logging.info('export %s=%s', env_name, env_value)
43
44   def Execute(self):
45     """Executes the app to be tested."""
46     logging.info('starting execution...')
47     proc = ['sh', path_utils.ScriptDir() + '/heapcheck_std.sh']
48     proc += self._args
49     self.PutEnvAndLog('G_SLICE', 'always-malloc')
50     self.PutEnvAndLog('NSS_DISABLE_ARENA_FREE_LIST', '1')
51     self.PutEnvAndLog('NSS_DISABLE_UNLOAD', '1')
52     self.PutEnvAndLog('GTEST_DEATH_TEST_USE_FORK', '1')
53     self.PutEnvAndLog('HEAPCHECK', self._mode)
54     self.PutEnvAndLog('HEAP_CHECK_ERROR_EXIT_CODE', '0')
55     self.PutEnvAndLog('HEAP_CHECK_MAX_LEAKS', '-1')
56     self.PutEnvAndLog('KEEP_SHADOW_STACKS', '1')
57     self.PutEnvAndLog('PPROF_PATH',
58         path_utils.ScriptDir() +
59         '/../../third_party/tcmalloc/chromium/src/pprof')
60     self.PutEnvAndLog('LD_LIBRARY_PATH',
61                       '/usr/lib/debug/:/usr/lib32/debug/')
62     # CHROME_DEVEL_SANDBOX causes problems with heapcheck
63     self.PutEnvAndLog('CHROME_DEVEL_SANDBOX', '');
64
65     return common.RunSubprocess(proc, self._timeout)
66
67   def Analyze(self, log_lines, check_sanity=False):
68     """Analyzes the app's output and applies suppressions to the reports.
69
70     Analyze() searches the logs for leak reports and tries to apply
71     suppressions to them. Unsuppressed reports and other log messages are
72     dumped as is.
73
74     If |check_sanity| is True, the list of suppressed reports is searched for a
75     report starting with SANITY_TEST_SUPPRESSION. If there isn't one, Analyze
76     returns 2 regardless of the unsuppressed reports.
77
78     Args:
79       log_lines:      An iterator over the app's log lines.
80       check_sanity:   A flag that determines whether we should check the tool's
81                       sanity.
82     Returns:
83       2, if the sanity check fails,
84       1, if unsuppressed reports remain in the output and the sanity check
85       passes,
86       0, if all the errors are suppressed and the sanity check passes.
87     """
88     return_code = 0
89     # leak signature: [number of bytes, number of objects]
90     cur_leak_signature = None
91     cur_stack = []
92     cur_report = []
93     reported_hashes = {}
94     # Statistics grouped by suppression description:
95     # [hit count, bytes, objects].
96     used_suppressions = {}
97     hooked_allocator_line_encountered = False
98     for line in log_lines:
99       line = line.rstrip()  # remove the trailing \n
100       match = self.STACK_LINE_RE.match(line)
101       if match:
102         cur_stack.append(match.groups()[0])
103         cur_report.append(line)
104         continue
105       else:
106         if cur_stack:
107           # Try to find the suppression that applies to the current leak stack.
108           description = ''
109           for supp in self._suppressions:
110             if supp.Match(cur_stack):
111               cur_stack = []
112               description = supp.description
113               break
114           if cur_stack:
115             if not cur_leak_signature:
116               print 'Missing leak signature for the following stack: '
117               for frame in cur_stack:
118                 print '   ' + frame
119               print 'Aborting...'
120               return 3
121
122             # Drop boring callers from the stack to get less redundant info
123             # and fewer unique reports.
124             found_boring = False
125             for i in range(1, len(cur_stack)):
126               for j in self.BORING_CALLERS:
127                 if re.match(j, cur_stack[i]):
128                   cur_stack = cur_stack[:i]
129                   cur_report = cur_report[:i]
130                   found_boring = True
131                   break
132               if found_boring:
133                 break
134
135             error_hash = hash("".join(cur_stack)) & 0xffffffffffffffff
136             if error_hash not in reported_hashes:
137               reported_hashes[error_hash] = 1
138               # Print the report and set the return code to 1.
139               print ('Leak of %d bytes in %d objects allocated from:'
140                      % tuple(cur_leak_signature))
141               print '\n'.join(cur_report)
142               return_code = 1
143               # Generate the suppression iff the stack contains more than one
144               # frame (otherwise it's likely to be broken)
145               if len(cur_stack) > 1 or found_boring:
146                 print '\nSuppression (error hash=#%016X#):\n{'  % (error_hash)
147                 print '   <insert_a_suppression_name_here>'
148                 print '   Heapcheck:Leak'
149                 for frame in cur_stack:
150                   print '   fun:' + frame
151                 print '}\n\n'
152               else:
153                 print ('This stack may be broken due to omitted frame pointers.'
154                        ' It is not recommended to suppress it.\n')
155           else:
156             # Update the suppressions histogram.
157             if description in used_suppressions:
158               hits, bytes, objects = used_suppressions[description]
159               hits += 1
160               bytes += cur_leak_signature[0]
161               objects += cur_leak_signature[1]
162               used_suppressions[description] = [hits, bytes, objects]
163             else:
164               used_suppressions[description] = [1] + cur_leak_signature
165         cur_stack = []
166         cur_report = []
167         cur_leak_signature = None
168         match = self.LEAK_REPORT_RE.match(line)
169         if match:
170           cur_leak_signature = map(int, match.groups())
171         else:
172           match = self.HOOKED_ALLOCATOR_RE.match(line)
173           if match:
174             hooked_allocator_line_encountered = True
175           else:
176             print line
177     # Print the list of suppressions used.
178     is_sane = False
179     if used_suppressions:
180       print
181       print '-----------------------------------------------------'
182       print 'Suppressions used:'
183       print '   count    bytes  objects name'
184       histo = {}
185       for description in used_suppressions:
186         if description.startswith(HeapcheckWrapper.SANITY_TEST_SUPPRESSION):
187           is_sane = True
188         hits, bytes, objects = used_suppressions[description]
189         line = '%8d %8d %8d %s' % (hits, bytes, objects, description)
190         if hits in histo:
191           histo[hits].append(line)
192         else:
193           histo[hits] = [line]
194       keys = histo.keys()
195       keys.sort()
196       for count in keys:
197         for line in histo[count]:
198           print line
199       print '-----------------------------------------------------'
200     if hooked_allocator_line_encountered:
201       print ('WARNING: Workaround for http://crbug.com/132867 (tons of '
202              '"Hooked allocator frame not found, returning empty trace") '
203              'in effect.')
204     if check_sanity and not is_sane:
205       logging.error("Sanity check failed")
206       return 2
207     else:
208       return return_code
209
210   def RunTestsAndAnalyze(self, check_sanity):
211     exec_retcode = self.Execute()
212     log_file = file(self.TMP_FILE, 'r')
213     analyze_retcode = self.Analyze(log_file, check_sanity)
214     log_file.close()
215
216     if analyze_retcode:
217       logging.error("Analyze failed.")
218       return analyze_retcode
219
220     if exec_retcode:
221       logging.error("Test execution failed.")
222       return exec_retcode
223     else:
224       logging.info("Test execution completed successfully.")
225
226     return 0
227
228   def Main(self, args, check_sanity=False):
229     self._args = args
230     start = datetime.datetime.now()
231     retcode = -1
232     retcode = self.RunTestsAndAnalyze(check_sanity)
233     end = datetime.datetime.now()
234     seconds = (end - start).seconds
235     hours = seconds / 3600
236     seconds %= 3600
237     minutes = seconds / 60
238     seconds %= 60
239     logging.info('elapsed time: %02d:%02d:%02d', hours, minutes, seconds)
240     logging.info('For more information on the Heapcheck bot see '
241                  'http://dev.chromium.org/developers/how-tos/'
242                  'using-the-heap-leak-checker')
243     return retcode
244
245
246 def RunTool(args, supp_files, module):
247   tool = HeapcheckWrapper(supp_files)
248   MODULES_TO_SANITY_CHECK = ["base"]
249   check_sanity = module in MODULES_TO_SANITY_CHECK
250   return tool.Main(args[1:], check_sanity)