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