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