| OLD | NEW | 
|---|
|  | (Empty) | 
| 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) |  | 
| OLD | NEW | 
|---|