Improve android stack parser
There was several issues with the Android stack parser that prevented it
to work correctly in a number of cases. The two cases fixed here:
- When a mojo app is retrieved with arguments
(dart_content_handler.mojo?strict=true), we want to ignore ?strict=true
when looking for a match on the local filesystem.
- There were cases when paths were not normalized correctly (for example,
double-slashes), preventing the libraries to match.
This fixes domokit/devtools#23
R=ppi@chromium.org, ppi
Review URL: https://codereview.chromium.org/1306603002 .
Cr-Mirrored-From: https://github.com/domokit/mojo
Cr-Mirrored-Commit: 763f44f16c1f564be73b9c6dbaef3edba542b157
diff --git a/android_stack_parser/stack b/android_stack_parser/stack
index 97913ab..f75d097 100755
--- a/android_stack_parser/stack
+++ b/android_stack_parser/stack
@@ -17,11 +17,11 @@
"""stack symbolizes native crash dumps."""
import getopt
-import glob
import os
-import re
+import os.path
import sys
+import stack_utils
import stack_core
import subprocess
import symbol
@@ -71,102 +71,6 @@
# pylint: enable-msg=C6310
sys.exit(1)
-def UnzipSymbols(symbolfile, symdir=None):
- """Unzips a file to _DEFAULT_SYMROOT and returns the unzipped location.
-
- Args:
- symbolfile: The .zip file to unzip
- symdir: Optional temporary directory to use for extraction
-
- Returns:
- A tuple containing (the directory into which the zip file was unzipped,
- the path to the "symbols" directory in the unzipped file). To clean
- up, the caller can delete the first element of the tuple.
-
- Raises:
- SymbolDownloadException: When the unzip fails.
- """
- if not symdir:
- symdir = "%s/%s" % (_DEFAULT_SYMROOT, hash(symbolfile))
- if not os.path.exists(symdir):
- os.makedirs(symdir)
-
- print "extracting %s..." % symbolfile
- saveddir = os.getcwd()
- os.chdir(symdir)
- try:
- unzipcode = subprocess.call(["unzip", "-qq", "-o", symbolfile])
- if unzipcode > 0:
- os.remove(symbolfile)
- raise SymbolDownloadException("failed to extract symbol files (%s)."
- % symbolfile)
- finally:
- os.chdir(saveddir)
-
- android_symbols = glob.glob("%s/out/target/product/*/symbols" % symdir)
- if android_symbols:
- return (symdir, android_symbols[0])
- else:
- # This is a zip of Chrome symbols, so symbol.CHROME_SYMBOLS_DIR needs to be
- # updated to point here.
- symbol.CHROME_SYMBOLS_DIR = symdir
- return (symdir, symdir)
-
-
-def GetBasenameFromMojoApp(url):
- """Used by GetSymbolMapping() to extract the basename from the location the
- mojo app was downloaded from. The location is a URL, e.g.
- http://foo/bar/x.so."""
- index = url.rfind('/')
- return url[(index + 1):] if index != -1 else url
-
-
-def GetSymboledNameForMojoApp(path):
- """Used by GetSymbolMapping to get the non-stripped library name for an
- installed mojo app."""
- # e.g. tracing.mojo -> libtracing_library.so
- name, ext = os.path.splitext(path)
- if ext != '.mojo':
- return path
- return 'lib%s_library.so' % name
-
-
-def GetSymbolMapping(lines):
- """Returns a mapping (dictionary) from download file to .so."""
- regex = re.compile('Caching mojo app (\S+) at (\S+)')
- mappings = {}
- for line in lines:
- result = regex.search(line)
- if result:
- url = GetBasenameFromMojoApp(result.group(1))
- mappings[result.group(2)] = GetSymboledNameForMojoApp(url)
- return mappings
-
-
-def _LowestAncestorContainingRelpath(dir_path, relpath):
- """Returns the lowest ancestor dir of |dir_path| that contains |relpath|.
- """
- cur_dir_path = os.path.abspath(dir_path)
- while True:
- if os.path.exists(os.path.join(cur_dir_path, relpath)):
- return cur_dir_path
-
- next_dir_path = os.path.dirname(cur_dir_path)
- if next_dir_path != cur_dir_path:
- cur_dir_path = next_dir_path
- else:
- return None
-
-
-def _GuessDir(relpath):
- """Returns absolute path to location |relpath| in the lowest ancestor of this
- file that contains it."""
- lowest_ancestor = _LowestAncestorContainingRelpath(
- os.path.dirname(__file__), relpath)
- if not lowest_ancestor:
- return None
- return os.path.join(lowest_ancestor, relpath)
-
def main():
@@ -204,7 +108,7 @@
more_info = False
if not symbol.BUILD_DIR:
- guess = _GuessDir(_DEFAULT_BUILD_DIR)
+ guess = stack_utils.GuessDir(_DEFAULT_BUILD_DIR)
if not guess:
print "Couldn't find the build directory, please pass --build-dir."
return 1
@@ -212,7 +116,7 @@
symbol.BUILD_DIR = guess
if not symbol.NDK_DIR:
- guess = _GuessDir(_DEFAULT_NDK_DIR)
+ guess = stack_utils.GuessDir(_DEFAULT_NDK_DIR)
if not guess:
print "Couldn't find the Android NDK, please pass --ndk-dir."
return 1
@@ -235,13 +139,13 @@
rootdir = None
if zip_arg:
- rootdir, symbol.SYMBOLS_DIR = UnzipSymbols(zip_arg)
+ rootdir, symbol.SYMBOLS_DIR = stack_utils.UnzipSymbols(zip_arg)
if symbol.SYMBOLS_DIR:
print "Reading Android symbols from", symbol.SYMBOLS_DIR
print "Reading Mojo symbols from", symbol.BUILD_DIR
- stack_core.ConvertTrace(lines, more_info, GetSymbolMapping(lines))
+ stack_core.ConvertTrace(lines, more_info, stack_utils.GetSymbolMapping(lines))
if rootdir:
# be a good citizen and clean up...os.rmdir and os.removedirs() don't work
diff --git a/android_stack_parser/stack_utils.py b/android_stack_parser/stack_utils.py
new file mode 100644
index 0000000..f6efb86
--- /dev/null
+++ b/android_stack_parser/stack_utils.py
@@ -0,0 +1,108 @@
+# Copyright 2015 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.
+
+"""Utility functions for the stack tool."""
+
+import glob
+import os
+import os.path
+import re
+
+
+def UnzipSymbols(symbolfile, symdir=None):
+ """Unzips a file to _DEFAULT_SYMROOT and returns the unzipped location.
+
+ Args:
+ symbolfile: The .zip file to unzip
+ symdir: Optional temporary directory to use for extraction
+
+ Returns:
+ A tuple containing (the directory into which the zip file was unzipped,
+ the path to the "symbols" directory in the unzipped file). To clean
+ up, the caller can delete the first element of the tuple.
+
+ Raises:
+ SymbolDownloadException: When the unzip fails.
+ """
+ if not symdir:
+ symdir = "%s/%s" % (_DEFAULT_SYMROOT, hash(symbolfile))
+ if not os.path.exists(symdir):
+ os.makedirs(symdir)
+
+ print "extracting %s..." % symbolfile
+ saveddir = os.getcwd()
+ os.chdir(symdir)
+ try:
+ unzipcode = subprocess.call(["unzip", "-qq", "-o", symbolfile])
+ if unzipcode > 0:
+ os.remove(symbolfile)
+ raise SymbolDownloadException("failed to extract symbol files (%s)."
+ % symbolfile)
+ finally:
+ os.chdir(saveddir)
+
+ android_symbols = glob.glob("%s/out/target/product/*/symbols" % symdir)
+ if android_symbols:
+ return (symdir, android_symbols[0])
+ else:
+ # This is a zip of Chrome symbols, so symbol.CHROME_SYMBOLS_DIR needs to be
+ # updated to point here.
+ symbol.CHROME_SYMBOLS_DIR = symdir
+ return (symdir, symdir)
+
+
+def GetBasenameFromMojoApp(url):
+ """Used by GetSymbolMapping() to extract the basename from the location the
+ mojo app was downloaded from. The location is a URL, e.g.
+ http://foo/bar/x.so."""
+ index = url.rfind('/')
+ return url[(index + 1):] if index != -1 else url
+
+
+def GetSymboledNameForMojoApp(path):
+ """Used by GetSymbolMapping to get the non-stripped library name for an
+ installed mojo app."""
+ # e.g. tracing.mojo -> libtracing_library.so
+ name, ext = os.path.splitext(path)
+ if ext != '.mojo':
+ return path
+ return 'lib%s_library.so' % name
+
+
+def GetSymbolMapping(lines):
+ """Returns a mapping (dictionary) from download file to .so."""
+ regex = re.compile('Caching mojo app (\S+?)(?:\?\S+)? at (\S+)')
+ mappings = {}
+ for line in lines:
+ result = regex.search(line)
+ if result:
+ url = GetBasenameFromMojoApp(result.group(1))
+ mappings[os.path.normpath(result.group(2))] = GetSymboledNameForMojoApp(
+ url)
+ return mappings
+
+
+def _LowestAncestorContainingRelpath(dir_path, relpath):
+ """Returns the lowest ancestor dir of |dir_path| that contains |relpath|.
+ """
+ cur_dir_path = os.path.abspath(dir_path)
+ while True:
+ if os.path.exists(os.path.join(cur_dir_path, relpath)):
+ return cur_dir_path
+
+ next_dir_path = os.path.dirname(cur_dir_path)
+ if next_dir_path != cur_dir_path:
+ cur_dir_path = next_dir_path
+ else:
+ return None
+
+
+def GuessDir(relpath):
+ """Returns absolute path to location |relpath| in the lowest ancestor of this
+ file that contains it."""
+ lowest_ancestor = _LowestAncestorContainingRelpath(
+ os.path.dirname(__file__), relpath)
+ if not lowest_ancestor:
+ return None
+ return os.path.join(lowest_ancestor, relpath)
diff --git a/android_stack_parser/stack_utils_unittest.py b/android_stack_parser/stack_utils_unittest.py
new file mode 100644
index 0000000..6329aa6
--- /dev/null
+++ b/android_stack_parser/stack_utils_unittest.py
@@ -0,0 +1,70 @@
+# Copyright 2015 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.
+
+"""Tests for the native crash dump symbolizer utility functions."""
+
+import unittest
+import stack_utils
+
+
+class StackUtilsTest(unittest.TestCase):
+ """Tests the native crash dump symbolizer utility functions."""
+
+ def test_GetSymbolMapping_no_match(self):
+ """Verifies that, if no mapping is on the input, no mapping is on the
+ output.
+ """
+ lines = ["This is a test case\n", "Caching all mojo apps", ""]
+ self.assertDictEqual({}, stack_utils.GetSymbolMapping(lines))
+
+ def test_GetSymbolMapping_simple_match(self):
+ """Verifies a simple symbol mapping."""
+ lines = ["This is a test case\n", "Caching all mojo apps",
+ "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app "
+ "https://apps.mojo/myapp.mojo at /path/to/myapp.mojo/.lM03ws"]
+ golden_dict = {
+ "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so"
+ }
+ actual_dict = stack_utils.GetSymbolMapping(lines)
+ self.assertDictEqual(golden_dict, actual_dict)
+
+ def test_GetSymbolMapping_multiple_match(self):
+ """Verifies mapping of multiple symbol files."""
+ lines = ["This is a test case\n", "Caching all mojo apps",
+ "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app "
+ "https://apps.mojo/myapp.mojo at /path/to/myapp.mojo/.lM03ws",
+ "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app "
+ "https://apps.mojo/otherapp.mojo at /path/to/otherapp.mojo/.kW07s"]
+ golden_dict = {
+ "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so",
+ "/path/to/otherapp.mojo/.kW07s": "libotherapp_library.so"
+ }
+ actual_dict = stack_utils.GetSymbolMapping(lines)
+ self.assertDictEqual(golden_dict, actual_dict)
+
+ def test_GetSymbolMapping_parameter_match(self):
+ """Verifies parameters are stripped from mappings."""
+ lines = ["This is a test case\n", "Caching all mojo apps",
+ "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app "
+ "https://apps.mojo/myapp.mojo?q=hello at /path/to/myapp.mojo/.lM03ws"]
+ golden_dict = {
+ "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so"
+ }
+ actual_dict = stack_utils.GetSymbolMapping(lines)
+ self.assertDictEqual(golden_dict, actual_dict)
+
+ def test_GetSymbolMapping_normalize(self):
+ """Verifies paths are normalized in mappings."""
+ lines = ["This is a test case\n", "Caching all mojo apps",
+ "I/mojo(2): [INFO:somefile.cc(85)] Caching mojo app "
+ "https://apps.mojo/myapp.mojo at /path/to/.//myapp.mojo/.lM03ws"]
+ golden_dict = {
+ "/path/to/myapp.mojo/.lM03ws": "libmyapp_library.so"
+ }
+ actual_dict = stack_utils.GetSymbolMapping(lines)
+ self.assertDictEqual(golden_dict, actual_dict)
+
+
+if __name__ == "__main__":
+ unittest.main()