1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/media/webrtc/trunk/build/android/pylib/debug_info.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,196 @@ 1.4 +# Copyright (c) 2012 The Chromium Authors. All rights reserved. 1.5 +# Use of this source code is governed by a BSD-style license that can be 1.6 +# found in the LICENSE file. 1.7 + 1.8 +"""Collect debug info for a test.""" 1.9 + 1.10 +import datetime 1.11 +import logging 1.12 +import os 1.13 +import re 1.14 +import shutil 1.15 +import string 1.16 +import subprocess 1.17 +import tempfile 1.18 + 1.19 +import cmd_helper 1.20 + 1.21 + 1.22 +TOMBSTONE_DIR = '/data/tombstones/' 1.23 + 1.24 + 1.25 +class GTestDebugInfo(object): 1.26 + """A helper class to collect related debug information for a gtest. 1.27 + 1.28 + Debug info is collected in two steps: 1.29 + - first, object(s) of this class (one per device), accumulate logs 1.30 + and screenshots in tempdir. 1.31 + - once the test has finished, call ZipAndCleanResults to create 1.32 + a zip containing the logs from all devices, and clean them up. 1.33 + 1.34 + Args: 1.35 + adb: ADB interface the tests are using. 1.36 + device: Serial# of the Android device in which the specified gtest runs. 1.37 + testsuite_name: Name of the specified gtest. 1.38 + gtest_filter: Test filter used by the specified gtest. 1.39 + """ 1.40 + 1.41 + def __init__(self, adb, device, testsuite_name, gtest_filter): 1.42 + """Initializes the DebugInfo class for a specified gtest.""" 1.43 + self.adb = adb 1.44 + self.device = device 1.45 + self.testsuite_name = testsuite_name 1.46 + self.gtest_filter = gtest_filter 1.47 + self.logcat_process = None 1.48 + self.has_storage = False 1.49 + self.log_dir = os.path.join(tempfile.gettempdir(), 1.50 + 'gtest_debug_info', 1.51 + self.testsuite_name, 1.52 + self.device) 1.53 + if not os.path.exists(self.log_dir): 1.54 + os.makedirs(self.log_dir) 1.55 + self.log_file_name = os.path.join(self.log_dir, 1.56 + self._GeneratePrefixName() + '_log.txt') 1.57 + self.old_crash_files = self._ListCrashFiles() 1.58 + 1.59 + def _GetSignatureFromGTestFilter(self): 1.60 + """Gets a signature from gtest_filter. 1.61 + 1.62 + Signature is used to identify the tests from which we collect debug 1.63 + information. 1.64 + 1.65 + Returns: 1.66 + A signature string. Returns 'all' if there is no gtest filter. 1.67 + """ 1.68 + if not self.gtest_filter: 1.69 + return 'all' 1.70 + filename_chars = "-_()%s%s" % (string.ascii_letters, string.digits) 1.71 + signature = ''.join(c for c in self.gtest_filter if c in filename_chars) 1.72 + if len(signature) > 64: 1.73 + # The signature can't be too long, as it'll be part of a file name. 1.74 + signature = signature[:64] 1.75 + return signature 1.76 + 1.77 + def _GeneratePrefixName(self): 1.78 + """Generates a prefix name for debug information of the test. 1.79 + 1.80 + The prefix name consists of the following: 1.81 + (1) root name of test_suite_base. 1.82 + (2) device serial number. 1.83 + (3) prefix of filter signature generate from gtest_filter. 1.84 + (4) date & time when calling this method. 1.85 + 1.86 + Returns: 1.87 + Name of the log file. 1.88 + """ 1.89 + return (os.path.splitext(self.testsuite_name)[0] + '_' + self.device + '_' + 1.90 + self._GetSignatureFromGTestFilter() + '_' + 1.91 + datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-%f')) 1.92 + 1.93 + def StartRecordingLog(self, clear=True, filters=['*:v']): 1.94 + """Starts recording logcat output to a file. 1.95 + 1.96 + This call should come before running test, with calling StopRecordingLog 1.97 + following the tests. 1.98 + 1.99 + Args: 1.100 + clear: True if existing log output should be cleared. 1.101 + filters: A list of logcat filters to be used. 1.102 + """ 1.103 + self.StopRecordingLog() 1.104 + if clear: 1.105 + cmd_helper.RunCmd(['adb', '-s', self.device, 'logcat', '-c']) 1.106 + logging.info('Start dumping log to %s ...', self.log_file_name) 1.107 + command = 'adb -s %s logcat -v threadtime %s > %s' % (self.device, 1.108 + ' '.join(filters), 1.109 + self.log_file_name) 1.110 + self.logcat_process = subprocess.Popen(command, shell=True) 1.111 + 1.112 + def StopRecordingLog(self): 1.113 + """Stops an existing logcat recording subprocess.""" 1.114 + if not self.logcat_process: 1.115 + return 1.116 + # Cannot evaluate directly as 0 is a possible value. 1.117 + if self.logcat_process.poll() is None: 1.118 + self.logcat_process.kill() 1.119 + self.logcat_process = None 1.120 + logging.info('Finish log dump.') 1.121 + 1.122 + def TakeScreenshot(self, identifier_mark): 1.123 + """Takes a screen shot from current specified device. 1.124 + 1.125 + Args: 1.126 + identifier_mark: A string to identify the screen shot DebugInfo will take. 1.127 + It will be part of filename of the screen shot. Empty 1.128 + string is acceptable. 1.129 + Returns: 1.130 + Returns the file name on the host of the screenshot if successful, 1.131 + None otherwise. 1.132 + """ 1.133 + assert isinstance(identifier_mark, str) 1.134 + screenshot_path = os.path.join(os.getenv('ANDROID_HOST_OUT', ''), 1.135 + 'bin', 1.136 + 'screenshot2') 1.137 + if not os.path.exists(screenshot_path): 1.138 + logging.error('Failed to take screen shot from device %s', self.device) 1.139 + return None 1.140 + shot_path = os.path.join(self.log_dir, ''.join([self._GeneratePrefixName(), 1.141 + identifier_mark, 1.142 + '_screenshot.png'])) 1.143 + re_success = re.compile(re.escape('Success.'), re.MULTILINE) 1.144 + if re_success.findall(cmd_helper.GetCmdOutput([screenshot_path, '-s', 1.145 + self.device, shot_path])): 1.146 + logging.info('Successfully took a screen shot to %s', shot_path) 1.147 + return shot_path 1.148 + logging.error('Failed to take screen shot from device %s', self.device) 1.149 + return None 1.150 + 1.151 + def _ListCrashFiles(self): 1.152 + """Collects crash files from current specified device. 1.153 + 1.154 + Returns: 1.155 + A dict of crash files in format {"name": (size, lastmod), ...}. 1.156 + """ 1.157 + return self.adb.ListPathContents(TOMBSTONE_DIR) 1.158 + 1.159 + def ArchiveNewCrashFiles(self): 1.160 + """Archives the crash files newly generated until calling this method.""" 1.161 + current_crash_files = self._ListCrashFiles() 1.162 + files = [] 1.163 + for f in current_crash_files: 1.164 + if f not in self.old_crash_files: 1.165 + files += [f] 1.166 + elif current_crash_files[f] != self.old_crash_files[f]: 1.167 + # Tombstones dir can only have maximum 10 files, so we need to compare 1.168 + # size and timestamp information of file if the file exists. 1.169 + files += [f] 1.170 + if files: 1.171 + logging.info('New crash file(s):%s' % ' '.join(files)) 1.172 + for f in files: 1.173 + self.adb.Adb().Pull(TOMBSTONE_DIR + f, 1.174 + os.path.join(self.log_dir, f)) 1.175 + 1.176 + @staticmethod 1.177 + def ZipAndCleanResults(dest_dir, dump_file_name): 1.178 + """A helper method to zip all debug information results into a dump file. 1.179 + 1.180 + Args: 1.181 + dest_dir: Dir path in where we put the dump file. 1.182 + dump_file_name: Desired name of the dump file. This method makes sure 1.183 + '.zip' will be added as ext name. 1.184 + """ 1.185 + if not dest_dir or not dump_file_name: 1.186 + return 1.187 + cmd_helper.RunCmd(['mkdir', '-p', dest_dir]) 1.188 + log_basename = os.path.basename(dump_file_name) 1.189 + log_zip_file = os.path.join(dest_dir, 1.190 + os.path.splitext(log_basename)[0] + '.zip') 1.191 + logging.info('Zipping debug dumps into %s ...', log_zip_file) 1.192 + # Add new dumps into the zip file. The zip may exist already if previous 1.193 + # gtest also dumps the debug information. It's OK since we clean up the old 1.194 + # dumps in each build step. 1.195 + log_src_dir = os.path.join(tempfile.gettempdir(), 'gtest_debug_info') 1.196 + cmd_helper.RunCmd(['zip', '-q', '-r', log_zip_file, log_src_dir]) 1.197 + assert os.path.exists(log_zip_file) 1.198 + assert os.path.exists(log_src_dir) 1.199 + shutil.rmtree(log_src_dir)