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.
5 """Wrapper for running the test under heapchecker and analyzing the output."""
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 STACK_LINE_RE
= re
.compile('\s*@\s*(?:0x)?[0-9a-fA-F]+\s*([^\n]*)')
23 BORING_CALLERS
= common
.BoringCallers(mangled
=False, use_re_wildcards
=True)
25 def __init__(self
, supp_files
):
28 self
._nocleanup
_on
_exit
= False
29 self
._suppressions
= []
30 for fname
in supp_files
:
31 self
._suppressions
.extend(suppressions
.ReadSuppressionsFromFile(fname
))
32 if os
.path
.exists(self
.TMP_FILE
):
33 os
.remove(self
.TMP_FILE
)
35 def PutEnvAndLog(self
, env_name
, env_value
):
36 """Sets the env var |env_name| to |env_value| and writes to logging.info.
38 os
.putenv(env_name
, env_value
)
39 logging
.info('export %s=%s', env_name
, env_value
)
42 """Executes the app to be tested."""
43 logging
.info('starting execution...')
44 proc
= ['sh', path_utils
.ScriptDir() + '/heapcheck_std.sh']
46 self
.PutEnvAndLog('G_SLICE', 'always-malloc')
47 self
.PutEnvAndLog('NSS_DISABLE_ARENA_FREE_LIST', '1')
48 self
.PutEnvAndLog('NSS_DISABLE_UNLOAD', '1')
49 self
.PutEnvAndLog('GTEST_DEATH_TEST_USE_FORK', '1')
50 self
.PutEnvAndLog('HEAPCHECK', self
._mode
)
51 self
.PutEnvAndLog('HEAP_CHECK_ERROR_EXIT_CODE', '0')
52 self
.PutEnvAndLog('HEAP_CHECK_MAX_LEAKS', '-1')
53 self
.PutEnvAndLog('KEEP_SHADOW_STACKS', '1')
54 self
.PutEnvAndLog('PPROF_PATH',
55 path_utils
.ScriptDir() +
56 '/../../third_party/tcmalloc/chromium/src/pprof')
57 self
.PutEnvAndLog('LD_LIBRARY_PATH',
58 '/usr/lib/debug/:/usr/lib32/debug/')
60 return common
.RunSubprocess(proc
, self
._timeout
)
62 def Analyze(self
, log_lines
, check_sanity
=False):
63 """Analyzes the app's output and applies suppressions to the reports.
65 Analyze() searches the logs for leak reports and tries to apply
66 suppressions to them. Unsuppressed reports and other log messages are
69 If |check_sanity| is True, the list of suppressed reports is searched for a
70 report starting with SANITY_TEST_SUPPRESSION. If there isn't one, Analyze
71 returns 2 regardless of the unsuppressed reports.
74 log_lines: An iterator over the app's log lines.
75 check_sanity: A flag that determines whether we should check the tool's
78 2, if the sanity check fails,
79 1, if unsuppressed reports remain in the output and the sanity check
81 0, if all the errors are suppressed and the sanity check passes.
84 # leak signature: [number of bytes, number of objects]
85 cur_leak_signature
= None
89 # Statistics grouped by suppression description:
90 # [hit count, bytes, objects].
91 used_suppressions
= {}
92 for line
in log_lines
:
93 line
= line
.rstrip() # remove the trailing \n
94 match
= self
.STACK_LINE_RE
.match(line
)
96 cur_stack
.append(match
.groups()[0])
97 cur_report
.append(line
)
101 # Try to find the suppression that applies to the current leak stack.
103 for supp
in self
._suppressions
:
104 if supp
.Match(cur_stack
):
106 description
= supp
.description
109 if not cur_leak_signature
:
110 print 'Missing leak signature for the following stack: '
111 for frame
in cur_stack
:
116 # Drop boring callers from the stack to get less redundant info
117 # and fewer unique reports.
119 for i
in range(1, len(cur_stack
)):
120 for j
in self
.BORING_CALLERS
:
121 if re
.match(j
, cur_stack
[i
]):
122 cur_stack
= cur_stack
[:i
]
123 cur_report
= cur_report
[:i
]
129 error_hash
= hash("".join(cur_stack
)) & 0xffffffffffffffff
130 if error_hash
not in reported_hashes
:
131 reported_hashes
[error_hash
] = 1
132 # Print the report and set the return code to 1.
133 print ('Leak of %d bytes in %d objects allocated from:'
134 % tuple(cur_leak_signature
))
135 print '\n'.join(cur_report
)
137 # Generate the suppression iff the stack contains more than one
138 # frame (otherwise it's likely to be broken)
139 if len(cur_stack
) > 1 or found_boring
:
140 print '\nSuppression (error hash=#%016X#):\n{' % (error_hash
)
141 print ' <insert_a_suppression_name_here>'
142 print ' Heapcheck:Leak'
143 for frame
in cur_stack
:
144 print ' fun:' + frame
147 print ('This stack may be broken due to omitted frame pointers.'
148 ' It is not recommended to suppress it.\n')
150 # Update the suppressions histogram.
151 if description
in used_suppressions
:
152 hits
, bytes
, objects
= used_suppressions
[description
]
154 bytes
+= cur_leak_signature
[0]
155 objects
+= cur_leak_signature
[1]
156 used_suppressions
[description
] = [hits
, bytes
, objects
]
158 used_suppressions
[description
] = [1] + cur_leak_signature
161 cur_leak_signature
= None
162 match
= self
.LEAK_REPORT_RE
.match(line
)
164 cur_leak_signature
= map(int, match
.groups())
167 # Print the list of suppressions used.
169 if used_suppressions
:
171 print '-----------------------------------------------------'
172 print 'Suppressions used:'
173 print ' count bytes objects name'
175 for description
in used_suppressions
:
176 if description
.startswith(HeapcheckWrapper
.SANITY_TEST_SUPPRESSION
):
178 hits
, bytes
, objects
= used_suppressions
[description
]
179 line
= '%8d %8d %8d %s' % (hits
, bytes
, objects
, description
)
181 histo
[hits
].append(line
)
187 for line
in histo
[count
]:
189 print '-----------------------------------------------------'
190 if check_sanity
and not is_sane
:
191 logging
.error("Sanity check failed")
196 def RunTestsAndAnalyze(self
, check_sanity
):
197 exec_retcode
= self
.Execute()
198 log_file
= file(self
.TMP_FILE
, 'r')
199 analyze_retcode
= self
.Analyze(log_file
, check_sanity
)
203 logging
.error("Analyze failed.")
204 return analyze_retcode
207 logging
.error("Test execution failed.")
210 logging
.info("Test execution completed successfully.")
214 def Main(self
, args
, check_sanity
=False):
216 start
= datetime
.datetime
.now()
218 retcode
= self
.RunTestsAndAnalyze(check_sanity
)
219 end
= datetime
.datetime
.now()
220 seconds
= (end
- start
).seconds
221 hours
= seconds
/ 3600
223 minutes
= seconds
/ 60
225 logging
.info('elapsed time: %02d:%02d:%02d', hours
, minutes
, seconds
)
226 logging
.info('For more information on the Heapcheck bot see '
227 'http://dev.chromium.org/developers/how-tos/'
228 'using-the-heap-leak-checker')
232 def RunTool(args
, supp_files
, module
):
233 tool
= HeapcheckWrapper(supp_files
)
234 MODULES_TO_SANITY_CHECK
= ["base"]
235 check_sanity
= module
in MODULES_TO_SANITY_CHECK
236 return tool
.Main(args
[1:], check_sanity
)