| #!/usr/bin/env vpython3 |
| # Copyright 2021 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from logging import exception |
| import os |
| import shutil |
| import sys |
| import tempfile |
| import unittest |
| |
| import breakpad_file_extractor |
| import get_symbols_util |
| |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, 'perf')) |
| |
| from core import path_util |
| |
| path_util.AddPyUtilsToPath() |
| path_util.AddTracingToPath() |
| |
| import metadata_extractor |
| |
| import mock |
| |
| |
| class ExtractBreakpadTestCase(unittest.TestCase): |
| |
| def setUp(self): |
| # Create test inputs for ExtractBreakpadFiles() function. |
| self.test_build_dir = tempfile.mkdtemp() |
| self.test_breakpad_dir = tempfile.mkdtemp() |
| self.test_dump_syms_dir = tempfile.mkdtemp() |
| |
| # NamedTemporaryFile() is hard coded to have a set of random 8 characters |
| # appended to whatever prefix is given. Those characters can't be easily |
| # removed, so |self.test_dump_syms_binary| is opened this way. |
| self.test_dump_syms_binary = os.path.join(self.test_dump_syms_dir, |
| 'dump_syms') |
| with open(self.test_dump_syms_binary, 'w'): |
| pass |
| |
| # Stash function. |
| self.RunDumpSyms_stash = breakpad_file_extractor._RunDumpSyms |
| |
| def tearDown(self): |
| shutil.rmtree(self.test_build_dir) |
| shutil.rmtree(self.test_breakpad_dir) |
| shutil.rmtree(self.test_dump_syms_dir) |
| |
| # Unstash function. |
| breakpad_file_extractor._RunDumpSyms = self.RunDumpSyms_stash |
| |
| def _setupSubtreeFiles(self): |
| # Create subtree directory structure. All files deleted when |
| # |test_breakpad_dir| is recursively deleted. |
| out = tempfile.mkdtemp(dir=self.test_breakpad_dir) |
| release = tempfile.mkdtemp(dir=out) |
| subdir = tempfile.mkdtemp(dir=release) |
| unstripped_dir = os.path.join(release, 'lib.unstripped') |
| os.mkdir(unstripped_dir) |
| |
| # Create symbol files. |
| symbol_files = [] |
| symbol_files.append(os.path.join(subdir, 'subdir.so')) |
| symbol_files.append(os.path.join(unstripped_dir, 'unstripped.so')) |
| symbol_files.append(os.path.join(unstripped_dir, 'unstripped2.so')) |
| |
| for new_file in symbol_files: |
| with open(new_file, 'w') as _: |
| pass |
| |
| # Build side effect mapping. |
| side_effect_map = { |
| symbol_files[0]: |
| 'MODULE Android x86_64 34984AB4EF948C0000000000000000000 subdir.so', |
| symbol_files[1]: 'MODULE Android x86_64 34984AB4EF948D unstripped.so', |
| symbol_files[2]: 'MODULE Android x86_64 34984AB4EF949A unstripped2.so' |
| } |
| |
| return symbol_files, side_effect_map |
| |
| def _getDumpSymsMockSideEffect(self, side_effect_map): |
| def run_dumpsyms_side_effect(dump_syms_binary, |
| input_file_path, |
| output_file_path, |
| only_module_header=False): |
| self.assertEqual(self.test_dump_syms_binary, dump_syms_binary) |
| if only_module_header: |
| # Extract Module ID. |
| with open(output_file_path, 'w') as f: |
| # Write the correct module header into the output f |
| f.write(side_effect_map[input_file_path]) |
| else: |
| # Extract breakpads. |
| with open(output_file_path, 'w'): |
| pass |
| return True |
| |
| return run_dumpsyms_side_effect |
| |
| def _getExpectedModuleExtractionCalls(self, symbol_files): |
| expected_module_calls = [ |
| mock.call(self.test_dump_syms_binary, |
| symbol_fle, |
| mock.ANY, |
| only_module_header=True) for symbol_fle in symbol_files |
| ] |
| return expected_module_calls |
| |
| def _getExpectedBreakpadExtractionCalls(self, extracted_files, |
| breakpad_files): |
| expected_extract_calls = [ |
| mock.call(self.test_dump_syms_binary, extracted_file, |
| breakpad_files[file_iter]) |
| for file_iter, extracted_file in enumerate(extracted_files) |
| ] |
| return expected_extract_calls |
| |
| def _getAndEnsureExtractedBreakpadFiles(self, extracted_files): |
| breakpad_files = [] |
| for extracted_file in extracted_files: |
| breakpad_filename = os.path.basename(extracted_file) + '.breakpad' |
| breakpad_file = os.path.join(self.test_breakpad_dir, breakpad_filename) |
| assert (os.path.isfile(breakpad_file)) |
| breakpad_files.append(breakpad_file) |
| return breakpad_files |
| |
| def _getAndEnsureExpectedSubtreeBreakpadFiles(self, extracted_files): |
| breakpad_files = [] |
| for extracted_file in extracted_files: |
| breakpad_file = extracted_file + '.breakpad' |
| assert (os.path.isfile(breakpad_file)) |
| breakpad_files.append(breakpad_file) |
| return breakpad_files |
| |
| def _checkExtractWithOneBinary(self, dump_syms_path, build_dir, breakpad_dir): |
| # Create test file in |test_build_dir| and test file in |test_breakpad_dir|. |
| test_input_file = tempfile.NamedTemporaryFile(suffix='.so', dir=build_dir) |
| # |test_output_file_path| requires a specific name, so NamedTemporaryFile() |
| # is not used. |
| input_file_name = os.path.split(test_input_file.name)[1] |
| test_output_file_path = '{output_path}.breakpad'.format( |
| output_path=os.path.join(breakpad_dir, input_file_name)) |
| with open(test_output_file_path, 'w'): |
| pass |
| |
| # Create tempfiles that should be ignored when extracting symbol files. |
| with tempfile.NamedTemporaryFile( |
| suffix='.TOC', dir=build_dir), tempfile.NamedTemporaryFile( |
| suffix='.java', dir=build_dir), tempfile.NamedTemporaryFile( |
| suffix='.zip', dir=build_dir), tempfile.NamedTemporaryFile( |
| suffix='_apk', dir=build_dir), tempfile.NamedTemporaryFile( |
| suffix='.so.dwp', |
| dir=build_dir), tempfile.NamedTemporaryFile( |
| suffix='.so.dwo', |
| dir=build_dir), tempfile.NamedTemporaryFile( |
| suffix='_chromesymbols.zip', dir=build_dir): |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock() |
| breakpad_file_extractor.ExtractBreakpadFiles(dump_syms_path, build_dir, |
| breakpad_dir) |
| |
| breakpad_file_extractor._RunDumpSyms.assert_called_once_with( |
| dump_syms_path, test_input_file.name, test_output_file_path) |
| |
| # Check that one file exists in the output directory. |
| self.assertEqual(len(os.listdir(breakpad_dir)), 1) |
| self.assertEqual( |
| os.listdir(breakpad_dir)[0], |
| os.path.basename(test_input_file.name) + '.breakpad') |
| |
| def testOneBinaryFile(self): |
| self._checkExtractWithOneBinary(self.test_dump_syms_binary, |
| self.test_build_dir, self.test_breakpad_dir) |
| |
| def testDumpSymsInBuildDir(self): |
| new_dump_syms_path = os.path.join(self.test_build_dir, 'dump_syms') |
| with open(new_dump_syms_path, 'w'): |
| pass |
| self._checkExtractWithOneBinary(new_dump_syms_path, self.test_build_dir, |
| self.test_breakpad_dir) |
| |
| def testSymbolsInLibUnstrippedFolder(self): |
| os.path.join(self.test_build_dir, 'lib.unstripped') |
| self._checkExtractWithOneBinary(self.test_dump_syms_binary, |
| self.test_build_dir, self.test_breakpad_dir) |
| |
| def testMultipleBinaryFiles(self): |
| # Create files in |test_build_dir|. All files are removed when |
| # |test_build_dir| is recursively deleted. |
| symbol_files = [] |
| so_file = os.path.join(self.test_build_dir, 'test_file.so') |
| with open(so_file, 'w') as _: |
| pass |
| symbol_files.append(so_file) |
| exe_file = os.path.join(self.test_build_dir, 'test_file.exe') |
| with open(exe_file, 'w') as _: |
| pass |
| symbol_files.append(exe_file) |
| chrome_file = os.path.join(self.test_build_dir, 'chrome') |
| with open(chrome_file, 'w') as _: |
| pass |
| symbol_files.append(chrome_file) |
| |
| # Form output file paths. |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock( |
| side_effect=self._getDumpSymsMockSideEffect({})) |
| breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary, |
| self.test_build_dir, |
| self.test_breakpad_dir) |
| |
| # Check that each expected call to _RunDumpSyms() has been made. |
| breakpad_files = self._getAndEnsureExtractedBreakpadFiles(symbol_files) |
| expected_calls = self._getExpectedBreakpadExtractionCalls( |
| symbol_files, breakpad_files) |
| breakpad_file_extractor._RunDumpSyms.assert_has_calls(expected_calls, |
| any_order=True) |
| |
| def testDumpSymsNotFound(self): |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock() |
| exception_msg = 'dump_syms binary not found.' |
| with self.assertRaises(Exception) as e: |
| breakpad_file_extractor.ExtractBreakpadFiles('fake/path/dump_syms', |
| self.test_build_dir, |
| self.test_breakpad_dir) |
| self.assertIn(exception_msg, str(e.exception)) |
| |
| def testFakeDirectories(self): |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock() |
| exception_msg = 'Invalid breakpad output directory' |
| with self.assertRaises(Exception) as e: |
| breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary, |
| self.test_build_dir, |
| 'fake_breakpad_dir') |
| self.assertIn(exception_msg, str(e.exception)) |
| |
| exception_msg = 'Invalid build directory' |
| with self.assertRaises(Exception) as e: |
| breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary, |
| 'fake_binary_dir', |
| self.test_breakpad_dir) |
| self.assertIn(exception_msg, str(e.exception)) |
| |
| def testSymbolizedNoFiles(self): |
| did_extract = breakpad_file_extractor.ExtractBreakpadFiles( |
| self.test_dump_syms_binary, self.test_build_dir, self.test_breakpad_dir) |
| self.assertFalse(did_extract) |
| |
| def testNotSearchUnstripped(self): |
| # Make 'lib.unstripped' directory and file. Our script should not run |
| # dump_syms on this file. |
| lib_unstripped = os.path.join(self.test_build_dir, 'lib.unstripped') |
| os.mkdir(lib_unstripped) |
| lib_unstripped_file = os.path.join(lib_unstripped, 'unstripped.so') |
| with open(lib_unstripped_file, 'w') as _: |
| pass |
| |
| # Make file to run dump_syms on in input directory. |
| extracted_file_name = 'extracted.so' |
| extracted_file = os.path.join(self.test_build_dir, extracted_file_name) |
| with open(extracted_file, 'w') as _: |
| pass |
| |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock() |
| breakpad_file_extractor.ExtractBreakpadFiles(self.test_dump_syms_binary, |
| self.test_build_dir, |
| self.test_breakpad_dir, |
| search_unstripped=False) |
| |
| # Check that _RunDumpSyms() only called for extracted file and not the |
| # lib.unstripped files. |
| extracted_output_path = '{output_path}.breakpad'.format( |
| output_path=os.path.join(self.test_breakpad_dir, extracted_file_name)) |
| breakpad_file_extractor._RunDumpSyms.assert_called_once_with( |
| self.test_dump_syms_binary, extracted_file, extracted_output_path) |
| |
| def testIgnorePartitionFiles(self): |
| partition_file = os.path.join(self.test_build_dir, 'partition.so') |
| with open(partition_file, 'w') as file1: |
| file1.write( |
| 'MODULE Linux x86_64 34984AB4EF948C0000000000000000000 name1.so') |
| |
| did_extract = breakpad_file_extractor.ExtractBreakpadFiles( |
| self.test_dump_syms_binary, self.test_build_dir, self.test_breakpad_dir) |
| self.assertFalse(did_extract) |
| |
| os.remove(partition_file) |
| |
| def testIgnoreCombinedFiles(self): |
| combined_file1 = os.path.join(self.test_build_dir, 'chrome_combined.so') |
| combined_file2 = os.path.join(self.test_build_dir, 'libchrome_combined.so') |
| with open(combined_file1, 'w') as file1: |
| file1.write( |
| 'MODULE Linux x86_64 34984AB4EF948C0000000000000000000 name1.so') |
| with open(combined_file2, 'w') as file2: |
| file2.write( |
| 'MODULE Linux x86_64 34984AB4EF948C0000000000000000000 name2.so') |
| |
| did_extract = breakpad_file_extractor.ExtractBreakpadFiles( |
| self.test_dump_syms_binary, self.test_build_dir, self.test_breakpad_dir) |
| self.assertFalse(did_extract) |
| |
| os.remove(combined_file1) |
| os.remove(combined_file2) |
| |
| def testExtractOnSubtree(self): |
| # Setup subtree symbol files. |
| symbol_files, side_effect_map = self._setupSubtreeFiles() |
| subdir_symbols = symbol_files[0] |
| unstripped_symbols = symbol_files[1] |
| |
| # Setup metadata. |
| metadata = metadata_extractor.MetadataExtractor('trace_processor_shell', |
| 'trace_file.proto') |
| metadata.InitializeForTesting( |
| modules={ |
| '/subdir.so': '34984AB4EF948D', |
| '/unstripped.so': '34984AB4EF948C0000000000000000000' |
| }) |
| extracted_files = [subdir_symbols, unstripped_symbols] |
| |
| # Setup |_RunDumpSyms| mock for module ID optimization. |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock( |
| side_effect=self._getDumpSymsMockSideEffect(side_effect_map)) |
| breakpad_file_extractor.ExtractBreakpadOnSubtree(self.test_breakpad_dir, |
| metadata, |
| self.test_dump_syms_binary) |
| |
| # Ensure correct |_RunDumpSyms| calls. |
| expected_module_calls = self._getExpectedModuleExtractionCalls(symbol_files) |
| |
| breakpad_files = self._getAndEnsureExpectedSubtreeBreakpadFiles( |
| extracted_files) |
| expected_extract_calls = self._getExpectedBreakpadExtractionCalls( |
| extracted_files, breakpad_files) |
| |
| breakpad_file_extractor._RunDumpSyms.assert_has_calls( |
| expected_module_calls + expected_extract_calls, any_order=True) |
| |
| def testSubtreeNoFilesExtracted(self): |
| # Setup subtree symbol files. No files to be extracted. |
| symbol_files, side_effect_map = self._setupSubtreeFiles() |
| |
| # Empty set of module IDs to extract. Nothing should be extracted. |
| metadata = metadata_extractor.MetadataExtractor('trace_processor_shell', |
| 'trace_file.proto') |
| metadata.InitializeForTesting(modules={}) |
| |
| # Setup |_RunDumpSyms| mock for module ID optimization. |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock( |
| side_effect=self._getDumpSymsMockSideEffect(side_effect_map)) |
| exception_msg = ( |
| 'No breakpad symbols could be extracted from files in the subtree: ' + |
| self.test_breakpad_dir) |
| with self.assertRaises(Exception) as e: |
| breakpad_file_extractor.ExtractBreakpadOnSubtree( |
| self.test_breakpad_dir, metadata, self.test_dump_syms_binary) |
| self.assertIn(exception_msg, str(e.exception)) |
| |
| # Should be calls to extract module ID, but none to extract breakpad. |
| expected_module_calls = self._getExpectedModuleExtractionCalls(symbol_files) |
| breakpad_file_extractor._RunDumpSyms.assert_has_calls(expected_module_calls, |
| any_order=True) |
| |
| def testFindOnSubtree(self): |
| # Setup subtree symbol files. |
| _, side_effect_map = self._setupSubtreeFiles() |
| |
| # Setup |_RunDumpSyms| mock for module ID optimization. |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock( |
| side_effect=self._getDumpSymsMockSideEffect(side_effect_map)) |
| |
| found = get_symbols_util.FindMatchingModule( |
| self.test_breakpad_dir, self.test_dump_syms_binary, |
| '34984AB4EF948C0000000000000000000') |
| self.assertIn('subdir.so', found) |
| |
| def testNotFindOnSubtree(self): |
| # Setup subtree symbol files. |
| _, side_effect_map = self._setupSubtreeFiles() |
| |
| # Setup |_RunDumpSyms| mock for module ID optimization. |
| breakpad_file_extractor._RunDumpSyms = mock.MagicMock( |
| side_effect=self._getDumpSymsMockSideEffect(side_effect_map)) |
| |
| found = get_symbols_util.FindMatchingModule(self.test_breakpad_dir, |
| self.test_dump_syms_binary, |
| 'NOTFOUND') |
| self.assertIsNone(found) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |