chromium/tools/clang/scripts/remote_link_unit_tests.py

#! /usr/bin/env python3
# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Unit tests for remote_link.
#
# Usage:
#
#   tools/clang/scripts/remote_link_unit_tests.py
#
# A coverage report combining these tests with the integration tests
# in remote_link_integration_tests.py can be generated by running:
#
#   env COVERAGE_FILE=.coverage.unit python3 third_party/pycoverage run \
#     tools/clang/scripts/remote_link_unit_tests.py
#   env COVERAGE_FILE=.coverage.integration python3 third_party/pycoverage \
#     run tools/clang/scripts/remote_link_integration_tests.py
#   python3 third_party/pycoverage combine
#   python3 third_party/pycoverage html
#
# The report will be available as htmlcov/index.html

import remote_ld
import remote_link

import os
import unittest
from unittest import mock

from remote_link_test_utils import named_directory, working_directory


class FakeFs(object):
  """
  Context manager that mocks the functions through which remote_link
  interacts with the filesystem.
  """

  def __init__(self, bitcode_files=None, other_files=None):
    self.bitcode_files = set(bitcode_files or [])
    self.other_files = set(other_files or [])

    def ensure_file(path):
      self.other_files.add(path)

    def exists(path):
      return path in self.bitcode_files or path in self.other_files

    def is_bitcode_file(path):
      return path in self.bitcode_files

    self.mock_ensure_file = mock.patch('remote_link.ensure_file', ensure_file)
    self.mock_exists = mock.patch('os.path.exists', exists)
    self.mock_is_bitcode_file = mock.patch('remote_link.is_bitcode_file',
                                           is_bitcode_file)

  def __enter__(self):
    self.mock_ensure_file.start()
    self.mock_exists.start()
    self.mock_is_bitcode_file.start()
    return self

  def __exit__(self, exnty, *args, **kwargs):
    self.mock_is_bitcode_file.stop()
    self.mock_exists.stop()
    self.mock_ensure_file.stop()
    return exnty is None


