breakpad_file_extractor_unittest.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. #!/usr/bin/env vpython3
  2. # Copyright 2021 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. from logging import exception
  6. import os
  7. import shutil
  8. import sys
  9. import tempfile
  10. import unittest
  11. import breakpad_file_extractor
  12. import get_symbols_util
  13. sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, 'perf'))
  14. from core import path_util
  15. path_util.AddPyUtilsToPath()
  16. path_util.AddTracingToPath()
  17. import metadata_extractor
  18. import mock
  19. class ExtractBreakpadTestCase(unittest.TestCase):
  20. def setUp(self):
  21. # Create test inputs for ExtractBreakpadFiles() function.
  22. self.test_build_dir = tempfile.mkdtemp()
  23. self.test_breakpad_dir = tempfile.mkdtemp()
  24. self.test_dump_syms_dir = tempfile.mkdtemp()
  25. # NamedTemporaryFile() is hard coded to have a set of random 8 characters
  26. # appended to whatever prefix is given. Those characters can't be easily
  27. # removed, so |self.test_dump_syms_binary| is opened this way.
  28. self.test_dump_syms_binary = os.path.join(self.test_dump_syms_dir,
  29. 'dump_syms')
  30. with open(self.test_dump_syms_binary, 'w'):
  31. pass
  32. # Stash function.
  33. self.RunDumpSyms_stash = breakpad_file_extractor._RunDumpSyms
  34. def tearDown(self):
  35. shutil.rmtree(self.test_build_dir)
  36. shutil.rmtree(self.test_breakpad_dir)
  37. shutil.rmtree(self.test_dump_syms_dir)
  38. # Unstash function.
  39. breakpad_file_extractor._RunDumpSyms = self.RunDumpSyms_stash
  40. def _setupSubtreeFiles(self):
  41. # Create subtree directory structure. All files deleted when
  42. # |test_breakpad_dir| is recursively deleted.
  43. out = tempfile.mkdtemp(dir=self.test_breakpad_dir)
  44. release = tempfile.mkdtemp(dir=out)
  45. subdir = tempfile.mkdtemp(dir=release)
  46. unstripped_dir = os.path.join(release, 'lib.unstripped')
  47. os.mkdir(unstripped_dir)
  48. # Create symbol files.
  49. symbol_files = []
  50. symbol_files.append(os.path.join(subdir, 'subdir.so'))
  51. symbol_files.append(os.path.join(unstripped_dir, 'unstripped.so'))
  52. symbol_files.append(os.path.join(unstripped_dir, 'unstripped2.so'))
  53. for new_file in symbol_files:
  54. with open(new_file, 'w') as _:
  55. pass
  56. # Build side effect mapping.
  57. side_effect_map = {
  58. symbol_files[0]:
  59. 'MODULE Android x86_64 34984AB4EF948C0000000000000000000 subdir.so',
  60. symbol_files[1]: 'MODULE Android x86_64 34984AB4EF948D unstripped.so',
  61. symbol_files[2]: 'MODULE Android x86_64 34984AB4EF949A unstripped2.so'
  62. }
  63. return symbol_files, side_effect_map
  64. def _getDumpSymsMockSideEffect(self, side_effect_map):
  65. def run_dumpsyms_side_effect(dump_syms_binary,
  66. input_file_path,
  67. output_file_path,
  68. only_module_header=False):
  69. self.assertEqual(self.test_dump_syms_binary, dump_syms_binary)
  70. if only_module_header:
  71. # Extract Module ID.
  72. with open(output_file_path, 'w') as f:
  73. # Write the correct module header into the output f
  74. f.write(side_effect_map[input_file_path])
  75. else:
  76. # Extract breakpads.
  77. with open(output_file_path, 'w'):
  78. pass
  79. return True
  80. return run_dumpsyms_side_effect
  81. def _getExpectedModuleExtractionCalls(self, symbol_files):
  82. expected_module_calls = [
  83. mock.call(self.test_dump_syms_binary,
  84. symbol_fle,
  85. mock.ANY,
  86. only_module_header=True) for symbol_fle in symbol_files
  87. ]
  88. return expected_module_calls
  89. def _getExpectedBreakpadExtractionCalls(self, extracted_files,
  90. breakpad_files):
  91. expected_extract_calls = [
  92. mock.call(self.test_dump_syms_binary, extracted_file,
  93. breakpad_files[file_iter])
  94. for file_iter, extracted_file in enumerate(extracted_files)
  95. ]
  96. return expected_extract_calls
  97. def _getAndEnsureExtractedBreakpadFiles(self, extracted_files):
  98. breakpad_files = []
  99. for extracted_file in extracted_files:
  100. breakpad_filename = os.path.basename(extracted_file) + '.breakpad'
  101. breakpad_file = os.path.join(self.test_breakpad_dir, breakpad_filename)
  102. assert (os.path.isfile(breakpad_file))
  103. breakpad_files.append(breakpad_file)
  104. return breakpad_files
  105. def _getAndEnsureExpectedSubtreeBreakpadFiles(self, extracted_files):
  106. breakpad_files = []
  107. for extracted_file in extracted_files:
  108. breakpad_file = extracted_file + '.breakpad'
  109. assert (os.path.isfile(breakpad_file))
  110. breakpad_files.append(breakpad_file)
  111. return breakpad_files
  112. def _checkExtractWithOneBinary(self, dump_syms_path, build_dir, breakpad_dir):
  113. # Create test file in |test_build_dir| and test file in |test_breakpad_dir|.
  114. test_input_file = tempfile.NamedTemporaryFile(suffix='.so', dir=build_dir)
  115. # |test_output_file_path| requires a specific name, so NamedTemporaryFile()
  116. # is not used.
  117. input_file_name = os.path.split(test_input_file.name)[1]
  118. test_output_file_path = '{output_path}.breakpad'.format(
  119. output_path=os.path.join(breakpad_dir, input_file_name))
  120. with open(test_output_file_path, 'w'):
  121. pass
  122. # Create tempfiles that should be ignored when extracting symbol files.
  123. with tempfile.NamedTemporaryFile(
  124. suffix='.TOC', dir=build_dir), tempfile.NamedTemporaryFile(
  125. suffix='.java', dir=build_dir), tempfile.NamedTemporaryFile(
  126. suffix='.zip', dir=build_dir), tempfile.NamedTemporaryFile(
  127. suffix='_apk', dir=build_dir), tempfile.NamedTemporaryFile(
  128. suffix='.so.dwp',
  129. dir=build_dir), tempfile.NamedTemporaryFile(
  130. suffix='.so.dwo',
  131. dir=build_dir), tempfile.NamedTemporaryFile(
  132. suffix='_chromesymbols.zip', dir=build_dir):
  133. breakpad_file_extractor._RunDumpSyms = mock.MagicMock()
  134. breakpad_file_extractor.ExtractBreakpadFiles(dump_syms_path, build_dir,
  135. breakpad_dir)
  136. breakpad_file_extractor._RunDumpSyms.assert_called_once_with(
  137. dump_syms_path, test_input_file.name, test_output_file_path)
  138. # Check that one file exists in the output directory.
  139. self.assertEqual(len(os.listdir(breakpad_dir)), 1)
  140. self.assertEqual(
  141. os.listdir(breakpad_dir)[0],
  142. os.path.basename(test_input_file.name) + '.breakpad')
  143. def testOneBinaryFile(self):
  144. self._checkExtractWithOneBinary(self.test_dump_syms_binary,
  145. self.test_build_dir, self.test_breakpad_dir)
  146. def testDumpSymsInBuildDir(self):
  147. new_dump_syms_path = os.path.join(self.test_build_dir, 'dump_syms')
  148. with open(new_dump_syms_path, 'w'):
  149. pass
  150. self._checkExtractWithOneBinary(new_dump_syms_path, self.test_build_dir,
  151. self.test_breakpad_dir)
  152. def testSymbolsInLibUnstrippedFolder(self):
  153. os.path.join(self.test_build_dir, 'lib.unstripped')
  154. self._checkExtractWithOneBinary(self.test_dump_syms_binary,
  155. self.test_build_dir, self.test_breakpad_dir)
  156. def testMultipleBinaryFiles(self):
  157. # Create files in |test_build_dir|. All files are removed when
  158. # |test_build_dir| is recursively deleted.
  159. symbol_files = []
  160. so_file = os.path.join(self.test_build_dir, 'test_file.so')
  161. with open(so_file, 'w') as _:
  162. pass
  163. symbol_files.append(so_file)
  164. exe_file = os.path.join(self.test_build_dir, 'test_file.exe')
  165. with open(exe_file, 'w') as _:
  166. pass
  167. symbol_files.append(exe_file)
  168. chrome_file = os.path.join(self.test_build_dir, 'chrome')
  169. with open(chrome_file, 'w') as _:
  170. pass
  171. symbol_files.append(chrome_file)
  172. # Form output file paths.
  173. breakpad_file_extractor._RunDumpSyms = mock.MagicMock(
  174. side_effect=self._getDumpSymsMockSideEffect({}))
  175. breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary,
  176. self.test_build_dir,
  177. self.test_breakpad_dir)
  178. # Check that each expected call to _RunDumpSyms() has been made.
  179. breakpad_files = self._getAndEnsureExtractedBreakpadFiles(symbol_files)
  180. expected_calls = self._getExpectedBreakpadExtractionCalls(
  181. symbol_files, breakpad_files)
  182. breakpad_file_extractor._RunDumpSyms.assert_has_calls(expected_calls,
  183. any_order=True)
  184. def testDumpSymsNotFound(self):
  185. breakpad_file_extractor._RunDumpSyms = mock.MagicMock()
  186. exception_msg = 'dump_syms binary not found.'
  187. with self.assertRaises(Exception) as e:
  188. breakpad_file_extractor.ExtractBreakpadFiles('fake/path/dump_syms',
  189. self.test_build_dir,
  190. self.test_breakpad_dir)
  191. self.assertIn(exception_msg, str(e.exception))
  192. def testFakeDirectories(self):
  193. breakpad_file_extractor._RunDumpSyms = mock.MagicMock()
  194. exception_msg = 'Invalid breakpad output directory'
  195. with self.assertRaises(Exception) as e:
  196. breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary,
  197. self.test_build_dir,
  198. 'fake_breakpad_dir')
  199. self.assertIn(exception_msg, str(e.exception))
  200. exception_msg = 'Invalid build directory'
  201. with self.assertRaises(Exception) as e:
  202. breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary,
  203. 'fake_binary_dir',
  204. self.test_breakpad_dir)
  205. self.assertIn(exception_msg, str(e.exception))
  206. def testSymbolizedNoFiles(self):
  207. did_extract = breakpad_file_extractor.ExtractBreakpadFiles(
  208. self.test_dump_syms_binary, self.test_build_dir, self.test_breakpad_dir)
  209. self.assertFalse(did_extract)
  210. def testNotSearchUnstripped(self):
  211. # Make 'lib.unstripped' directory and file. Our script should not run
  212. # dump_syms on this file.
  213. lib_unstripped = os.path.join(self.test_build_dir, 'lib.unstripped')
  214. os.mkdir(lib_unstripped)
  215. lib_unstripped_file = os.path.join(lib_unstripped, 'unstripped.so')
  216. with open(lib_unstripped_file, 'w') as _:
  217. pass
  218. # Make file to run dump_syms on in input directory.
  219. extracted_file_name = 'extracted.so'
  220. extracted_file = os.path.join(self.test_build_dir, extracted_file_name)
  221. with open(extracted_file, 'w') as _:
  222. pass
  223. breakpad_file_extractor._RunDumpSyms = mock.MagicMock()
  224. breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary,
  225. self.test_build_dir,
  226. self.test_breakpad_dir,
  227. search_unstripped=False)
  228. # Check that _RunDumpSyms() only called for extracted file and not the
  229. # lib.unstripped files.
  230. extracted_output_path = '{output_path}.breakpad'.format(
  231. output_path=os.path.join(self.test_breakpad_dir, extracted_file_name))
  232. breakpad_file_extractor._RunDumpSyms.assert_called_once_with(
  233. self.test_dump_syms_binary, extracted_file, extracted_output_path)
  234. def testIgnorePartitionFiles(self):
  235. partition_file = os.path.join(self.test_build_dir, 'partition.so')
  236. with open(partition_file, 'w') as file1:
  237. file1.write(
  238. 'MODULE Linux x86_64 34984AB4EF948C0000000000000000000 name1.so')
  239. did_extract = breakpad_file_extractor.ExtractBreakpadFiles(
  240. self.test_dump_syms_binary, self.test_build_dir, self.test_breakpad_dir)
  241. self.assertFalse(did_extract)
  242. os.remove(partition_file)
  243. def testIgnoreCombinedFiles(self):
  244. combined_file1 = os.path.join(self.test_build_dir, 'chrome_combined.so')
  245. combined_file2 = os.path.join(self.test_build_dir, 'libchrome_combined.so')
  246. with open(combined_file1, 'w') as file1:
  247. file1.write(
  248. 'MODULE Linux x86_64 34984AB4EF948C0000000000000000000 name1.so')
  249. with open(combined_file2, 'w') as file2:
  250. file2.write(
  251. 'MODULE Linux x86_64 34984AB4EF948C0000000000000000000 name2.so')
  252. did_extract = breakpad_file_extractor.ExtractBreakpadFiles(
  253. self.test_dump_syms_binary, self.test_build_dir, self.test_breakpad_dir)
  254. self.assertFalse(did_extract)
  255. os.remove(combined_file1)
  256. os.remove(combined_file2)
  257. def testExtractOnSubtree(self):
  258. # Setup subtree symbol files.
  259. symbol_files, side_effect_map = self._setupSubtreeFiles()
  260. subdir_symbols = symbol_files[0]
  261. unstripped_symbols = symbol_files[1]
  262. # Setup metadata.
  263. metadata = metadata_extractor.MetadataExtractor('trace_processor_shell',
  264. 'trace_file.proto')
  265. metadata.InitializeForTesting(
  266. modules={
  267. '/subdir.so': '34984AB4EF948D',
  268. '/unstripped.so': '34984AB4EF948C0000000000000000000'
  269. })
  270. extracted_files = [subdir_symbols, unstripped_symbols]
  271. # Setup |_RunDumpSyms| mock for module ID optimization.
  272. breakpad_file_extractor._RunDumpSyms = mock.MagicMock(
  273. side_effect=self._getDumpSymsMockSideEffect(side_effect_map))
  274. breakpad_file_extractor.ExtractBreakpadOnSubtree(self.test_breakpad_dir,
  275. metadata,
  276. self.test_dump_syms_binary)
  277. # Ensure correct |_RunDumpSyms| calls.
  278. expected_module_calls = self._getExpectedModuleExtractionCalls(symbol_files)
  279. breakpad_files = self._getAndEnsureExpectedSubtreeBreakpadFiles(
  280. extracted_files)
  281. expected_extract_calls = self._getExpectedBreakpadExtractionCalls(
  282. extracted_files, breakpad_files)
  283. breakpad_file_extractor._RunDumpSyms.assert_has_calls(
  284. expected_module_calls + expected_extract_calls, any_order=True)
  285. def testSubtreeNoFilesExtracted(self):
  286. # Setup subtree symbol files. No files to be extracted.
  287. symbol_files, side_effect_map = self._setupSubtreeFiles()
  288. # Empty set of module IDs to extract. Nothing should be extracted.
  289. metadata = metadata_extractor.MetadataExtractor('trace_processor_shell',
  290. 'trace_file.proto')
  291. metadata.InitializeForTesting(modules={})
  292. # Setup |_RunDumpSyms| mock for module ID optimization.
  293. breakpad_file_extractor._RunDumpSyms = mock.MagicMock(
  294. side_effect=self._getDumpSymsMockSideEffect(side_effect_map))
  295. exception_msg = (
  296. 'No breakpad symbols could be extracted from files in the subtree: ' +
  297. self.test_breakpad_dir)
  298. with self.assertRaises(Exception) as e:
  299. breakpad_file_extractor.ExtractBreakpadOnSubtree(
  300. self.test_breakpad_dir, metadata, self.test_dump_syms_binary)
  301. self.assertIn(exception_msg, str(e.exception))
  302. # Should be calls to extract module ID, but none to extract breakpad.
  303. expected_module_calls = self._getExpectedModuleExtractionCalls(symbol_files)
  304. breakpad_file_extractor._RunDumpSyms.assert_has_calls(expected_module_calls,
  305. any_order=True)
  306. def testFindOnSubtree(self):
  307. # Setup subtree symbol files.
  308. _, side_effect_map = self._setupSubtreeFiles()
  309. # Setup |_RunDumpSyms| mock for module ID optimization.
  310. breakpad_file_extractor._RunDumpSyms = mock.MagicMock(
  311. side_effect=self._getDumpSymsMockSideEffect(side_effect_map))
  312. found = get_symbols_util.FindMatchingModule(
  313. self.test_breakpad_dir, self.test_dump_syms_binary,
  314. '34984AB4EF948C0000000000000000000')
  315. self.assertIn('subdir.so', found)
  316. def testNotFindOnSubtree(self):
  317. # Setup subtree symbol files.
  318. _, side_effect_map = self._setupSubtreeFiles()
  319. # Setup |_RunDumpSyms| mock for module ID optimization.
  320. breakpad_file_extractor._RunDumpSyms = mock.MagicMock(
  321. side_effect=self._getDumpSymsMockSideEffect(side_effect_map))
  322. found = get_symbols_util.FindMatchingModule(self.test_breakpad_dir,
  323. self.test_dump_syms_binary,
  324. 'NOTFOUND')
  325. self.assertIsNone(found)
  326. if __name__ == '__main__':
  327. unittest.main()