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