class RemoteLinkUnitTest(unittest.TestCase):
  """
  Unit tests for remote_link.
  """

  def test_analyze_expanded_args_nocodegen(self):
    with FakeFs(other_files=['foo.o', 'bar.o']):
      self.assertIsNone(remote_ld.RemoteLinkUnix().analyze_expanded_args(
          ['clang', 'foo.o', 'bar.o', '-o', 'foo'], 'foo', 'clang', 'lto.foo',
          'common', False))

  def test_analyze_expanded_args_one_codegen(self):
    with FakeFs(bitcode_files=['foo.o'], other_files=['bar.o']):
      result = remote_ld.RemoteLinkUnix().analyze_expanded_args(
          ['clang', 'foo.o', 'bar.o', '-o', 'foo'], 'foo', 'clang', 'lto.foo',
          'common', False)
      self.assertIsNotNone(result)
      self.assertNotEqual(len(result.codegen), 0)
      self.assertEqual(result.codegen[0][1], 'foo.o')
      self.assertEqual(len(result.codegen), 1)
      self.assertIn('foo.o', result.index_params)
      self.assertIn('bar.o', result.index_params)
      self.assertIn('bar.o', result.final_params)
      # foo.o should not be in final_params because it will be added via
      # the used object file.
      self.assertNotIn('foo.o', result.final_params)

  def test_analyze_expanded_args_params(self):
    with FakeFs(bitcode_files=['foo.o']):
      result = remote_ld.RemoteLinkUnix().analyze_expanded_args([
          'clang', '-O2', '--target=arm-none-eabi', '-march=armv7-a',
          '-flto=thin', '-fsplit-lto-unit', '-fwhole-program-vtables',
          '-fsanitize=cfi', '-g', '-gsplit-dwarf', '-mllvm',
          '-generate-type-units', 'foo.o', '-o', 'foo'
      ], 'foo', 'clang', 'lto.foo', 'common', False)
      self.assertIsNotNone(result)
      self.assertIn('-Wl,-plugin-opt=obj-path=lto.foo/foo.split.o',
                    result.index_params)
      self.assertIn('-O2', result.index_params)
      self.assertIn('--target=arm-none-eabi', result.codegen_params)
      self.assertIn('-march=armv7-a', result.codegen_params)
      self.assertIn('-g', result.index_params)
      self.assertIn('-gsplit-dwarf', result.index_params)
      self.assertIn('-mllvm -generate-type-units',
                    ' '.join(result.index_params))
      self.assertIn('-flto=thin', result.index_params)
      self.assertIn('-fwhole-program-vtables', result.index_params)
      self.assertIn('-fsanitize=cfi', result.index_params)

      self.assertIn('-O2', result.codegen_params)
      self.assertIn('--target=arm-none-eabi', result.codegen_params)
      self.assertIn('-march=armv7-a', result.codegen_params)
      self.assertIn('-gsplit-dwarf', result.codegen_params)
      self.assertIn('-mllvm -generate-type-units',
                    ' '.join(result.codegen_params))
      self.assertNotIn('-flto=thin', result.codegen_params)
      self.assertNotIn('-fwhole-program-vtables', result.codegen_params)
      self.assertNotIn('-fsanitize=cfi', result.codegen_params)

      self.assertIn('-flto=thin', result.final_params)

  def test_codegen_params_default(self):
    with FakeFs(bitcode_files=['foo.o'], other_files=['bar.o']):
      result = remote_ld.RemoteLinkUnix().analyze_expanded_args(
          ['clang', 'foo.o', 'bar.o', '-o', 'foo'], 'foo', 'clang', 'lto.foo',
          'common', False)
      # Codegen optimization level should default to 2.
      self.assertIn('-O2', result.codegen_params)
      # -fdata-sections and -ffunction-sections default to on to match the
      # behavior of local linking.
      self.assertIn('-fdata-sections', result.codegen_params)
      self.assertIn('-ffunction-sections', result.codegen_params)

  def test_codegen_params_default_cl(self):
    with FakeFs(bitcode_files=['foo.obj'], other_files=['bar.obj']):
      result = remote_link.RemoteLinkWindows().analyze_expanded_args(
          ['clang-cl', 'foo.obj', 'bar.obj', '-Fefoo.exe'], 'foo.exe',
          'clang-cl', 'lto.foo', 'common', False)
      # Codegen optimization level should default to 2.
      self.assertIn('-O2', result.codegen_params)
      # -Gw and -Gy default to on to match the behavior of local linking.
      self.assertIn('-Gw', result.codegen_params)
      self.assertIn('-Gy', result.codegen_params)

  def test_codegen_params_no_data_sections(self):
    with FakeFs(bitcode_files=['foo.o'], other_files=['bar.o']):
      result = remote_ld.RemoteLinkUnix().analyze_expanded_args(
          ['clang', '-fno-data-sections', 'foo.o', 'bar.o', '-o', 'foo'], 'foo',
          'clang', 'lto.foo', 'common', False)
      self.assertNotIn('-fdata-sections', result.codegen_params)
      self.assertIn('-ffunction-sections', result.codegen_params)

  def test_codegen_params_no_function_sections(self):
    with FakeFs(bitcode_files=['foo.o'], other_files=['bar.o']):
      result = remote_ld.RemoteLinkUnix().analyze_expanded_args(
          ['clang', '-fno-function-sections', 'foo.o', 'bar.o', '-o', 'foo'],
          'foo', 'clang', 'lto.foo', 'common', False)
      self.assertIn('-fdata-sections', result.codegen_params)
      self.assertNotIn('-ffunction-sections', result.codegen_params)

  def test_codegen_params_no_data_sections_cl(self):
    with FakeFs(bitcode_files=['foo.obj'], other_files=['bar.obj']):
      result = remote_link.RemoteLinkWindows().analyze_expanded_args(
          ['clang-cl', '/Gw-', 'foo.obj', 'bar.obj', '/Fefoo.exe'], 'foo.exe',
          'clang-cl', 'lto.foo', 'common', False)
      self.assertNotIn('-fdata-sections', result.codegen_params)
      self.assertNotIn('-Gw', result.codegen_params)
      self.assertNotIn('/Gw', result.codegen_params)
      self.assertIn('-Gy', result.codegen_params)

  def test_codegen_params_no_function_sections_cl(self):
    with FakeFs(bitcode_files=['foo.obj'], other_files=['bar.obj']):
      result = remote_link.RemoteLinkWindows().analyze_expanded_args(
          ['clang-cl', '/Gy-', 'foo.obj', 'bar.obj', '/Fefoo.exe'], 'foo.exe',
          'clang-cl', 'lto.foo', 'common', False)
      self.assertIn('-Gw', result.codegen_params)
      self.assertNotIn('-ffunction-sections', result.codegen_params)
      self.assertNotIn('-Gy', result.codegen_params)
      self.assertNotIn('/Gy', result.codegen_params)

  def test_codegen_params_explicit_data_and_function_sections(self):
    with FakeFs(bitcode_files=['foo.o'], other_files=['bar.o']):
      result = remote_ld.RemoteLinkUnix().analyze_expanded_args([
          'clang', '-ffunction-sections', '-fdata-sections', 'foo.o', 'bar.o',
          '-o', 'foo'
      ], 'foo', 'clang', 'lto.foo', 'common', False)
      self.assertIn('-fdata-sections', result.codegen_params)
      self.assertIn('-ffunction-sections', result.codegen_params)

  def test_codegen_params_explicit_data_and_function_sections_cl(self):
    with FakeFs(bitcode_files=['foo.obj'], other_files=['bar.obj']):
      result = remote_link.RemoteLinkWindows().analyze_expanded_args(
          ['clang-cl', '/Gy', '-Gw', 'foo.obj', 'bar.obj', '/Fefoo.exe'],
          'foo.exe', 'clang-cl', 'lto.foo', 'common', False)
      self.assertIn('-Gw', result.codegen_params)
      self.assertIn('/Gy', result.codegen_params)
      self.assertNotIn('-fdata-sections', result.codegen_params)
      self.assertNotIn('-ffunction-sections', result.codegen_params)

  def test_ensure_file_no_dir(self):
    with named_directory() as d, working_directory(d):
      self.assertFalse(os.path.exists('test'))
      remote_link.ensure_file('test')
      self.assertTrue(os.path.exists('test'))

  def test_ensure_file_existing(self):
    with named_directory() as d, working_directory(d):
      self.assertFalse(os.path.exists('foo/test'))
      remote_link.ensure_file('foo/test')
      self.assertTrue(os.path.exists('foo/test'))
      os.utime('foo/test', (0, 0))
      statresult = os.stat('foo/test')
      remote_link.ensure_file('foo/test')
      self.assertTrue(os.path.exists('foo/test'))
      newstatresult = os.stat('foo/test')
      self.assertEqual(newstatresult.st_mtime, statresult.st_mtime)

  def test_ensure_file_error(self):
    with named_directory() as d, working_directory(d):
      self.assertFalse(os.path.exists('test'))
      remote_link.ensure_file('test')
      self.assertTrue(os.path.exists('test'))
      self.assertRaises(OSError, remote_link.ensure_file, 'test/impossible')

  def test_transform_codegen_param_on_mllvm(self):
    # Regression test for crbug.com/1135234
    link = remote_ld.RemoteLinkUnix()
    self.assertEqual(
        link.transform_codegen_param_common('-mllvm,-import-instr-limit=20'),
        ['-mllvm', '-import-instr-limit=20'])


if __name__ == '__main__':
  unittest.main()