Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(28)

Side by Side Diff: build/android/pylib/debug_info.py

Issue 10777017: Android: further simplication for test runners. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Nit on comment Created 8 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | build/android/pylib/single_test_runner.py » ('j') | build/android/run_tests.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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)
OLDNEW
« no previous file with comments | « no previous file | build/android/pylib/single_test_runner.py » ('j') | build/android/run_tests.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698