• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2013 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""stack symbolizes native crash dumps."""
18
19import collections
20import functools
21import os
22import pathlib
23import re
24import subprocess
25import symbol
26import tempfile
27import unittest
28
29import example_crashes
30
31def ConvertTrace(lines):
32  tracer = TraceConverter()
33  print("Reading symbols from", symbol.SYMBOLS_DIR)
34  tracer.ConvertTrace(lines)
35
36class TraceConverter:
37  process_info_line = re.compile(r"(pid: [0-9]+, tid: [0-9]+.*)")
38  revision_line = re.compile(r"(Revision: '(.*)')")
39  signal_line = re.compile(r"(signal [0-9]+ \(.*\).*)")
40  abort_message_line = re.compile(r"(Abort message: '.*')")
41  thread_line = re.compile(r"(.*)(--- ){15}---")
42  dalvik_jni_thread_line = re.compile("(\".*\" prio=[0-9]+ tid=[0-9]+ NATIVE.*)")
43  dalvik_native_thread_line = re.compile("(\".*\" sysTid=[0-9]+ nice=[0-9]+.*)")
44  register_line = re.compile("$a")
45  trace_line = re.compile("$a")
46  sanitizer_trace_line = re.compile("$a")
47  value_line = re.compile("$a")
48  code_line = re.compile("$a")
49  zipinfo_central_directory_line = re.compile(r"Central\s+directory\s+entry")
50  zipinfo_central_info_match = re.compile(
51      r"^\s*(\S+)$\s*offset of local header from start of archive:\s*(\d+)"
52      r".*^\s*compressed size:\s+(\d+)", re.M | re.S)
53  unreachable_line = re.compile(r"((\d+ bytes in \d+ unreachable allocations)|"
54                                r"(\d+ bytes unreachable at [0-9a-f]+)|"
55                                r"(referencing \d+ unreachable bytes in \d+ allocation(s)?)|"
56                                r"(and \d+ similar unreachable bytes in \d+ allocation(s)?))")
57  trace_lines = []
58  value_lines = []
59  last_frame = -1
60  width = "{8}"
61  spacing = ""
62  apk_info = dict()
63
64  register_names = {
65    "arm": "r0|r1|r2|r3|r4|r5|r6|r7|r8|r9|sl|fp|ip|sp|lr|pc|cpsr",
66    "arm64": "x0|x1|x2|x3|x4|x5|x6|x7|x8|x9|x10|x11|x12|x13|x14|x15|x16|x17|x18|x19|x20|x21|x22|x23|x24|x25|x26|x27|x28|x29|x30|sp|pc|pstate",
67    "mips": "zr|at|v0|v1|a0|a1|a2|a3|t0|t1|t2|t3|t4|t5|t6|t7|s0|s1|s2|s3|s4|s5|s6|s7|t8|t9|k0|k1|gp|sp|s8|ra|hi|lo|bva|epc",
68    "mips64": "zr|at|v0|v1|a0|a1|a2|a3|a4|a5|a6|a7|t0|t1|t2|t3|s0|s1|s2|s3|s4|s5|s6|s7|t8|t9|k0|k1|gp|sp|s8|ra|hi|lo|bva|epc",
69    "x86": "eax|ebx|ecx|edx|esi|edi|x?cs|x?ds|x?es|x?fs|x?ss|eip|ebp|esp|flags",
70    "x86_64": "rax|rbx|rcx|rdx|rsi|rdi|r8|r9|r10|r11|r12|r13|r14|r15|cs|ss|rip|rbp|rsp|eflags",
71  }
72
73  # We use the "file" command line tool to extract BuildId from ELF files.
74  ElfInfo = collections.namedtuple("ElfInfo", ["bitness", "build_id"])
75  file_tool_output = re.compile(r"ELF (?P<bitness>32|64)-bit .*"
76                                r"BuildID(\[.*\])?=(?P<build_id>[0-9a-f]+)")
77
78  def UpdateAbiRegexes(self):
79    if symbol.ARCH == "arm64" or symbol.ARCH == "mips64" or symbol.ARCH == "x86_64":
80      self.width = "{16}"
81      self.spacing = "        "
82    else:
83      self.width = "{8}"
84      self.spacing = ""
85
86    self.register_line = re.compile("(([ ]*\\b(" + self.register_names[symbol.ARCH] + ")\\b +[0-9a-f]" + self.width + "){2,5})")
87
88    # Note that both trace and value line matching allow for variable amounts of
89    # whitespace (e.g. \t). This is because the we want to allow for the stack
90    # tool to operate on AndroidFeedback provided system logs. AndroidFeedback
91    # strips out double spaces that are found in tombsone files and logcat output.
92    #
93    # Examples of matched trace lines include lines from tombstone files like:
94    #   #00  pc 001cf42e  /data/data/com.my.project/lib/libmyproject.so
95    #
96    # Or lines from AndroidFeedback crash report system logs like:
97    #   03-25 00:51:05.520 I/DEBUG ( 65): #00 pc 001cf42e /data/data/com.my.project/lib/libmyproject.so
98    # Please note the spacing differences.
99    self.trace_line = re.compile(
100        r".*"                                                 # Random start stuff.
101        r"\#(?P<frame>[0-9]+)"                                # Frame number.
102        r"[ \t]+..[ \t]+"                                     # (space)pc(space).
103        r"(?P<offset>[0-9a-f]" + self.width + ")[ \t]+"       # Offset (hex number given without
104                                                              #         0x prefix).
105        r"(?P<dso>\[[^\]]+\]|[^\r\n \t]*)"                    # Library name.
106        r"( \(offset (?P<so_offset>0x[0-9a-fA-F]+)\))?"       # Offset into the file to find the start of the shared so.
107        r"(?P<symbolpresent> \((?P<symbol>.*?)\))?"           # Is the symbol there? (non-greedy)
108        r"( \(BuildId: (?P<build_id>.*)\))?"                  # Optional build-id of the ELF file.
109        r"[ \t]*$")                                           # End of line (to expand non-greedy match).
110                                                              # pylint: disable-msg=C6310
111    # Sanitizer output. This is different from debuggerd output, and it is easier to handle this as
112    # its own regex. Example:
113    # 08-19 05:29:26.283   397   403 I         :     #0 0xb6a15237  (/system/lib/libclang_rt.asan-arm-android.so+0x4f237)
114    self.sanitizer_trace_line = re.compile(
115        r".*"                                                 # Random start stuff.
116        r"\#(?P<frame>[0-9]+)"                                # Frame number.
117        r"[ \t]+0x[0-9a-f]+[ \t]+"                            # PC, not interesting to us.
118        r"\("                                                 # Opening paren.
119        r"(?P<dso>[^+]+)"                                     # Library name.
120        r"\+"                                                 # '+'
121        r"0x(?P<offset>[0-9a-f]+)"                            # Offset (hex number given with
122                                                              #         0x prefix).
123        r"\)")                                                # Closing paren.
124                                                              # pylint: disable-msg=C6310
125    # Examples of matched value lines include:
126    #   bea4170c  8018e4e9  /data/data/com.my.project/lib/libmyproject.so
127    #   bea4170c  8018e4e9  /data/data/com.my.project/lib/libmyproject.so (symbol)
128    #   03-25 00:51:05.530 I/DEBUG ( 65): bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so
129    # Again, note the spacing differences.
130    self.value_line = re.compile(r"(.*)([0-9a-f]" + self.width + r")[ \t]+([0-9a-f]" + self.width + r")[ \t]+([^\r\n \t]*)( \((.*)\))?")
131    # Lines from 'code around' sections of the output will be matched before
132    # value lines because otheriwse the 'code around' sections will be confused as
133    # value lines.
134    #
135    # Examples include:
136    #   801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8
137    #   03-25 00:51:05.530 I/DEBUG ( 65): 801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8
138    self.code_line = re.compile(r"(.*)[ \t]*[a-f0-9]" + self.width +
139                                r"[ \t]*[a-f0-9]" + self.width +
140                                r"[ \t]*[a-f0-9]" + self.width +
141                                r"[ \t]*[a-f0-9]" + self.width +
142                                r"[ \t]*[a-f0-9]" + self.width +
143                                r"[ \t]*[ \r\n]")  # pylint: disable-msg=C6310
144
145  def CleanLine(self, ln):
146    # AndroidFeedback adds zero width spaces into its crash reports. These
147    # should be removed or the regular expresssions will fail to match.
148    return ln.encode().decode(encoding='utf8', errors='ignore')
149
150  def PrintTraceLines(self, trace_lines):
151    """Print back trace."""
152    maxlen = max(len(tl[1]) for tl in trace_lines)
153    print("\nStack Trace:")
154    print("  RELADDR   " + self.spacing + "FUNCTION".ljust(maxlen) + "  FILE:LINE")
155    for tl in self.trace_lines:
156      (addr, symbol_with_offset, location) = tl
157      print("  %8s  %s  %s" % (addr, symbol_with_offset.ljust(maxlen), location))
158
159  def PrintValueLines(self, value_lines):
160    """Print stack data values."""
161    maxlen = max(len(tl[2]) for tl in self.value_lines)
162    print("\nStack Data:")
163    print("  ADDR      " + self.spacing + "VALUE     " + "FUNCTION".ljust(maxlen) + "  FILE:LINE")
164    for vl in self.value_lines:
165      (addr, value, symbol_with_offset, location) = vl
166      print("  %8s  %8s  %s  %s" % (addr, value, symbol_with_offset.ljust(maxlen), location))
167
168  def PrintOutput(self, trace_lines, value_lines):
169    if self.trace_lines:
170      self.PrintTraceLines(self.trace_lines)
171    if self.value_lines:
172      self.PrintValueLines(self.value_lines)
173
174  def PrintDivider(self):
175    print("\n-----------------------------------------------------\n")
176
177  def DeleteApkTmpFiles(self):
178    for _, _, tmp_files in self.apk_info.values():
179      for tmp_file in tmp_files.values():
180        os.unlink(tmp_file)
181
182  def ConvertTrace(self, lines):
183    lines = [self.CleanLine(line) for line in lines]
184    try:
185      if not symbol.ARCH:
186        symbol.SetAbi(lines)
187      self.UpdateAbiRegexes()
188      for line in lines:
189        self.ProcessLine(line)
190      self.PrintOutput(self.trace_lines, self.value_lines)
191    finally:
192      # Delete any temporary files created while processing the lines.
193      self.DeleteApkTmpFiles()
194
195  def MatchTraceLine(self, line):
196    match = self.trace_line.match(line)
197    if match:
198      return {"frame": match.group("frame"),
199              "offset": match.group("offset"),
200              "so_offset": match.group("so_offset"),
201              "dso": match.group("dso"),
202              "symbol_present": bool(match.group("symbolpresent")),
203              "symbol_name": match.group("symbol"),
204              "build_id": match.group("build_id")}
205    match = self.sanitizer_trace_line.match(line)
206    if match:
207      return {"frame": match.group("frame"),
208              "offset": match.group("offset"),
209              "so_offset": None,
210              "dso": match.group("dso"),
211              "symbol_present": False,
212              "symbol_name": None,
213              "build_id": None}
214    return None
215
216  def ExtractLibFromApk(self, apk, shared_lib_name):
217    # Create a temporary file containing the shared library from the apk.
218    tmp_file = None
219    try:
220      tmp_fd, tmp_file = tempfile.mkstemp()
221      if subprocess.call(["unzip", "-p", apk, shared_lib_name], stdout=tmp_fd) == 0:
222        os.close(tmp_fd)
223        shared_file = tmp_file
224        tmp_file = None
225        return shared_file
226    finally:
227      if tmp_file:
228        os.close(tmp_fd)
229        os.unlink(tmp_file)
230    return None
231
232  def ProcessCentralInfo(self, offset_list, central_info):
233    match = self.zipinfo_central_info_match.search(central_info)
234    if not match:
235      raise Exception("Cannot find all info from zipinfo\n" + central_info)
236    name = match.group(1)
237    start = int(match.group(2))
238    end = start + int(match.group(3))
239
240    offset_list.append([name, start, end])
241    return name, start, end
242
243  def GetLibFromApk(self, apk, offset):
244    # Convert the string to hex.
245    offset = int(offset, 16)
246
247    # Check if we already have information about this offset.
248    if apk in self.apk_info:
249      apk_full_path, offset_list, tmp_files = self.apk_info[apk]
250      for file_name, start, end in offset_list:
251        if offset >= start and offset < end:
252          if file_name in tmp_files:
253            return file_name, tmp_files[file_name]
254          tmp_file = self.ExtractLibFromApk(apk_full_path, file_name)
255          if tmp_file:
256            tmp_files[file_name] = tmp_file
257            return file_name, tmp_file
258          break
259      return None, None
260
261    if not "ANDROID_PRODUCT_OUT" in os.environ:
262      print("ANDROID_PRODUCT_OUT environment variable not set.")
263      return None, None
264    out_dir = os.environ["ANDROID_PRODUCT_OUT"]
265    if not os.path.exists(out_dir):
266      print("ANDROID_PRODUCT_OUT", out_dir, "does not exist.")
267      return None, None
268    if apk.startswith("/"):
269      apk_full_path = out_dir + apk
270    else:
271      apk_full_path = os.path.join(out_dir, apk)
272    if not os.path.exists(apk_full_path):
273      print("Cannot find apk", apk)
274      return None, None
275
276    cmd = subprocess.Popen(["zipinfo", "-v", apk_full_path], stdout=subprocess.PIPE,
277                           encoding='utf8')
278    # Find the first central info marker.
279    for line in cmd.stdout:
280      if self.zipinfo_central_directory_line.search(line):
281        break
282
283    central_info = ""
284    file_name = None
285    offset_list = []
286    for line in cmd.stdout:
287      match = self.zipinfo_central_directory_line.search(line)
288      if match:
289        cur_name, start, end = self.ProcessCentralInfo(offset_list, central_info)
290        if not file_name and offset >= start and offset < end:
291          file_name = cur_name
292        central_info = ""
293      else:
294        central_info += line
295    if central_info:
296      cur_name, start, end = self.ProcessCentralInfo(offset_list, central_info)
297      if not file_name and offset >= start and offset < end:
298        file_name = cur_name
299
300    # Make sure the offset_list is sorted, the zip file does not guarantee
301    # that the entries are in order.
302    offset_list = sorted(offset_list, key=lambda entry: entry[1])
303
304    # Save the information from the zip.
305    tmp_files = dict()
306    self.apk_info[apk] = [apk_full_path, offset_list, tmp_files]
307    if not file_name:
308      return None, None
309    tmp_shared_lib = self.ExtractLibFromApk(apk_full_path, file_name)
310    if tmp_shared_lib:
311      tmp_files[file_name] = tmp_shared_lib
312      return file_name, tmp_shared_lib
313    return None, None
314
315  # Find all files in the symbols directory and group them by basename (without directory).
316  @functools.lru_cache(maxsize=None)
317  def GlobSymbolsDir(self, symbols_dir):
318    files_by_basename = {}
319    for path in sorted(pathlib.Path(symbols_dir).glob("**/*")):
320      files_by_basename.setdefault(path.name, []).append(path)
321    return files_by_basename
322
323  # Use the "file" command line tool to find the bitness and build_id of given ELF file.
324  @functools.lru_cache(maxsize=None)
325  def GetLibraryInfo(self, lib):
326    stdout = subprocess.check_output(["file", lib], text=True)
327    match = self.file_tool_output.search(stdout)
328    if match:
329      return self.ElfInfo(bitness=match.group("bitness"), build_id=match.group("build_id"))
330    return None
331
332  # Search for a library with the given basename and build_id anywhere in the symbols directory.
333  @functools.lru_cache(maxsize=None)
334  def GetLibraryByBuildId(self, symbols_dir, basename, build_id):
335    for candidate in self.GlobSymbolsDir(symbols_dir).get(basename, []):
336      info = self.GetLibraryInfo(candidate)
337      if info and info.build_id == build_id:
338        return "/" + str(candidate.relative_to(symbols_dir))
339    return None
340
341  def GetLibPath(self, lib):
342    symbol_dir = symbol.SYMBOLS_DIR
343    if os.path.isfile(symbol_dir + lib):
344      return lib
345
346    # When using atest, test paths are different between the out/ directory
347    # and device. Apply fixups.
348    if not lib.startswith("/data/local/tests/") and not lib.startswith("/data/local/tmp/"):
349      print("WARNING: Cannot find %s in symbol directory" % lib)
350      return lib
351
352    test_name = lib.rsplit("/", 1)[-1]
353    test_dir = "/data/nativetest"
354    test_dir_bitness = ""
355    if symbol.ARCH.endswith("64"):
356      bitness = "64"
357      test_dir_bitness = "64"
358    else:
359      bitness = "32"
360
361    # Unfortunately, the location of the real symbol file is not
362    # standardized, so we need to go hunting for it.
363
364    # This is in vendor, look for the value in:
365    #   /data/nativetest{64}/vendor/test_name/test_name
366    if lib.startswith("/data/local/tests/vendor/"):
367       lib_path = os.path.join(test_dir + test_dir_bitness, "vendor", test_name, test_name)
368       if os.path.isfile(symbol_dir + lib_path):
369         return lib_path
370
371    # Look for the path in:
372    #   /data/nativetest{64}/test_name/test_name
373    lib_path = os.path.join(test_dir + test_dir_bitness, test_name, test_name)
374    if os.path.isfile(symbol_dir + lib_path):
375      return lib_path
376
377    # CtsXXX tests are in really non-standard locations try:
378    #  /data/nativetest/{test_name}
379    lib_path = os.path.join(test_dir, test_name)
380    if os.path.isfile(symbol_dir + lib_path):
381      return lib_path
382    # Try:
383    #   /data/nativetest/{test_name}{32|64}
384    lib_path += bitness
385    if os.path.isfile(symbol_dir + lib_path):
386      return lib_path
387
388    # Cannot find location, give up and return the original path
389    print("WARNING: Cannot find %s in symbol directory" % lib)
390    return lib
391
392
393  def ProcessLine(self, line):
394    ret = False
395    process_header = self.process_info_line.search(line)
396    signal_header = self.signal_line.search(line)
397    abort_message_header = self.abort_message_line.search(line)
398    thread_header = self.thread_line.search(line)
399    register_header = self.register_line.search(line)
400    revision_header = self.revision_line.search(line)
401    dalvik_jni_thread_header = self.dalvik_jni_thread_line.search(line)
402    dalvik_native_thread_header = self.dalvik_native_thread_line.search(line)
403    unreachable_header = self.unreachable_line.search(line)
404    if process_header or signal_header or abort_message_header or thread_header or \
405        register_header or dalvik_jni_thread_header or dalvik_native_thread_header or \
406        revision_header or unreachable_header:
407      ret = True
408      if self.trace_lines or self.value_lines:
409        self.PrintOutput(self.trace_lines, self.value_lines)
410        self.PrintDivider()
411        self.trace_lines = []
412        self.value_lines = []
413        self.last_frame = -1
414      if process_header:
415        print(process_header.group(1))
416      if signal_header:
417        print(signal_header.group(1))
418      if abort_message_header:
419        print(abort_message_header.group(1))
420      if register_header:
421        print(register_header.group(1))
422      if thread_header:
423        print(thread_header.group(1))
424      if dalvik_jni_thread_header:
425        print(dalvik_jni_thread_header.group(1))
426      if dalvik_native_thread_header:
427        print(dalvik_native_thread_header.group(1))
428      if revision_header:
429        print(revision_header.group(1))
430      if unreachable_header:
431        print(unreachable_header.group(1))
432      return True
433    trace_line_dict = self.MatchTraceLine(line)
434    if trace_line_dict is not None:
435      ret = True
436      frame = int(trace_line_dict["frame"])
437      code_addr = trace_line_dict["offset"]
438      area = trace_line_dict["dso"]
439      so_offset = trace_line_dict["so_offset"]
440      symbol_present = trace_line_dict["symbol_present"]
441      symbol_name = trace_line_dict["symbol_name"]
442      build_id = trace_line_dict["build_id"]
443
444      if frame <= self.last_frame and (self.trace_lines or self.value_lines):
445        self.PrintOutput(self.trace_lines, self.value_lines)
446        self.PrintDivider()
447        self.trace_lines = []
448        self.value_lines = []
449      self.last_frame = frame
450
451      if area == "<unknown>" or area == "[heap]" or area == "[stack]":
452        self.trace_lines.append((code_addr, "", area))
453      else:
454        # If this is an apk, it usually means that there is actually
455        # a shared so that was loaded directly out of it. In that case,
456        # extract the shared library and the name of the shared library.
457        lib = None
458        # The format of the map name:
459        #   Some.apk!libshared.so
460        # or
461        #   Some.apk
462        if so_offset:
463          # If it ends in apk, we are done.
464          apk = None
465          if area.endswith(".apk"):
466            apk = area
467          else:
468            index = area.rfind(".so!")
469            if index != -1:
470              # Sometimes we'll see something like:
471              #   #01 pc abcd  libart.so!libart.so (offset 0x134000)
472              # Remove everything after the ! and zero the offset value.
473              area = area[0:index + 3]
474              so_offset = 0
475            else:
476              index = area.rfind(".apk!")
477              if index != -1:
478                apk = area[0:index + 4]
479          if apk:
480            lib_name, lib = self.GetLibFromApk(apk, so_offset)
481        if not lib:
482          lib = area
483          lib_name = None
484
485        if build_id:
486          # If we have the build_id, do a brute-force search of the symbols directory.
487          basename = os.path.basename(lib)
488          lib = self.GetLibraryByBuildId(symbol.SYMBOLS_DIR, basename, build_id)
489          if not lib:
490            print("WARNING: Cannot find {} with build id {} in symbols directory."
491                  .format(basename, build_id))
492        else:
493          # When using atest, test paths are different between the out/ directory
494          # and device. Apply fixups.
495          lib = self.GetLibPath(lib)
496
497        # If a calls b which further calls c and c is inlined to b, we want to
498        # display "a -> b -> c" in the stack trace instead of just "a -> c"
499        info = symbol.SymbolInformation(lib, code_addr)
500        nest_count = len(info) - 1
501        for (source_symbol, source_location, symbol_with_offset) in info:
502          if not source_symbol:
503            if symbol_present:
504              source_symbol = symbol.CallCppFilt(symbol_name)
505            else:
506              source_symbol = "<unknown>"
507          if not symbol.VERBOSE:
508            source_symbol = symbol.FormatSymbolWithoutParameters(source_symbol)
509            symbol_with_offset = symbol.FormatSymbolWithoutParameters(symbol_with_offset)
510          if not source_location:
511            source_location = area
512            if lib_name:
513              source_location += "(" + lib_name + ")"
514          if nest_count > 0:
515            nest_count = nest_count - 1
516            arrow = "v------>"
517            if symbol.ARCH == "arm64" or symbol.ARCH == "mips64" or symbol.ARCH == "x86_64":
518              arrow = "v-------------->"
519            self.trace_lines.append((arrow, source_symbol, source_location))
520          else:
521            if not symbol_with_offset:
522              symbol_with_offset = source_symbol
523            self.trace_lines.append((code_addr, symbol_with_offset, source_location))
524    if self.code_line.match(line):
525      # Code lines should be ignored. If this were exluded the 'code around'
526      # sections would trigger value_line matches.
527      return ret
528    if self.value_line.match(line):
529      ret = True
530      match = self.value_line.match(line)
531      (unused_, addr, value, area, symbol_present, symbol_name) = match.groups()
532      if area == "<unknown>" or area == "[heap]" or area == "[stack]" or not area:
533        self.value_lines.append((addr, value, "", area))
534      else:
535        info = symbol.SymbolInformation(area, value)
536        (source_symbol, source_location, object_symbol_with_offset) = info.pop()
537        # If there is no information, skip this.
538        if source_symbol or source_location or object_symbol_with_offset:
539          if not source_symbol:
540            if symbol_present:
541              source_symbol = symbol.CallCppFilt(symbol_name)
542            else:
543              source_symbol = "<unknown>"
544          if not source_location:
545            source_location = area
546          if not object_symbol_with_offset:
547            object_symbol_with_offset = source_symbol
548          self.value_lines.append((addr,
549                                   value,
550                                   object_symbol_with_offset,
551                                   source_location))
552
553    return ret
554
555
556class RegisterPatternTests(unittest.TestCase):
557  def assert_register_matches(self, abi, example_crash, stupid_pattern):
558    tc = TraceConverter()
559    lines = example_crash.split('\n')
560    symbol.SetAbi(lines)
561    tc.UpdateAbiRegexes()
562    for line in lines:
563      tc.ProcessLine(line)
564      is_register = (re.search(stupid_pattern, line) is not None)
565      matched = (tc.register_line.search(line) is not None)
566      self.assertEqual(matched, is_register, line)
567    tc.PrintOutput(tc.trace_lines, tc.value_lines)
568
569  def test_arm_registers(self):
570    self.assert_register_matches("arm", example_crashes.arm, '\\b(r0|r4|r8|ip)\\b')
571
572  def test_arm64_registers(self):
573    self.assert_register_matches("arm64", example_crashes.arm64, '\\b(x0|x4|x8|x12|x16|x20|x24|x28|sp)\\b')
574
575  def test_mips_registers(self):
576    self.assert_register_matches("mips", example_crashes.mips, '\\b(zr|a0|t0|t4|s0|s4|t8|gp|hi)\\b')
577
578  def test_mips64_registers(self):
579    self.assert_register_matches("mips64", example_crashes.mips64, '\\b(zr|a0|a4|t0|s0|s4|t8|gp|hi)\\b')
580
581  def test_x86_registers(self):
582    self.assert_register_matches("x86", example_crashes.x86, '\\b(eax|esi|xcs|eip)\\b')
583
584  def test_x86_64_registers(self):
585    self.assert_register_matches("x86_64", example_crashes.x86_64, '\\b(rax|rsi|r8|r12|cs|rip)\\b')
586
587class LibmemunreachablePatternTests(unittest.TestCase):
588  def test_libmemunreachable(self):
589    tc = TraceConverter()
590    lines = example_crashes.libmemunreachable.split('\n')
591
592    symbol.SetAbi(lines)
593    self.assertEqual(symbol.ARCH, "arm")
594
595    tc.UpdateAbiRegexes()
596    header_lines = 0
597    trace_lines = 0
598    for line in lines:
599      tc.ProcessLine(line)
600      if re.search(tc.unreachable_line, line) is not None:
601        header_lines += 1
602      if tc.MatchTraceLine(line) is not None:
603        trace_lines += 1
604    self.assertEqual(header_lines, 3)
605    self.assertEqual(trace_lines, 2)
606    tc.PrintOutput(tc.trace_lines, tc.value_lines)
607
608class LongASANStackTests(unittest.TestCase):
609  # Test that a long ASAN-style (non-padded frame numbers) stack trace is not split into two
610  # when the frame number becomes two digits. This happened before as the frame number was
611  # handled as a string and not converted to an integral.
612  def test_long_asan_crash(self):
613    tc = TraceConverter()
614    lines = example_crashes.long_asan_crash.splitlines()
615    symbol.SetAbi(lines)
616    tc.UpdateAbiRegexes()
617    # Test by making sure trace_line_count is monotonically non-decreasing. If the stack trace
618    # is split, a separator is printed and trace_lines is flushed.
619    trace_line_count = 0
620    for line in lines:
621      tc.ProcessLine(line)
622      self.assertLessEqual(trace_line_count, len(tc.trace_lines))
623      trace_line_count = len(tc.trace_lines)
624    # The split happened at transition of frame #9 -> #10. Make sure we have parsed (and stored)
625    # more than ten frames.
626    self.assertGreater(trace_line_count, 10)
627    tc.PrintOutput(tc.trace_lines, tc.value_lines)
628
629class ValueLinesTest(unittest.TestCase):
630  def test_value_line_skipped(self):
631    tc = TraceConverter()
632    symbol.SetAbi(["ABI: 'arm'"])
633    tc.UpdateAbiRegexes()
634    tc.ProcessLine("    12345678  00001000  .")
635    self.assertEqual([], tc.value_lines)
636
637if __name__ == '__main__':
638    unittest.main(verbosity=2)
639