summaryrefslogtreecommitdiff
path: root/tools/drmemory/scripts/drmemory_analyze.py
blob: 29fc0ed4b0c52de0e13cda5b2afb5a553ebd61c6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/env python
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# drmemory_analyze.py

''' Given a Dr. Memory output file, parses errors and uniques them.'''

from collections import defaultdict
import common
import hashlib
import logging
import optparse
import os
import re
import subprocess
import sys
import time

class DrMemoryError:
  def __init__(self, report, suppression, testcase):
    self._report = report
    self._testcase = testcase

    # Chromium-specific transformations of the suppressions:
    # Replace 'any_test.exe' and 'chrome.dll' with '*', then remove the
    # Dr.Memory-generated error ids from the name= lines as they don't
    # make sense in a multiprocess report.
    supp_lines = suppression.split("\n")
    for l in xrange(len(supp_lines)):
      if supp_lines[l].startswith("name="):
        supp_lines[l] = "name=<insert_a_suppression_name_here>"
      if supp_lines[l].startswith("chrome.dll!"):
        supp_lines[l] = supp_lines[l].replace("chrome.dll!", "*!")
      bang_index = supp_lines[l].find("!")
      d_exe_index = supp_lines[l].find(".exe!")
      if bang_index >= 4 and d_exe_index + 4 == bang_index:
        supp_lines[l] = "*" + supp_lines[l][bang_index:]
    self._suppression = "\n".join(supp_lines)

  def __str__(self):
    output = ""
    output += "### BEGIN MEMORY TOOL REPORT (error hash=#%016X#)\n" % \
        self.ErrorHash()
    output += self._report + "\n"
    if self._testcase:
      output += "The report came from the `%s` test.\n" % self._testcase
    output += "Suppression (error hash=#%016X#):\n" % self.ErrorHash()
    output += ("  For more info on using suppressions see "
        "http://dev.chromium.org/developers/how-tos/using-drmemory#TOC-Suppressing-error-reports-from-the-\n")
    output += "{\n%s\n}\n" % self._suppression
    output += "### END MEMORY TOOL REPORT (error hash=#%016X#)\n" % \
        self.ErrorHash()
    return output

  # This is a device-independent hash identifying the suppression.
  # By printing out this hash we can find duplicate reports between tests and
  # different shards running on multiple buildbots
  def ErrorHash(self):
    return int(hashlib.md5(self._suppression).hexdigest()[:16], 16)

  def __hash__(self):
    return hash(self._suppression)

  def __eq__(self, rhs):
    return self._suppression == rhs


class DrMemoryAnalyzer:
  ''' Given a set of Dr.Memory output files, parse all the errors out of
  them, unique them and output the results.'''

  def __init__(self):
    self.known_errors = set()
    self.error_count = 0;

  def ReadLine(self):
    self.line_ = self.cur_fd_.readline()

  def ReadSection(self):
    result = [self.line_]
    self.ReadLine()
    while len(self.line_.strip()) > 0:
      result.append(self.line_)
      self.ReadLine()
    return result

  def ParseReportFile(self, filename, testcase):
    ret = []

    # First, read the generated suppressions file so we can easily lookup a
    # suppression for a given error.
    supp_fd = open(filename.replace("results", "suppress"), 'r')
    generated_suppressions = {}  # Key -> Error #, Value -> Suppression text.
    for line in supp_fd:
      # NOTE: this regexp looks fragile. Might break if the generated
      # suppression format slightly changes.
      m = re.search("# Suppression for Error #([0-9]+)", line.strip())
      if not m:
        continue
      error_id = int(m.groups()[0])
      assert error_id not in generated_suppressions
      # OK, now read the next suppression:
      cur_supp = ""
      for supp_line in supp_fd:
        if supp_line.startswith("#") or supp_line.strip() == "":
          break
        cur_supp += supp_line
      generated_suppressions[error_id] = cur_supp.strip()
    supp_fd.close()

    self.cur_fd_ = open(filename, 'r')
    while True:
      self.ReadLine()
      if (self.line_ == ''): break

      match = re.search("^Error #([0-9]+): (.*)", self.line_)
      if match:
        error_id = int(match.groups()[0])
        self.line_ = match.groups()[1].strip() + "\n"
        report = "".join(self.ReadSection()).strip()
        suppression = generated_suppressions[error_id]
        ret.append(DrMemoryError(report, suppression, testcase))

      if re.search("SUPPRESSIONS USED:", self.line_):
        self.ReadLine()
        while self.line_.strip() != "":
          line = self.line_.strip()
          (count, name) = re.match(" *([0-9\?]+)x(?: \(.*?\))?: (.*)",
                                   line).groups()
          if (count == "?"):
            # Whole-module have no count available: assume 1
            count = 1
          else:
            count = int(count)
          self.used_suppressions[name] += count
          self.ReadLine()

      if self.line_.startswith("ASSERT FAILURE"):
        ret.append(self.line_.strip())

    self.cur_fd_.close()
    return ret

  def Report(self, filenames, testcase, check_sanity):
    sys.stdout.flush()
    # TODO(timurrrr): support positive tests / check_sanity==True
    self.used_suppressions = defaultdict(int)

    to_report = []
    reports_for_this_test = set()
    for f in filenames:
      cur_reports = self.ParseReportFile(f, testcase)

      # Filter out the reports that were there in previous tests.
      for r in cur_reports:
        if r in reports_for_this_test:
          # A similar report is about to be printed for this test.
          pass
        elif r in self.known_errors:
          # A similar report has already been printed in one of the prev tests.
          to_report.append("This error was already printed in some "
                           "other test, see 'hash=#%016X#'" % r.ErrorHash())
          reports_for_this_test.add(r)
        else:
          self.known_errors.add(r)
          reports_for_this_test.add(r)
          to_report.append(r)

    common.PrintUsedSuppressionsList(self.used_suppressions)

    if not to_report:
      logging.info("PASS: No error reports found")
      return 0

    sys.stdout.flush()
    sys.stderr.flush()
    logging.info("Found %i error reports" % len(to_report))
    for report in to_report:
      self.error_count += 1
      logging.info("Report #%d\n%s" % (self.error_count, report))
    logging.info("Total: %i error reports" % len(to_report))
    sys.stdout.flush()
    return -1


def main():
  '''For testing only. The DrMemoryAnalyze class should be imported instead.'''
  parser = optparse.OptionParser("usage: %prog <files to analyze>")

  (options, args) = parser.parse_args()
  if len(args) == 0:
    parser.error("no filename specified")
  filenames = args

  logging.getLogger().setLevel(logging.INFO)
  return DrMemoryAnalyzer().Report(filenames, None, False)


if __name__ == '__main__':
  sys.exit(main())