| 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 # tsan_analyze.py | |
| 7 | |
| 8 ''' Given a ThreadSanitizer output file, parses errors and uniques them.''' | |
| 9 | |
| 10 import gdb_helper | |
| 11 | |
| 12 from collections import defaultdict | |
| 13 import hashlib | |
| 14 import logging | |
| 15 import optparse | |
| 16 import os | |
| 17 import re | |
| 18 import subprocess | |
| 19 import sys | |
| 20 import time | |
| 21 | |
| 22 import common | |
| 23 | |
| 24 # Global symbol table (ugh) | |
| 25 TheAddressTable = None | |
| 26 | |
| 27 class _StackTraceLine(object): | |
| 28 def __init__(self, line, address, binary): | |
| 29 self.raw_line_ = line | |
| 30 self.address = address | |
| 31 self.binary = binary | |
| 32 def __str__(self): | |
| 33 global TheAddressTable | |
| 34 file, line = TheAddressTable.GetFileLine(self.binary, self.address) | |
| 35 if (file is None) or (line is None): | |
| 36 return self.raw_line_ | |
| 37 else: | |
| 38 return self.raw_line_.replace(self.binary, '%s:%s' % (file, line)) | |
| 39 | |
| 40 class TsanAnalyzer(object): | |
| 41 ''' Given a set of ThreadSanitizer output files, parse all the errors out of | |
| 42 them, unique them and output the results.''' | |
| 43 | |
| 44 LOAD_LIB_RE = re.compile('--[0-9]+-- ([^(:]*) \((0x[0-9a-f]+)\)') | |
| 45 TSAN_LINE_RE = re.compile('==[0-9]+==\s*[#0-9]+\s*' | |
| 46 '([0-9A-Fa-fx]+):' | |
| 47 '(?:[^ ]* )*' | |
| 48 '([^ :\n]+)' | |
| 49 '') | |
| 50 THREAD_CREATION_STR = ("INFO: T.* " | |
| 51 "(has been created by T.* at this point|is program's main thread)") | |
| 52 | |
| 53 SANITY_TEST_SUPPRESSION = ("ThreadSanitizer sanity test " | |
| 54 "(ToolsSanityTest.DataRace)") | |
| 55 TSAN_RACE_DESCRIPTION = "Possible data race" | |
| 56 TSAN_WARNING_DESCRIPTION = ("Unlocking a non-locked lock" | |
| 57 "|accessing an invalid lock" | |
| 58 "|which did not acquire this lock") | |
| 59 RACE_VERIFIER_LINE = "Confirmed a race|unexpected race" | |
| 60 TSAN_ASSERTION = "Assertion failed: " | |
| 61 | |
| 62 def __init__(self, use_gdb=False): | |
| 63 '''Reads in a set of files.''' | |
| 64 | |
| 65 self._use_gdb = use_gdb | |
| 66 self._cur_testcase = None | |
| 67 | |
| 68 def ReadLine(self): | |
| 69 self.line_ = self.cur_fd_.readline() | |
| 70 self.stack_trace_line_ = None | |
| 71 if not self._use_gdb: | |
| 72 return | |
| 73 global TheAddressTable | |
| 74 match = TsanAnalyzer.LOAD_LIB_RE.match(self.line_) | |
| 75 if match: | |
| 76 binary, ip = match.groups() | |
| 77 TheAddressTable.AddBinaryAt(binary, ip) | |
| 78 return | |
| 79 match = TsanAnalyzer.TSAN_LINE_RE.match(self.line_) | |
| 80 if match: | |
| 81 address, binary_name = match.groups() | |
| 82 stack_trace_line = _StackTraceLine(self.line_, address, binary_name) | |
| 83 TheAddressTable.Add(stack_trace_line.binary, stack_trace_line.address) | |
| 84 self.stack_trace_line_ = stack_trace_line | |
| 85 | |
| 86 def ReadSection(self): | |
| 87 """ Example of a section: | |
| 88 ==4528== WARNING: Possible data race: {{{ | |
| 89 ==4528== T20 (L{}): | |
| 90 ==4528== #0 MyTest::Foo1 | |
| 91 ==4528== #1 MyThread::ThreadBody | |
| 92 ==4528== Concurrent write happened at this point: | |
| 93 ==4528== T19 (L{}): | |
| 94 ==4528== #0 MyTest::Foo2 | |
| 95 ==4528== #1 MyThread::ThreadBody | |
| 96 ==4528== }}} | |
| 97 ------- suppression ------- | |
| 98 { | |
| 99 <Put your suppression name here> | |
| 100 ThreadSanitizer:Race | |
| 101 fun:MyTest::Foo1 | |
| 102 fun:MyThread::ThreadBody | |
| 103 } | |
| 104 ------- end suppression ------- | |
| 105 """ | |
| 106 result = [self.line_] | |
| 107 if re.search("{{{", self.line_): | |
| 108 while not re.search('}}}', self.line_): | |
| 109 self.ReadLine() | |
| 110 if self.stack_trace_line_ is None: | |
| 111 result.append(self.line_) | |
| 112 else: | |
| 113 result.append(self.stack_trace_line_) | |
| 114 self.ReadLine() | |
| 115 if re.match('-+ suppression -+', self.line_): | |
| 116 # We need to calculate the suppression hash and prepend a line like | |
| 117 # "Suppression (error hash=#0123456789ABCDEF#):" so the buildbot can | |
| 118 # extract the suppression snippet. | |
| 119 supp = "" | |
| 120 while not re.match('-+ end suppression -+', self.line_): | |
| 121 self.ReadLine() | |
| 122 supp += self.line_ | |
| 123 self.ReadLine() | |
| 124 if self._cur_testcase: | |
| 125 result.append("The report came from the `%s` test.\n" % \ | |
| 126 self._cur_testcase) | |
| 127 result.append("Suppression (error hash=#%016X#):\n" % \ | |
| 128 (int(hashlib.md5(supp).hexdigest()[:16], 16))) | |
| 129 result.append(" For more info on using suppressions see " | |
| 130 "http://dev.chromium.org/developers/how-tos/using-valgrind/threadsan
itizer#TOC-Suppressing-data-races\n") | |
| 131 result.append(supp) | |
| 132 else: | |
| 133 self.ReadLine() | |
| 134 | |
| 135 return result | |
| 136 | |
| 137 def ReadTillTheEnd(self): | |
| 138 result = [self.line_] | |
| 139 while self.line_: | |
| 140 self.ReadLine() | |
| 141 result.append(self.line_) | |
| 142 return result | |
| 143 | |
| 144 def ParseReportFile(self, filename): | |
| 145 '''Parses a report file and returns a list of ThreadSanitizer reports. | |
| 146 | |
| 147 | |
| 148 Args: | |
| 149 filename: report filename. | |
| 150 Returns: | |
| 151 list of (list of (str iff self._use_gdb, _StackTraceLine otherwise)). | |
| 152 ''' | |
| 153 ret = [] | |
| 154 self.cur_fd_ = open(filename, 'r') | |
| 155 | |
| 156 while True: | |
| 157 # Read ThreadSanitizer reports. | |
| 158 self.ReadLine() | |
| 159 if not self.line_: | |
| 160 break | |
| 161 | |
| 162 while True: | |
| 163 tmp = [] | |
| 164 while re.search(TsanAnalyzer.RACE_VERIFIER_LINE, self.line_): | |
| 165 tmp.append(self.line_) | |
| 166 self.ReadLine() | |
| 167 while re.search(TsanAnalyzer.THREAD_CREATION_STR, self.line_): | |
| 168 tmp.extend(self.ReadSection()) | |
| 169 if re.search(TsanAnalyzer.TSAN_RACE_DESCRIPTION, self.line_): | |
| 170 tmp.extend(self.ReadSection()) | |
| 171 ret.append(tmp) # includes RaceVerifier and thread creation stacks | |
| 172 elif (re.search(TsanAnalyzer.TSAN_WARNING_DESCRIPTION, self.line_) and | |
| 173 not common.IsWindows()): # workaround for http://crbug.com/53198 | |
| 174 tmp.extend(self.ReadSection()) | |
| 175 ret.append(tmp) | |
| 176 else: | |
| 177 break | |
| 178 | |
| 179 tmp = [] | |
| 180 if re.search(TsanAnalyzer.TSAN_ASSERTION, self.line_): | |
| 181 tmp.extend(self.ReadTillTheEnd()) | |
| 182 ret.append(tmp) | |
| 183 break | |
| 184 | |
| 185 match = re.search("used_suppression:\s+([0-9]+)\s(.*)", self.line_) | |
| 186 if match: | |
| 187 count, supp_name = match.groups() | |
| 188 count = int(count) | |
| 189 self.used_suppressions[supp_name] += count | |
| 190 self.cur_fd_.close() | |
| 191 return ret | |
| 192 | |
| 193 def GetReports(self, files): | |
| 194 '''Extracts reports from a set of files. | |
| 195 | |
| 196 Reads a set of files and returns a list of all discovered | |
| 197 ThreadSanitizer race reports. As a side effect, populates | |
| 198 self.used_suppressions with appropriate info. | |
| 199 ''' | |
| 200 | |
| 201 global TheAddressTable | |
| 202 if self._use_gdb: | |
| 203 TheAddressTable = gdb_helper.AddressTable() | |
| 204 else: | |
| 205 TheAddressTable = None | |
| 206 reports = [] | |
| 207 self.used_suppressions = defaultdict(int) | |
| 208 for file in files: | |
| 209 reports.extend(self.ParseReportFile(file)) | |
| 210 if self._use_gdb: | |
| 211 TheAddressTable.ResolveAll() | |
| 212 # Make each line of each report a string. | |
| 213 reports = map(lambda(x): map(str, x), reports) | |
| 214 return [''.join(report_lines) for report_lines in reports] | |
| 215 | |
| 216 def Report(self, files, testcase, check_sanity=False): | |
| 217 '''Reads in a set of files and prints ThreadSanitizer report. | |
| 218 | |
| 219 Args: | |
| 220 files: A list of filenames. | |
| 221 check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS | |
| 222 ''' | |
| 223 | |
| 224 # We set up _cur_testcase class-wide variable to avoid passing it through | |
| 225 # about 5 functions. | |
| 226 self._cur_testcase = testcase | |
| 227 reports = self.GetReports(files) | |
| 228 self._cur_testcase = None # just in case, shouldn't be used anymore | |
| 229 | |
| 230 common.PrintUsedSuppressionsList(self.used_suppressions) | |
| 231 | |
| 232 | |
| 233 retcode = 0 | |
| 234 if reports: | |
| 235 sys.stdout.flush() | |
| 236 sys.stderr.flush() | |
| 237 logging.info("FAIL! Found %i report(s)" % len(reports)) | |
| 238 for report in reports: | |
| 239 logging.info('\n' + report) | |
| 240 sys.stdout.flush() | |
| 241 retcode = -1 | |
| 242 | |
| 243 # Report tool's insanity even if there were errors. | |
| 244 if (check_sanity and | |
| 245 TsanAnalyzer.SANITY_TEST_SUPPRESSION not in self.used_suppressions): | |
| 246 logging.error("FAIL! Sanity check failed!") | |
| 247 retcode = -3 | |
| 248 | |
| 249 if retcode != 0: | |
| 250 return retcode | |
| 251 | |
| 252 logging.info("PASS: No reports found") | |
| 253 return 0 | |
| 254 | |
| 255 | |
| 256 def main(): | |
| 257 '''For testing only. The TsanAnalyzer class should be imported instead.''' | |
| 258 parser = optparse.OptionParser("usage: %prog <files to analyze>") | |
| 259 | |
| 260 (options, args) = parser.parse_args() | |
| 261 if not args: | |
| 262 parser.error("no filename specified") | |
| 263 filenames = args | |
| 264 | |
| 265 logging.getLogger().setLevel(logging.INFO) | |
| 266 analyzer = TsanAnalyzer(use_gdb=True) | |
| 267 return analyzer.Report(filenames, None) | |
| 268 | |
| 269 | |
| 270 if __name__ == '__main__': | |
| 271 sys.exit(main()) | |
| OLD | NEW |