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 is consist of the following: | |
Jing Zhao
2011/10/20 06:10:23
is consist of -> consists
michaelbai
2011/10/20 16:23:03
Done.
| |
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)) | |
Nirnimesh
2011/10/20 02:06:50
remove outer parens
michaelbai
2011/10/20 16:23:03
Done.
| |
202 for debug_info in debug_info_list: | |
203 debug_info.CleanupStorage() | |
OLD | NEW |