| Index: build/android/debug_info.py
|
| diff --git a/build/android/debug_info.py b/build/android/debug_info.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f4df79ca6f076d529c3b10bede52912d9651a9a4
|
| --- /dev/null
|
| +++ b/build/android/debug_info.py
|
| @@ -0,0 +1,203 @@
|
| +#!/usr/bin/python
|
| +# Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""Collect debug info for a test."""
|
| +
|
| +import datetime
|
| +import logging
|
| +import os
|
| +import re
|
| +import shutil
|
| +import string
|
| +import subprocess
|
| +import tempfile
|
| +
|
| +import cmd_helper
|
| +
|
| +
|
| +TOMBSTONE_DIR = '/data/tombstones/'
|
| +
|
| +
|
| +class GTestDebugInfo(object):
|
| + """A helper class to get relate debug information for a gtest.
|
| +
|
| + Args:
|
| + adb: ADB interface the tests are using.
|
| + device: Serial# of the Android device in which the specified gtest runs.
|
| + testsuite_name: Name of the specified gtest.
|
| + gtest_filter: Test filter used by the specified gtest.
|
| + """
|
| +
|
| + def __init__(self, adb, device, testsuite_name, gtest_filter,
|
| + collect_new_crashes=True):
|
| + """Initializes the DebugInfo class for a specified gtest."""
|
| + self.adb = adb
|
| + self.device = device
|
| + self.testsuite_name = testsuite_name
|
| + self.gtest_filter = gtest_filter
|
| + self.logcat_process = None
|
| + self.has_storage = False
|
| + self.log_dir = None
|
| + self.log_file_name = None
|
| + self.collect_new_crashes = collect_new_crashes
|
| + self.old_crash_files = self.ListCrashFiles()
|
| +
|
| + def InitStorage(self):
|
| + """Initializes the storage in where we put the debug information."""
|
| + if self.has_storage:
|
| + return
|
| + self.has_storage = True
|
| + self.log_dir = tempfile.mkdtemp()
|
| + self.log_file_name = os.path.join(self.log_dir,
|
| + self._GeneratePrefixName() + '_log.txt')
|
| +
|
| + def CleanupStorage(self):
|
| + """Cleans up the storage in where we put the debug information."""
|
| + if not self.has_storage:
|
| + return
|
| + self.has_storage = False
|
| + assert os.path.exists(self.log_dir)
|
| + shutil.rmtree(self.log_dir)
|
| + self.log_dir = None
|
| + self.log_file_name = None
|
| +
|
| + def GetStoragePath(self):
|
| + """Returns the path in where we store the debug information."""
|
| + self.InitStorage()
|
| + return self.log_dir
|
| +
|
| + def _GetSignatureFromGTestFilter(self):
|
| + """Gets a signature from gtest_filter.
|
| +
|
| + Signature is used to identify the tests from which we collect debug
|
| + information.
|
| +
|
| + Returns:
|
| + A signature string. Returns 'all' if there is no gtest filter.
|
| + """
|
| + if not self.gtest_filter:
|
| + return 'all'
|
| + filename_chars = "-_()%s%s" % (string.ascii_letters, string.digits)
|
| + return ''.join(c for c in self.gtest_filter if c in filename_chars)
|
| +
|
| + def _GeneratePrefixName(self):
|
| + """Generates a prefix name for debug information of the test.
|
| +
|
| + The prefix name consists of the following:
|
| + (1) root name of test_suite_base.
|
| + (2) device serial number.
|
| + (3) filter signature generate from gtest_filter.
|
| + (4) date & time when calling this method.
|
| +
|
| + Returns:
|
| + Name of the log file.
|
| + """
|
| + return (os.path.splitext(self.testsuite_name)[0] + '_' + self.device + '_' +
|
| + self._GetSignatureFromGTestFilter() + '_' +
|
| + datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-%f'))
|
| +
|
| + def StartRecordingLog(self, clear=True, filters=['*:v']):
|
| + """Starts recording logcat output to a file.
|
| +
|
| + This call should come before running test, with calling StopRecordingLog
|
| + following the tests.
|
| +
|
| + Args:
|
| + clear: True if existing log output should be cleared.
|
| + filters: A list of logcat filters to be used.
|
| + """
|
| + self.InitStorage()
|
| + self.StopRecordingLog()
|
| + if clear:
|
| + cmd_helper.RunCmd(['adb', 'logcat', '-c'])
|
| + logging.info('Start dumping log to %s ...' % self.log_file_name)
|
| + command = 'adb logcat -v threadtime %s > %s' % (' '.join(filters),
|
| + self.log_file_name)
|
| + self.logcat_process = subprocess.Popen(command, shell=True)
|
| +
|
| + def StopRecordingLog(self):
|
| + """Stops an existing logcat recording subprocess."""
|
| + if not self.logcat_process:
|
| + return
|
| + # Cannot evaluate directly as 0 is a possible value.
|
| + if self.logcat_process.poll() is None:
|
| + self.logcat_process.kill()
|
| + self.logcat_process = None
|
| + logging.info('Finish log dump.')
|
| +
|
| + def TakeScreenshot(self, identifier_mark):
|
| + """Takes a screen shot from current specified device.
|
| +
|
| + Args:
|
| + identifier_mark: A string to identify the screen shot DebugInfo will take.
|
| + It will be part of filename of the screen shot. Empty
|
| + string is acceptable.
|
| + Returns:
|
| + Returns True if successfully taking screen shot from device, otherwise
|
| + returns False.
|
| + """
|
| + self.InitStorage()
|
| + assert isinstance(identifier_mark, str)
|
| + shot_path = os.path.join(self.log_dir, ''.join([self._GeneratePrefixName(),
|
| + identifier_mark,
|
| + '_screenshot.png']))
|
| + screenshot_path = os.path.join(os.getenv('ANDROID_HOST_OUT'), 'bin',
|
| + 'screenshot2')
|
| + re_success = re.compile(re.escape('Success.'), re.MULTILINE)
|
| + if re_success.findall(cmd_helper.GetCmdOutput([screenshot_path, '-s',
|
| + self.device, shot_path])):
|
| + logging.info("Successfully took a screen shot to %s" % shot_path)
|
| + return True
|
| + logging.error('Failed to take screen shot from device %s' % self.device)
|
| + return False
|
| +
|
| + def ListCrashFiles(self):
|
| + """Collects crash files from current specified device.
|
| +
|
| + Returns:
|
| + A dict of crash files in format {"name": (size, lastmod), ...}.
|
| + """
|
| + if not self.collect_new_crashes:
|
| + return {}
|
| + return self.adb.ListPathContents(TOMBSTONE_DIR)
|
| +
|
| + def ArchiveNewCrashFiles(self):
|
| + """Archives the crash files newly generated until calling this method."""
|
| + if not self.collect_new_crashes:
|
| + return
|
| + current_crash_files = self.ListCrashFiles()
|
| + files = [f for f in current_crash_files if f not in self.old_crash_files]
|
| + logging.info('New crash file(s):%s' % ' '.join(files))
|
| + for f in files:
|
| + self.adb.Adb().Pull(TOMBSTONE_DIR + f,
|
| + os.path.join(self.GetStoragePath(), f))
|
| +
|
| + @staticmethod
|
| + def ZipAndCleanResults(dest_dir, dump_file_name, debug_info_list):
|
| + """A helper method to zip all debug information results into a dump file.
|
| +
|
| + Args:
|
| + dest-dir: Dir path in where we put the dump file.
|
| + dump_file_name: Desired name of the dump file. This method makes sure
|
| + '.zip' will be added as ext name.
|
| + debug_info_list: List of all debug info objects.
|
| + """
|
| + if not dest_dir or not dump_file_name or not debug_info_list:
|
| + return
|
| + cmd_helper.RunCmd(['mkdir', '-p', dest_dir])
|
| + log_basename = os.path.basename(dump_file_name)
|
| + log_file = os.path.join(dest_dir,
|
| + os.path.splitext(log_basename)[0] + '.zip')
|
| + logging.info('Zipping debug dumps into %s ...' % log_file)
|
| + for d in debug_info_list:
|
| + d.ArchiveNewCrashFiles()
|
| + # Add new dumps into the zip file. The zip may exist already if previous
|
| + # gtest also dumps the debug information. It's OK since we clean up the old
|
| + # dumps in each build step.
|
| + cmd_helper.RunCmd(['zip', '-q', '-r', log_file,
|
| + ' '.join([d.GetStoragePath() for d in debug_info_list])])
|
| + assert os.path.exists(log_file)
|
| + for debug_info in debug_info_list:
|
| + debug_info.CleanupStorage()
|
|
|