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 10from collections import defaultdict 11import common 12import hashlib 13import logging 14import optparse 15import os 16import re 17import subprocess 18import sys 19import time 20 21class 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-Suppressing-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 70class 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 188def 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 201if __name__ == '__main__': 202 sys.exit(main()) 203