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