debug_info.py 7.4 KB

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