kjellander | 0393de4 | 2017-06-18 13:21:21 -0700 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
| 3 | # |
| 4 | # Use of this source code is governed by a BSD-style license |
| 5 | # that can be found in the LICENSE file in the root of the source |
| 6 | # tree. An additional intellectual property rights grant can be found |
| 7 | # in the file PATENTS. All contributing project authors may |
| 8 | # be found in the AUTHORS file in the root of the source tree. |
| 9 | |
| 10 | # memcheck_analyze.py |
| 11 | |
| 12 | ''' Given a valgrind XML file, parses errors and uniques them.''' |
| 13 | |
| 14 | import gdb_helper |
| 15 | |
| 16 | from collections import defaultdict |
| 17 | import hashlib |
| 18 | import logging |
| 19 | import optparse |
| 20 | import os |
| 21 | import re |
| 22 | import subprocess |
| 23 | import sys |
| 24 | import time |
| 25 | from xml.dom.minidom import parse |
| 26 | from xml.parsers.expat import ExpatError |
| 27 | |
| 28 | import common |
| 29 | |
| 30 | # Global symbol table (yuck) |
| 31 | TheAddressTable = None |
| 32 | |
| 33 | # These are regexps that define functions (using C++ mangled names) |
| 34 | # we don't want to see in stack traces while pretty printing |
| 35 | # or generating suppressions. |
| 36 | # Just stop printing the stack/suppression frames when the current one |
| 37 | # matches any of these. |
| 38 | _BORING_CALLERS = common.BoringCallers(mangled=True, use_re_wildcards=True) |
| 39 | |
| 40 | def getTextOf(top_node, name): |
| 41 | ''' Returns all text in all DOM nodes with a certain |name| that are children |
| 42 | of |top_node|. |
| 43 | ''' |
| 44 | |
| 45 | text = "" |
| 46 | for nodes_named in top_node.getElementsByTagName(name): |
| 47 | text += "".join([node.data for node in nodes_named.childNodes |
| 48 | if node.nodeType == node.TEXT_NODE]) |
| 49 | return text |
| 50 | |
| 51 | def getCDATAOf(top_node, name): |
| 52 | ''' Returns all CDATA in all DOM nodes with a certain |name| that are children |
| 53 | of |top_node|. |
| 54 | ''' |
| 55 | |
| 56 | text = "" |
| 57 | for nodes_named in top_node.getElementsByTagName(name): |
| 58 | text += "".join([node.data for node in nodes_named.childNodes |
| 59 | if node.nodeType == node.CDATA_SECTION_NODE]) |
| 60 | if (text == ""): |
| 61 | return None |
| 62 | return text |
| 63 | |
| 64 | def shortenFilePath(source_dir, directory): |
| 65 | '''Returns a string with the string prefix |source_dir| removed from |
| 66 | |directory|.''' |
| 67 | prefixes_to_cut = ["build/src/", "valgrind/coregrind/", "out/Release/../../"] |
| 68 | |
| 69 | if source_dir: |
| 70 | prefixes_to_cut.append(source_dir) |
| 71 | |
| 72 | for p in prefixes_to_cut: |
| 73 | index = directory.rfind(p) |
| 74 | if index != -1: |
| 75 | directory = directory[index + len(p):] |
| 76 | |
| 77 | return directory |
| 78 | |
| 79 | # Constants that give real names to the abbreviations in valgrind XML output. |
| 80 | INSTRUCTION_POINTER = "ip" |
| 81 | OBJECT_FILE = "obj" |
| 82 | FUNCTION_NAME = "fn" |
| 83 | SRC_FILE_DIR = "dir" |
| 84 | SRC_FILE_NAME = "file" |
| 85 | SRC_LINE = "line" |
| 86 | |
| 87 | def gatherFrames(node, source_dir): |
| 88 | frames = [] |
| 89 | for frame in node.getElementsByTagName("frame"): |
| 90 | frame_dict = { |
| 91 | INSTRUCTION_POINTER : getTextOf(frame, INSTRUCTION_POINTER), |
| 92 | OBJECT_FILE : getTextOf(frame, OBJECT_FILE), |
| 93 | FUNCTION_NAME : getTextOf(frame, FUNCTION_NAME), |
| 94 | SRC_FILE_DIR : shortenFilePath( |
| 95 | source_dir, getTextOf(frame, SRC_FILE_DIR)), |
| 96 | SRC_FILE_NAME : getTextOf(frame, SRC_FILE_NAME), |
| 97 | SRC_LINE : getTextOf(frame, SRC_LINE) |
| 98 | } |
| 99 | |
| 100 | # Ignore this frame and all the following if it's a "boring" function. |
| 101 | enough_frames = False |
| 102 | for regexp in _BORING_CALLERS: |
| 103 | if re.match("^%s$" % regexp, frame_dict[FUNCTION_NAME]): |
| 104 | enough_frames = True |
| 105 | break |
| 106 | if enough_frames: |
| 107 | break |
| 108 | |
| 109 | frames += [frame_dict] |
| 110 | |
| 111 | global TheAddressTable |
| 112 | if TheAddressTable != None and frame_dict[SRC_LINE] == "": |
| 113 | # Try using gdb |
| 114 | TheAddressTable.Add(frame_dict[OBJECT_FILE], |
| 115 | frame_dict[INSTRUCTION_POINTER]) |
| 116 | return frames |
| 117 | |
| 118 | class ValgrindError: |
| 119 | ''' Takes a <DOM Element: error> node and reads all the data from it. A |
| 120 | ValgrindError is immutable and is hashed on its pretty printed output. |
| 121 | ''' |
| 122 | |
| 123 | def __init__(self, source_dir, error_node, commandline, testcase): |
| 124 | ''' Copies all the relevant information out of the DOM and into object |
| 125 | properties. |
| 126 | |
| 127 | Args: |
| 128 | error_node: The <error></error> DOM node we're extracting from. |
| 129 | source_dir: Prefix that should be stripped from the <dir> node. |
| 130 | commandline: The command that was run under valgrind |
| 131 | testcase: The test case name, if known. |
| 132 | ''' |
| 133 | |
| 134 | # Valgrind errors contain one <what><stack> pair, plus an optional |
| 135 | # <auxwhat><stack> pair, plus an optional <origin><what><stack></origin>, |
| 136 | # plus (since 3.5.0) a <suppression></suppression> pair. |
| 137 | # (Origin is nicely enclosed; too bad the other two aren't.) |
| 138 | # The most common way to see all three in one report is |
| 139 | # a syscall with a parameter that points to uninitialized memory, e.g. |
| 140 | # Format: |
| 141 | # <error> |
| 142 | # <unique>0x6d</unique> |
| 143 | # <tid>1</tid> |
| 144 | # <kind>SyscallParam</kind> |
| 145 | # <what>Syscall param write(buf) points to uninitialised byte(s)</what> |
| 146 | # <stack> |
| 147 | # <frame> |
| 148 | # ... |
| 149 | # </frame> |
| 150 | # </stack> |
| 151 | # <auxwhat>Address 0x5c9af4f is 7 bytes inside a block of ...</auxwhat> |
| 152 | # <stack> |
| 153 | # <frame> |
| 154 | # ... |
| 155 | # </frame> |
| 156 | # </stack> |
| 157 | # <origin> |
| 158 | # <what>Uninitialised value was created by a heap allocation</what> |
| 159 | # <stack> |
| 160 | # <frame> |
| 161 | # ... |
| 162 | # </frame> |
| 163 | # </stack> |
| 164 | # </origin> |
| 165 | # <suppression> |
| 166 | # <sname>insert_a_suppression_name_here</sname> |
| 167 | # <skind>Memcheck:Param</skind> |
| 168 | # <skaux>write(buf)</skaux> |
| 169 | # <sframe> <fun>__write_nocancel</fun> </sframe> |
| 170 | # ... |
| 171 | # <sframe> <fun>main</fun> </sframe> |
| 172 | # <rawtext> |
| 173 | # <![CDATA[ |
| 174 | # { |
| 175 | # <insert_a_suppression_name_here> |
| 176 | # Memcheck:Param |
| 177 | # write(buf) |
| 178 | # fun:__write_nocancel |
| 179 | # ... |
| 180 | # fun:main |
| 181 | # } |
| 182 | # ]]> |
| 183 | # </rawtext> |
| 184 | # </suppression> |
| 185 | # </error> |
| 186 | # |
| 187 | # Each frame looks like this: |
| 188 | # <frame> |
| 189 | # <ip>0x83751BC</ip> |
| 190 | # <obj>/data/dkegel/chrome-build/src/out/Release/base_unittests</obj> |
| 191 | # <fn>_ZN7testing8internal12TestInfoImpl7RunTestEPNS_8TestInfoE</fn> |
| 192 | # <dir>/data/dkegel/chrome-build/src/testing/gtest/src</dir> |
| 193 | # <file>gtest-internal-inl.h</file> |
| 194 | # <line>655</line> |
| 195 | # </frame> |
| 196 | # although the dir, file, and line elements are missing if there is |
| 197 | # no debug info. |
| 198 | |
| 199 | self._kind = getTextOf(error_node, "kind") |
| 200 | self._backtraces = [] |
| 201 | self._suppression = None |
| 202 | self._commandline = commandline |
| 203 | self._testcase = testcase |
| 204 | self._additional = [] |
| 205 | |
| 206 | # Iterate through the nodes, parsing <what|auxwhat><stack> pairs. |
| 207 | description = None |
| 208 | for node in error_node.childNodes: |
| 209 | if node.localName == "what" or node.localName == "auxwhat": |
| 210 | description = "".join([n.data for n in node.childNodes |
| 211 | if n.nodeType == n.TEXT_NODE]) |
| 212 | elif node.localName == "xwhat": |
| 213 | description = getTextOf(node, "text") |
| 214 | elif node.localName == "stack": |
| 215 | assert description |
| 216 | self._backtraces.append([description, gatherFrames(node, source_dir)]) |
| 217 | description = None |
| 218 | elif node.localName == "origin": |
| 219 | description = getTextOf(node, "what") |
| 220 | stack = node.getElementsByTagName("stack")[0] |
| 221 | frames = gatherFrames(stack, source_dir) |
| 222 | self._backtraces.append([description, frames]) |
| 223 | description = None |
| 224 | stack = None |
| 225 | frames = None |
| 226 | elif description and node.localName != None: |
| 227 | # The lastest description has no stack, e.g. "Address 0x28 is unknown" |
| 228 | self._additional.append(description) |
| 229 | description = None |
| 230 | |
| 231 | if node.localName == "suppression": |
| 232 | self._suppression = getCDATAOf(node, "rawtext"); |
| 233 | |
| 234 | def __str__(self): |
| 235 | ''' Pretty print the type and backtrace(s) of this specific error, |
| 236 | including suppression (which is just a mangled backtrace).''' |
| 237 | output = "" |
| 238 | output += "\n" # Make sure the ### is at the beginning of line. |
| 239 | output += "### BEGIN MEMORY TOOL REPORT (error hash=#%016X#)\n" % \ |
| 240 | self.ErrorHash() |
| 241 | if (self._commandline): |
| 242 | output += self._commandline + "\n" |
| 243 | |
| 244 | output += self._kind + "\n" |
| 245 | for backtrace in self._backtraces: |
| 246 | output += backtrace[0] + "\n" |
| 247 | filter = subprocess.Popen("c++filt -n", stdin=subprocess.PIPE, |
| 248 | stdout=subprocess.PIPE, |
| 249 | stderr=subprocess.STDOUT, |
| 250 | shell=True, |
| 251 | close_fds=True) |
| 252 | buf = "" |
| 253 | for frame in backtrace[1]: |
| 254 | buf += (frame[FUNCTION_NAME] or frame[INSTRUCTION_POINTER]) + "\n" |
| 255 | (stdoutbuf, stderrbuf) = filter.communicate(buf.encode('latin-1')) |
| 256 | demangled_names = stdoutbuf.split("\n") |
| 257 | |
| 258 | i = 0 |
| 259 | for frame in backtrace[1]: |
| 260 | output += (" " + demangled_names[i]) |
| 261 | i = i + 1 |
| 262 | |
| 263 | global TheAddressTable |
| 264 | if TheAddressTable != None and frame[SRC_FILE_DIR] == "": |
| 265 | # Try using gdb |
| 266 | foo = TheAddressTable.GetFileLine(frame[OBJECT_FILE], |
| 267 | frame[INSTRUCTION_POINTER]) |
| 268 | if foo[0] != None: |
| 269 | output += (" (" + foo[0] + ":" + foo[1] + ")") |
| 270 | elif frame[SRC_FILE_DIR] != "": |
| 271 | output += (" (" + frame[SRC_FILE_DIR] + "/" + frame[SRC_FILE_NAME] + |
| 272 | ":" + frame[SRC_LINE] + ")") |
| 273 | else: |
| 274 | output += " (" + frame[OBJECT_FILE] + ")" |
| 275 | output += "\n" |
| 276 | |
| 277 | for additional in self._additional: |
| 278 | output += additional + "\n" |
| 279 | |
| 280 | assert self._suppression != None, "Your Valgrind doesn't generate " \ |
| 281 | "suppressions - is it too old?" |
| 282 | |
| 283 | if self._testcase: |
| 284 | output += "The report came from the `%s` test.\n" % self._testcase |
| 285 | output += "Suppression (error hash=#%016X#):\n" % self.ErrorHash() |
| 286 | output += (" For more info on using suppressions see " |
| 287 | "http://dev.chromium.org/developers/tree-sheriffs/sheriff-details-chromium/memory-sheriff#TOC-Suppressing-memory-reports") |
| 288 | |
| 289 | # Widen suppression slightly to make portable between mac and linux |
| 290 | # TODO(timurrrr): Oops, these transformations should happen |
| 291 | # BEFORE calculating the hash! |
| 292 | supp = self._suppression; |
| 293 | supp = supp.replace("fun:_Znwj", "fun:_Znw*") |
| 294 | supp = supp.replace("fun:_Znwm", "fun:_Znw*") |
| 295 | supp = supp.replace("fun:_Znaj", "fun:_Zna*") |
| 296 | supp = supp.replace("fun:_Znam", "fun:_Zna*") |
| 297 | |
| 298 | # Make suppressions even less platform-dependent. |
| 299 | for sz in [1, 2, 4, 8]: |
| 300 | supp = supp.replace("Memcheck:Addr%d" % sz, "Memcheck:Unaddressable") |
| 301 | supp = supp.replace("Memcheck:Value%d" % sz, "Memcheck:Uninitialized") |
| 302 | supp = supp.replace("Memcheck:Cond", "Memcheck:Uninitialized") |
| 303 | |
| 304 | # Split into lines so we can enforce length limits |
| 305 | supplines = supp.split("\n") |
| 306 | supp = None # to avoid re-use |
| 307 | |
| 308 | # Truncate at line 26 (VG_MAX_SUPP_CALLERS plus 2 for name and type) |
| 309 | # or at the first 'boring' caller. |
| 310 | # (https://bugs.kde.org/show_bug.cgi?id=199468 proposes raising |
| 311 | # VG_MAX_SUPP_CALLERS, but we're probably fine with it as is.) |
| 312 | newlen = min(26, len(supplines)); |
| 313 | |
| 314 | # Drop boring frames and all the following. |
| 315 | enough_frames = False |
| 316 | for frameno in range(newlen): |
| 317 | for boring_caller in _BORING_CALLERS: |
| 318 | if re.match("^ +fun:%s$" % boring_caller, supplines[frameno]): |
| 319 | newlen = frameno |
| 320 | enough_frames = True |
| 321 | break |
| 322 | if enough_frames: |
| 323 | break |
| 324 | if (len(supplines) > newlen): |
| 325 | supplines = supplines[0:newlen] |
| 326 | supplines.append("}") |
| 327 | |
| 328 | for frame in range(len(supplines)): |
| 329 | # Replace the always-changing anonymous namespace prefix with "*". |
| 330 | m = re.match("( +fun:)_ZN.*_GLOBAL__N_.*\.cc_" + |
| 331 | "[0-9a-fA-F]{8}_[0-9a-fA-F]{8}(.*)", |
| 332 | supplines[frame]) |
| 333 | if m: |
| 334 | supplines[frame] = "*".join(m.groups()) |
| 335 | |
| 336 | output += "\n".join(supplines) + "\n" |
| 337 | output += "### END MEMORY TOOL REPORT (error hash=#%016X#)\n" % \ |
| 338 | self.ErrorHash() |
| 339 | |
| 340 | return output |
| 341 | |
| 342 | def UniqueString(self): |
| 343 | ''' String to use for object identity. Don't print this, use str(obj) |
| 344 | instead.''' |
| 345 | rep = self._kind + " " |
| 346 | for backtrace in self._backtraces: |
| 347 | for frame in backtrace[1]: |
| 348 | rep += frame[FUNCTION_NAME] |
| 349 | |
| 350 | if frame[SRC_FILE_DIR] != "": |
| 351 | rep += frame[SRC_FILE_DIR] + "/" + frame[SRC_FILE_NAME] |
| 352 | else: |
| 353 | rep += frame[OBJECT_FILE] |
| 354 | |
| 355 | return rep |
| 356 | |
| 357 | # This is a device-independent hash identifying the suppression. |
| 358 | # By printing out this hash we can find duplicate reports between tests and |
| 359 | # different shards running on multiple buildbots |
| 360 | def ErrorHash(self): |
| 361 | return int(hashlib.md5(self.UniqueString()).hexdigest()[:16], 16) |
| 362 | |
| 363 | def __hash__(self): |
| 364 | return hash(self.UniqueString()) |
| 365 | def __eq__(self, rhs): |
| 366 | return self.UniqueString() == rhs |
| 367 | |
| 368 | def log_is_finished(f, force_finish): |
| 369 | f.seek(0) |
| 370 | prev_line = "" |
| 371 | while True: |
| 372 | line = f.readline() |
| 373 | if line == "": |
| 374 | if not force_finish: |
| 375 | return False |
| 376 | # Okay, the log is not finished but we can make it up to be parseable: |
| 377 | if prev_line.strip() in ["</error>", "</errorcounts>", "</status>"]: |
| 378 | f.write("</valgrindoutput>\n") |
| 379 | return True |
| 380 | return False |
| 381 | if '</valgrindoutput>' in line: |
| 382 | # Valgrind often has garbage after </valgrindoutput> upon crash. |
| 383 | f.truncate() |
| 384 | return True |
| 385 | prev_line = line |
| 386 | |
| 387 | class MemcheckAnalyzer: |
| 388 | ''' Given a set of Valgrind XML files, parse all the errors out of them, |
| 389 | unique them and output the results.''' |
| 390 | |
| 391 | SANITY_TEST_SUPPRESSIONS = { |
| 392 | "Memcheck sanity test 01 (memory leak).": 1, |
| 393 | "Memcheck sanity test 02 (malloc/read left).": 1, |
| 394 | "Memcheck sanity test 03 (malloc/read right).": 1, |
| 395 | "Memcheck sanity test 04 (malloc/write left).": 1, |
| 396 | "Memcheck sanity test 05 (malloc/write right).": 1, |
| 397 | "Memcheck sanity test 06 (new/read left).": 1, |
| 398 | "Memcheck sanity test 07 (new/read right).": 1, |
| 399 | "Memcheck sanity test 08 (new/write left).": 1, |
| 400 | "Memcheck sanity test 09 (new/write right).": 1, |
| 401 | "Memcheck sanity test 10 (write after free).": 1, |
| 402 | "Memcheck sanity test 11 (write after delete).": 1, |
| 403 | "Memcheck sanity test 12 (array deleted without []).": 1, |
| 404 | "Memcheck sanity test 13 (single element deleted with []).": 1, |
| 405 | "Memcheck sanity test 14 (malloc/read uninit).": 1, |
| 406 | "Memcheck sanity test 15 (new/read uninit).": 1, |
| 407 | } |
| 408 | |
| 409 | # Max time to wait for memcheck logs to complete. |
| 410 | LOG_COMPLETION_TIMEOUT = 180.0 |
| 411 | |
| 412 | def __init__(self, source_dir, show_all_leaks=False, use_gdb=False): |
| 413 | '''Create a parser for Memcheck logs. |
| 414 | |
| 415 | Args: |
| 416 | source_dir: Path to top of source tree for this build |
| 417 | show_all_leaks: Whether to show even less important leaks |
| 418 | use_gdb: Whether to use gdb to resolve source filenames and line numbers |
| 419 | in the report stacktraces |
| 420 | ''' |
| 421 | self._source_dir = source_dir |
| 422 | self._show_all_leaks = show_all_leaks |
| 423 | self._use_gdb = use_gdb |
| 424 | |
| 425 | # Contains the set of unique errors |
| 426 | self._errors = set() |
| 427 | |
| 428 | # Contains the time when the we started analyzing the first log file. |
| 429 | # This variable is used to skip incomplete logs after some timeout. |
| 430 | self._analyze_start_time = None |
| 431 | |
| 432 | |
| 433 | def Report(self, files, testcase, check_sanity=False): |
| 434 | '''Reads in a set of files and prints Memcheck report. |
| 435 | |
| 436 | Args: |
| 437 | files: A list of filenames. |
| 438 | check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS |
| 439 | ''' |
| 440 | # Beyond the detailed errors parsed by ValgrindError above, |
| 441 | # the xml file contain records describing suppressions that were used: |
| 442 | # <suppcounts> |
| 443 | # <pair> |
| 444 | # <count>28</count> |
| 445 | # <name>pango_font_leak_todo</name> |
| 446 | # </pair> |
| 447 | # <pair> |
| 448 | # <count>378</count> |
| 449 | # <name>bug_13243</name> |
| 450 | # </pair> |
| 451 | # </suppcounts |
| 452 | # Collect these and print them at the end. |
| 453 | # |
| 454 | # With our patch for https://bugs.kde.org/show_bug.cgi?id=205000 in, |
| 455 | # the file also includes records of the form |
| 456 | # <load_obj><obj>/usr/lib/libgcc_s.1.dylib</obj><ip>0x27000</ip></load_obj> |
| 457 | # giving the filename and load address of each binary that was mapped |
| 458 | # into the process. |
| 459 | |
| 460 | global TheAddressTable |
| 461 | if self._use_gdb: |
| 462 | TheAddressTable = gdb_helper.AddressTable() |
| 463 | else: |
| 464 | TheAddressTable = None |
| 465 | cur_report_errors = set() |
| 466 | suppcounts = defaultdict(int) |
| 467 | badfiles = set() |
| 468 | |
| 469 | if self._analyze_start_time == None: |
| 470 | self._analyze_start_time = time.time() |
| 471 | start_time = self._analyze_start_time |
| 472 | |
| 473 | parse_failed = False |
| 474 | for file in files: |
| 475 | # Wait up to three minutes for valgrind to finish writing all files, |
| 476 | # but after that, just skip incomplete files and warn. |
| 477 | f = open(file, "r+") |
| 478 | pid = re.match(".*\.([0-9]+)$", file) |
| 479 | if pid: |
| 480 | pid = pid.groups()[0] |
| 481 | found = False |
| 482 | running = True |
| 483 | firstrun = True |
| 484 | skip = False |
| 485 | origsize = os.path.getsize(file) |
| 486 | while (running and not found and not skip and |
| 487 | (firstrun or |
| 488 | ((time.time() - start_time) < self.LOG_COMPLETION_TIMEOUT))): |
| 489 | firstrun = False |
| 490 | f.seek(0) |
| 491 | if pid: |
| 492 | # Make sure the process is still running so we don't wait for |
| 493 | # 3 minutes if it was killed. See http://crbug.com/17453 |
| 494 | ps_out = subprocess.Popen("ps p %s" % pid, shell=True, |
| 495 | stdout=subprocess.PIPE).stdout |
| 496 | if len(ps_out.readlines()) < 2: |
| 497 | running = False |
| 498 | else: |
| 499 | skip = True |
| 500 | running = False |
| 501 | found = log_is_finished(f, False) |
| 502 | if not running and not found: |
| 503 | logging.warn("Valgrind process PID = %s is not running but its " |
| 504 | "XML log has not been finished correctly.\n" |
| 505 | "Make it up by adding some closing tags manually." % pid) |
| 506 | found = log_is_finished(f, not running) |
| 507 | if running and not found: |
| 508 | time.sleep(1) |
| 509 | f.close() |
| 510 | if not found: |
| 511 | badfiles.add(file) |
| 512 | else: |
| 513 | newsize = os.path.getsize(file) |
| 514 | if origsize > newsize+1: |
| 515 | logging.warn(str(origsize - newsize) + |
| 516 | " bytes of junk were after </valgrindoutput> in %s!" % |
| 517 | file) |
| 518 | try: |
| 519 | parsed_file = parse(file); |
| 520 | except ExpatError, e: |
| 521 | parse_failed = True |
| 522 | logging.warn("could not parse %s: %s" % (file, e)) |
| 523 | lineno = e.lineno - 1 |
| 524 | context_lines = 5 |
| 525 | context_start = max(0, lineno - context_lines) |
| 526 | context_end = lineno + context_lines + 1 |
| 527 | context_file = open(file, "r") |
| 528 | for i in range(0, context_start): |
| 529 | context_file.readline() |
| 530 | for i in range(context_start, context_end): |
| 531 | context_data = context_file.readline().rstrip() |
| 532 | if i != lineno: |
| 533 | logging.warn(" %s" % context_data) |
| 534 | else: |
| 535 | logging.warn("> %s" % context_data) |
| 536 | context_file.close() |
| 537 | continue |
| 538 | if TheAddressTable != None: |
| 539 | load_objs = parsed_file.getElementsByTagName("load_obj") |
| 540 | for load_obj in load_objs: |
| 541 | obj = getTextOf(load_obj, "obj") |
| 542 | ip = getTextOf(load_obj, "ip") |
| 543 | TheAddressTable.AddBinaryAt(obj, ip) |
| 544 | |
| 545 | commandline = None |
| 546 | preamble = parsed_file.getElementsByTagName("preamble")[0]; |
| 547 | for node in preamble.getElementsByTagName("line"): |
| 548 | if node.localName == "line": |
| 549 | for x in node.childNodes: |
| 550 | if x.nodeType == node.TEXT_NODE and "Command" in x.data: |
| 551 | commandline = x.data |
| 552 | break |
| 553 | |
| 554 | raw_errors = parsed_file.getElementsByTagName("error") |
| 555 | for raw_error in raw_errors: |
| 556 | # Ignore "possible" leaks for now by default. |
| 557 | if (self._show_all_leaks or |
| 558 | getTextOf(raw_error, "kind") != "Leak_PossiblyLost"): |
| 559 | error = ValgrindError(self._source_dir, |
| 560 | raw_error, commandline, testcase) |
| 561 | if error not in cur_report_errors: |
| 562 | # We haven't seen such errors doing this report yet... |
| 563 | if error in self._errors: |
| 564 | # ... but we saw it in earlier reports, e.g. previous UI test |
| 565 | cur_report_errors.add("This error was already printed in " |
| 566 | "some other test, see 'hash=#%016X#'" % \ |
| 567 | error.ErrorHash()) |
| 568 | else: |
| 569 | # ... and we haven't seen it in other tests as well |
| 570 | self._errors.add(error) |
| 571 | cur_report_errors.add(error) |
| 572 | |
| 573 | suppcountlist = parsed_file.getElementsByTagName("suppcounts") |
| 574 | if len(suppcountlist) > 0: |
| 575 | suppcountlist = suppcountlist[0] |
| 576 | for node in suppcountlist.getElementsByTagName("pair"): |
| 577 | count = getTextOf(node, "count"); |
| 578 | name = getTextOf(node, "name"); |
| 579 | suppcounts[name] += int(count) |
| 580 | |
| 581 | if len(badfiles) > 0: |
| 582 | logging.warn("valgrind didn't finish writing %d files?!" % len(badfiles)) |
| 583 | for file in badfiles: |
| 584 | logging.warn("Last 20 lines of %s :" % file) |
| 585 | os.system("tail -n 20 '%s' 1>&2" % file) |
| 586 | |
| 587 | if parse_failed: |
| 588 | logging.error("FAIL! Couldn't parse Valgrind output file") |
| 589 | return -2 |
| 590 | |
| 591 | common.PrintUsedSuppressionsList(suppcounts) |
| 592 | |
| 593 | retcode = 0 |
| 594 | if cur_report_errors: |
| 595 | logging.error("FAIL! There were %s errors: " % len(cur_report_errors)) |
| 596 | |
| 597 | if TheAddressTable != None: |
| 598 | TheAddressTable.ResolveAll() |
| 599 | |
| 600 | for error in cur_report_errors: |
| 601 | logging.error(error) |
| 602 | |
| 603 | retcode = -1 |
| 604 | |
| 605 | # Report tool's insanity even if there were errors. |
| 606 | if check_sanity: |
| 607 | remaining_sanity_supp = MemcheckAnalyzer.SANITY_TEST_SUPPRESSIONS |
| 608 | for (name, count) in suppcounts.iteritems(): |
| 609 | # Workaround for http://crbug.com/334074 |
| 610 | if (name in remaining_sanity_supp and |
| 611 | remaining_sanity_supp[name] <= count): |
| 612 | del remaining_sanity_supp[name] |
| 613 | if remaining_sanity_supp: |
| 614 | logging.error("FAIL! Sanity check failed!") |
| 615 | logging.info("The following test errors were not handled: ") |
| 616 | for (name, count) in remaining_sanity_supp.iteritems(): |
| 617 | logging.info(" * %dx %s" % (count, name)) |
| 618 | retcode = -3 |
| 619 | |
| 620 | if retcode != 0: |
| 621 | return retcode |
| 622 | |
| 623 | logging.info("PASS! No errors found!") |
| 624 | return 0 |
| 625 | |
| 626 | |
| 627 | def _main(): |
| 628 | '''For testing only. The MemcheckAnalyzer class should be imported instead.''' |
| 629 | parser = optparse.OptionParser("usage: %prog [options] <files to analyze>") |
| 630 | parser.add_option("", "--source-dir", |
| 631 | help="path to top of source tree for this build" |
| 632 | "(used to normalize source paths in baseline)") |
| 633 | |
| 634 | (options, args) = parser.parse_args() |
| 635 | if len(args) == 0: |
| 636 | parser.error("no filename specified") |
| 637 | filenames = args |
| 638 | |
| 639 | analyzer = MemcheckAnalyzer(options.source_dir, use_gdb=True) |
| 640 | return analyzer.Report(filenames, None) |
| 641 | |
| 642 | |
| 643 | if __name__ == "__main__": |
| 644 | sys.exit(_main()) |