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