#!/usr/bin/env vpython3
# Copyright 2021 The Chromium Authors
# 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()