Home | History | Annotate | Download | only in valgrind
      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 # drmemory_analyze.py
      7 
      8 ''' Given a Dr. Memory output file, parses errors and uniques them.'''
      9 
     10 from collections import defaultdict
     11 import common
     12 import hashlib
     13 import logging
     14 import optparse
     15 import os
     16 import re
     17 import subprocess
     18 import sys
     19 import time
     20 
     21 class DrMemoryError:
     22   def __init__(self, report, suppression, testcase):
     23     self._report = report
     24     self._testcase = testcase
     25 
     26     # Chromium-specific transformations of the suppressions:
     27     # Replace 'any_test.exe' and 'chrome.dll' with '*', then remove the
     28     # Dr.Memory-generated error ids from the name= lines as they don't
     29     # make sense in a multiprocess report.
     30     supp_lines = suppression.split("\n")
     31     for l in xrange(len(supp_lines)):
     32       if supp_lines[l].startswith("name="):
     33         supp_lines[l] = "name=<insert_a_suppression_name_here>"
     34       if supp_lines[l].startswith("chrome.dll!"):
     35         supp_lines[l] = supp_lines[l].replace("chrome.dll!", "*!")
     36       bang_index = supp_lines[l].find("!")
     37       d_exe_index = supp_lines[l].find(".exe!")
     38       if bang_index >= 4 and d_exe_index + 4 == bang_index:
     39         supp_lines[l] = "*" + supp_lines[l][bang_index:]
     40     self._suppression = "\n".join(supp_lines)
     41 
     42   def __str__(self):
     43     output = self._report + "\n"
     44     if self._testcase:
     45       output += "The report came from the `%s` test.\n" % self._testcase
     46     output += "Suppression (error hash=#%016X#):\n" % self.ErrorHash()
     47     output += ("  For more info on using suppressions see "
     48         "http://dev.chromium.org/developers/how-tos/using-drmemory#TOC-Suppressing-error-reports-from-the-\n")
     49     output += "{\n%s\n}\n" % self._suppression
     50     return output
     51 
     52   # This is a device-independent hash identifying the suppression.
     53   # By printing out this hash we can find duplicate reports between tests and
     54   # different shards running on multiple buildbots
     55   def ErrorHash(self):
     56     return int(hashlib.md5(self._suppression).hexdigest()[:16], 16)
     57 
     58   def __hash__(self):
     59     return hash(self._suppression)
     60 
     61   def __eq__(self, rhs):
     62     return self._suppression == rhs
     63 
     64 
     65 class DrMemoryAnalyzer:
     66   ''' Given a set of Dr.Memory output files, parse all the errors out of
     67   them, unique them and output the results.'''
     68 
     69   def __init__(self):
     70     self.known_errors = set()
     71     self.error_count = 0;
     72 
     73   def ReadLine(self):
     74     self.line_ = self.cur_fd_.readline()
     75 
     76   def ReadSection(self):
     77     result = [self.line_]
     78     self.ReadLine()
     79     while len(self.line_.strip()) > 0:
     80       result.append(self.line_)
     81       self.ReadLine()
     82     return result
     83 
     84   def ParseReportFile(self, filename, testcase):
     85     ret = []
     86 
     87     # First, read the generated suppressions file so we can easily lookup a
     88     # suppression for a given error.
     89     supp_fd = open(filename.replace("results", "suppress"), 'r')
     90     generated_suppressions = {}  # Key -> Error #, Value -> Suppression text.
     91     for line in supp_fd:
     92       # NOTE: this regexp looks fragile. Might break if the generated
     93       # suppression format slightly changes.
     94       m = re.search("# Suppression for Error #([0-9]+)", line.strip())
     95       if not m:
     96         continue
     97       error_id = int(m.groups()[0])
     98       assert error_id not in generated_suppressions
     99       # OK, now read the next suppression:
    100       cur_supp = ""
    101       for supp_line in supp_fd:
    102         if supp_line.startswith("#") or supp_line.strip() == "":
    103           break
    104         cur_supp += supp_line
    105       generated_suppressions[error_id] = cur_supp.strip()
    106     supp_fd.close()
    107 
    108     self.cur_fd_ = open(filename, 'r')
    109     while True:
    110       self.ReadLine()
    111       if (self.line_ == ''): break
    112 
    113       match = re.search("^Error #([0-9]+): (.*)", self.line_)
    114       if match:
    115         error_id = int(match.groups()[0])
    116         self.line_ = match.groups()[1].strip() + "\n"
    117         report = "".join(self.ReadSection()).strip()
    118         suppression = generated_suppressions[error_id]
    119         ret.append(DrMemoryError(report, suppression, testcase))
    120 
    121       if re.search("SUPPRESSIONS USED:", self.line_):
    122         self.ReadLine()
    123         while self.line_.strip() != "":
    124           line = self.line_.strip()
    125           (count, name) = re.match(" *([0-9\?]+)x(?: \(.*?\))?: (.*)",
    126                                    line).groups()
    127           if (count == "?"):
    128             # Whole-module have no count available: assume 1
    129             count = 1
    130           else:
    131             count = int(count)
    132           self.used_suppressions[name] += count
    133           self.ReadLine()
    134 
    135       if self.line_.startswith("ASSERT FAILURE"):
    136         ret.append(self.line_.strip())
    137 
    138     self.cur_fd_.close()
    139     return ret
    140 
    141   def Report(self, filenames, testcase, check_sanity):
    142     sys.stdout.flush()
    143     # TODO(timurrrr): support positive tests / check_sanity==True
    144     self.used_suppressions = defaultdict(int)
    145 
    146     to_report = []
    147     reports_for_this_test = set()
    148     for f in filenames:
    149       cur_reports = self.ParseReportFile(f, testcase)
    150 
    151       # Filter out the reports that were there in previous tests.
    152       for r in cur_reports:
    153         if r in reports_for_this_test:
    154           # A similar report is about to be printed for this test.
    155           pass
    156         elif r in self.known_errors:
    157           # A similar report has already been printed in one of the prev tests.
    158           to_report.append("This error was already printed in some "
    159                            "other test, see 'hash=#%016X#'" % r.ErrorHash())
    160           reports_for_this_test.add(r)
    161         else:
    162           self.known_errors.add(r)
    163           reports_for_this_test.add(r)
    164           to_report.append(r)
    165 
    166     common.PrintUsedSuppressionsList(self.used_suppressions)
    167 
    168     if not to_report:
    169       logging.info("PASS: No error reports found")
    170       return 0
    171 
    172     sys.stdout.flush()
    173     sys.stderr.flush()
    174     logging.info("Found %i error reports" % len(to_report))
    175     for report in to_report:
    176       self.error_count += 1
    177       logging.info("Report #%d\n%s" % (self.error_count, report))
    178     logging.info("Total: %i error reports" % len(to_report))
    179     sys.stdout.flush()
    180     return -1
    181 
    182 
    183 def main():
    184   '''For testing only. The DrMemoryAnalyze class should be imported instead.'''
    185   parser = optparse.OptionParser("usage: %prog <files to analyze>")
    186 
    187   (options, args) = parser.parse_args()
    188   if len(args) == 0:
    189     parser.error("no filename specified")
    190   filenames = args
    191 
    192   logging.getLogger().setLevel(logging.INFO)
    193   return DrMemoryAnalyzer().Report(filenames, None, False)
    194 
    195 
    196 if __name__ == '__main__':
    197   sys.exit(main())
    198