OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Collect debug info for a test.""" |
| 7 |
| 8 import datetime |
| 9 import logging |
| 10 import os |
| 11 import re |
| 12 import shutil |
| 13 import string |
| 14 import subprocess |
| 15 import tempfile |
| 16 |
| 17 import cmd_helper |
| 18 |
| 19 |
| 20 TOMBSTONE_DIR = '/data/tombstones/' |
| 21 |
| 22 |
| 23 class GTestDebugInfo(object): |
| 24 """A helper class to get relate debug information for a gtest. |
| 25 |
| 26 Args: |
| 27 adb: ADB interface the tests are using. |
| 28 device: Serial# of the Android device in which the specified gtest runs. |
| 29 testsuite_name: Name of the specified gtest. |
| 30 gtest_filter: Test filter used by the specified gtest. |
| 31 """ |
| 32 |
| 33 def __init__(self, adb, device, testsuite_name, gtest_filter, |
| 34 collect_new_crashes=True): |
| 35 """Initializes the DebugInfo class for a specified gtest.""" |
| 36 self.adb = adb |
| 37 self.device = device |
| 38 self.testsuite_name = testsuite_name |
| 39 self.gtest_filter = gtest_filter |
| 40 self.logcat_process = None |
| 41 self.has_storage = False |
| 42 self.log_dir = None |
| 43 self.log_file_name = None |
| 44 self.collect_new_crashes = collect_new_crashes |
| 45 self.old_crash_files = self.ListCrashFiles() |
| 46 |
| 47 def InitStorage(self): |
| 48 """Initializes the storage in where we put the debug information.""" |
| 49 if self.has_storage: |
| 50 return |
| 51 self.has_storage = True |
| 52 self.log_dir = tempfile.mkdtemp() |
| 53 self.log_file_name = os.path.join(self.log_dir, |
| 54 self._GeneratePrefixName() + '_log.txt') |
| 55 |
| 56 def CleanupStorage(self): |
| 57 """Cleans up the storage in where we put the debug information.""" |
| 58 if not self.has_storage: |
| 59 return |
| 60 self.has_storage = False |
| 61 assert os.path.exists(self.log_dir) |
| 62 shutil.rmtree(self.log_dir) |
| 63 self.log_dir = None |
| 64 self.log_file_name = None |
| 65 |
| 66 def GetStoragePath(self): |
| 67 """Returns the path in where we store the debug information.""" |
| 68 self.InitStorage() |
| 69 return self.log_dir |
| 70 |
| 71 def _GetSignatureFromGTestFilter(self): |
| 72 """Gets a signature from gtest_filter. |
| 73 |
| 74 Signature is used to identify the tests from which we collect debug |
| 75 information. |
| 76 |
| 77 Returns: |
| 78 A signature string. Returns 'all' if there is no gtest filter. |
| 79 """ |
| 80 if not self.gtest_filter: |
| 81 return 'all' |
| 82 filename_chars = "-_()%s%s" % (string.ascii_letters, string.digits) |
| 83 return ''.join(c for c in self.gtest_filter if c in filename_chars) |
| 84 |
| 85 def _GeneratePrefixName(self): |
| 86 """Generates a prefix name for debug information of the test. |
| 87 |
| 88 The prefix name consists of the following: |
| 89 (1) root name of test_suite_base. |
| 90 (2) device serial number. |
| 91 (3) filter signature generate from gtest_filter. |
| 92 (4) date & time when calling this method. |
| 93 |
| 94 Returns: |
| 95 Name of the log file. |
| 96 """ |
| 97 return (os.path.splitext(self.testsuite_name)[0] + '_' + self.device + '_' + |
| 98 self._GetSignatureFromGTestFilter() + '_' + |
| 99 datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-%f')) |
| 100 |
| 101 def StartRecordingLog(self, clear=True, filters=['*:v']): |
| 102 """Starts recording logcat output to a file. |
| 103 |
| 104 This call should come before running test, with calling StopRecordingLog |
| 105 following the tests. |
| 106 |
| 107 Args: |
| 108 clear: True if existing log output should be cleared. |
| 109 filters: A list of logcat filters to be used. |
| 110 """ |
| 111 self.InitStorage() |
| 112 self.StopRecordingLog() |
| 113 if clear: |
| 114 cmd_helper.RunCmd(['adb', 'logcat', '-c']) |
| 115 logging.info('Start dumping log to %s ...' % self.log_file_name) |
| 116 command = 'adb logcat -v threadtime %s > %s' % (' '.join(filters), |
| 117 self.log_file_name) |
| 118 self.logcat_process = subprocess.Popen(command, shell=True) |
| 119 |
| 120 def StopRecordingLog(self): |
| 121 """Stops an existing logcat recording subprocess.""" |
| 122 if not self.logcat_process: |
| 123 return |
| 124 # Cannot evaluate directly as 0 is a possible value. |
| 125 if self.logcat_process.poll() is None: |
| 126 self.logcat_process.kill() |
| 127 self.logcat_process = None |
| 128 logging.info('Finish log dump.') |
| 129 |
| 130 def TakeScreenshot(self, identifier_mark): |
| 131 """Takes a screen shot from current specified device. |
| 132 |
| 133 Args: |
| 134 identifier_mark: A string to identify the screen shot DebugInfo will take. |
| 135 It will be part of filename of the screen shot. Empty |
| 136 string is acceptable. |
| 137 Returns: |
| 138 Returns True if successfully taking screen shot from device, otherwise |
| 139 returns False. |
| 140 """ |
| 141 self.InitStorage() |
| 142 assert isinstance(identifier_mark, str) |
| 143 shot_path = os.path.join(self.log_dir, ''.join([self._GeneratePrefixName(), |
| 144 identifier_mark, |
| 145 '_screenshot.png'])) |
| 146 screenshot_path = os.path.join(os.getenv('ANDROID_HOST_OUT'), 'bin', |
| 147 'screenshot2') |
| 148 re_success = re.compile(re.escape('Success.'), re.MULTILINE) |
| 149 if re_success.findall(cmd_helper.GetCmdOutput([screenshot_path, '-s', |
| 150 self.device, shot_path])): |
| 151 logging.info("Successfully took a screen shot to %s" % shot_path) |
| 152 return True |
| 153 logging.error('Failed to take screen shot from device %s' % self.device) |
| 154 return False |
| 155 |
| 156 def ListCrashFiles(self): |
| 157 """Collects crash files from current specified device. |
| 158 |
| 159 Returns: |
| 160 A dict of crash files in format {"name": (size, lastmod), ...}. |
| 161 """ |
| 162 if not self.collect_new_crashes: |
| 163 return {} |
| 164 return self.adb.ListPathContents(TOMBSTONE_DIR) |
| 165 |
| 166 def ArchiveNewCrashFiles(self): |
| 167 """Archives the crash files newly generated until calling this method.""" |
| 168 if not self.collect_new_crashes: |
| 169 return |
| 170 current_crash_files = self.ListCrashFiles() |
| 171 files = [f for f in current_crash_files if f not in self.old_crash_files] |
| 172 logging.info('New crash file(s):%s' % ' '.join(files)) |
| 173 for f in files: |
| 174 self.adb.Adb().Pull(TOMBSTONE_DIR + f, |
| 175 os.path.join(self.GetStoragePath(), f)) |
| 176 |
| 177 @staticmethod |
| 178 def ZipAndCleanResults(dest_dir, dump_file_name, debug_info_list): |
| 179 """A helper method to zip all debug information results into a dump file. |
| 180 |
| 181 Args: |
| 182 dest-dir: Dir path in where we put the dump file. |
| 183 dump_file_name: Desired name of the dump file. This method makes sure |
| 184 '.zip' will be added as ext name. |
| 185 debug_info_list: List of all debug info objects. |
| 186 """ |
| 187 if not dest_dir or not dump_file_name or not debug_info_list: |
| 188 return |
| 189 cmd_helper.RunCmd(['mkdir', '-p', dest_dir]) |
| 190 log_basename = os.path.basename(dump_file_name) |
| 191 log_file = os.path.join(dest_dir, |
| 192 os.path.splitext(log_basename)[0] + '.zip') |
| 193 logging.info('Zipping debug dumps into %s ...' % log_file) |
| 194 for d in debug_info_list: |
| 195 d.ArchiveNewCrashFiles() |
| 196 # Add new dumps into the zip file. The zip may exist already if previous |
| 197 # gtest also dumps the debug information. It's OK since we clean up the old |
| 198 # dumps in each build step. |
| 199 cmd_helper.RunCmd(['zip', '-q', '-r', log_file, |
| 200 ' '.join([d.GetStoragePath() for d in debug_info_list])]) |
| 201 assert os.path.exists(log_file) |
| 202 for debug_info in debug_info_list: |
| 203 debug_info.CleanupStorage() |
OLD | NEW |