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.
8 ''' Given a ThreadSanitizer output file, parses errors and uniques them.'''
12 from collections import defaultdict
24 # Global symbol table (ugh)
25 TheAddressTable = None
27 class _StackTraceLine(object):
28 def __init__(self, line, address, binary):
30 self.address = address
33 global TheAddressTable
34 file, line = TheAddressTable.GetFileLine(self.binary, self.address)
35 if (file is None) or (line is None):
38 return self.raw_line_.replace(self.binary, '%s:%s' % (file, line))
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.'''
44 LOAD_LIB_RE = re.compile('--[0-9]+-- ([^(:]*) \((0x[0-9a-f]+)\)')
45 TSAN_LINE_RE = re.compile('==[0-9]+==\s*[#0-9]+\s*'
50 THREAD_CREATION_STR = ("INFO: T.* "
51 "(has been created by T.* at this point|is program's main thread)")
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: "
62 def __init__(self, use_gdb=False):
63 '''Reads in a set of files.'''
65 self._use_gdb = use_gdb
66 self._cur_testcase = None
69 self.line_ = self.cur_fd_.readline()
70 self.stack_trace_line_ = None
73 global TheAddressTable
74 match = TsanAnalyzer.LOAD_LIB_RE.match(self.line_)
76 binary, ip = match.groups()
77 TheAddressTable.AddBinaryAt(binary, ip)
79 match = TsanAnalyzer.TSAN_LINE_RE.match(self.line_)
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
86 def ReadSection(self):
87 """ Example of a section:
88 ==4528== WARNING: Possible data race: {{{
90 ==4528== #0 MyTest::Foo1
91 ==4528== #1 MyThread::ThreadBody
92 ==4528== Concurrent write happened at this point:
94 ==4528== #0 MyTest::Foo2
95 ==4528== #1 MyThread::ThreadBody
97 ------- suppression -------
99 <Put your suppression name here>
102 fun:MyThread::ThreadBody
104 ------- end suppression -------
106 result = [self.line_]
107 if re.search("{{{", self.line_):
108 while not re.search('}}}', self.line_):
110 if self.stack_trace_line_ is None:
111 result.append(self.line_)
113 result.append(self.stack_trace_line_)
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.
120 while not re.match('-+ end suppression -+', self.line_):
124 if self._cur_testcase:
125 result.append("The report came from the `%s` test.\n" % \
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/threadsanitizer#TOC-Suppressing-data-races\n")
137 def ReadTillTheEnd(self):
138 result = [self.line_]
141 result.append(self.line_)
144 def ParseReportFile(self, filename):
145 '''Parses a report file and returns a list of ThreadSanitizer reports.
149 filename: report filename.
151 list of (list of (str iff self._use_gdb, _StackTraceLine otherwise)).
154 self.cur_fd_ = open(filename, 'r')
157 # Read ThreadSanitizer reports.
164 while re.search(TsanAnalyzer.RACE_VERIFIER_LINE, self.line_):
165 tmp.append(self.line_)
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())
180 if re.search(TsanAnalyzer.TSAN_ASSERTION, self.line_):
181 tmp.extend(self.ReadTillTheEnd())
185 match = re.search("used_suppression:\s+([0-9]+)\s(.*)", self.line_)
187 count, supp_name = match.groups()
189 self.used_suppressions[supp_name] += count
193 def GetReports(self, files):
194 '''Extracts reports from a set of files.
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.
201 global TheAddressTable
203 TheAddressTable = gdb_helper.AddressTable()
205 TheAddressTable = None
207 self.used_suppressions = defaultdict(int)
209 reports.extend(self.ParseReportFile(file))
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]
216 def Report(self, files, testcase, check_sanity=False):
217 '''Reads in a set of files and prints ThreadSanitizer report.
220 files: A list of filenames.
221 check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS
224 # We set up _cur_testcase class-wide variable to avoid passing it through
226 self._cur_testcase = testcase
227 reports = self.GetReports(files)
228 self._cur_testcase = None # just in case, shouldn't be used anymore
230 common.PrintUsedSuppressionsList(self.used_suppressions)
237 logging.info("FAIL! Found %i report(s)" % len(reports))
238 for report in reports:
239 logging.info('\n' + report)
243 # Report tool's insanity even if there were errors.
245 TsanAnalyzer.SANITY_TEST_SUPPRESSION not in self.used_suppressions):
246 logging.error("FAIL! Sanity check failed!")
252 logging.info("PASS: No reports found")
257 '''For testing only. The TsanAnalyzer class should be imported instead.'''
258 parser = optparse.OptionParser("usage: %prog <files to analyze>")
260 (options, args) = parser.parse_args()
262 parser.error("no filename specified")
265 logging.getLogger().setLevel(logging.INFO)
266 analyzer = TsanAnalyzer(use_gdb=True)
267 return analyzer.Report(filenames, None)
270 if __name__ == '__main__':