chromium/tools/tracing/breakpad_file_extractor_unittest.py

#!/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()