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