chromium/tools/clang/scripts/remote_link_integration_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.

# Integration tests for remote_link.
#
# Usage:
#
# Ensure that rewrapper, llvm-objdump, and llvm-dwarfdump are in your
# PATH.
# Then run:
#
#   tools/clang/scripts/remote_link_integration_tests.py
#
# See also remote_link_unit_tests.py, which contains unit tests and
# instructions for generating coverage information.

import remote_ld
import remote_link

from io import StringIO
import os
import re
import shlex
import subprocess
import unittest
from unittest import mock

from remote_link_test_utils import named_directory, working_directory

# Path constants.
CHROMIUM_DIR = os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..', '..', '..'))
LLVM_BIN_DIR = os.path.join(CHROMIUM_DIR, 'third_party', 'llvm-build',
                            'Release+Asserts', 'bin')


def _create_inputs(path):
  """
  Creates input files under path.
  """
  with open(os.path.join(path, 'main.cpp'), 'w') as f:
    f.write('extern int foo();\n'
            'int main(int argc, char *argv[]) {\n  return foo();\n}\n')
  with open(os.path.join(path, 'foo.cpp'), 'w') as f:
    f.write('int foo() {\n  return 12;\n}\n')
  with open(os.path.join(path, 'bar.cpp'), 'w') as f:
    f.write('int bar() {\n  return 9;\n}\n')


def _lto_args(generate_bitcode):
  """
  Returns list of arguments to clang to generate bitcode or not.
  """
  if generate_bitcode:
    return ['-flto=thin']
  else:
    return []


class RemoteLinkUnixAllowMain(remote_ld.RemoteLinkUnix):
  """
  Same as remote_ld.RemoteLinkUnix, but has "main" on the allow list.
  """

  def __init__(self, *args, **kwargs):
    super(RemoteLinkUnixAllowMain, self).__init__(*args, **kwargs)
    self.ALLOWLIST = {'main'}


class RemoteLinkWindowsAllowMain(remote_link.RemoteLinkWindows):
  """
  Same as remote_ld.RemoteLinkWindows, but has "main" on the allow list.
  """

  def __init__(self, *args, **kwargs):
    super(RemoteLinkWindowsAllowMain, self).__init__(*args, **kwargs)
    self.ALLOWLIST = {'main.exe'}


class RemoteLinkIntegrationTest(unittest.TestCase):
  def clangcl(self):
    return os.path.join(LLVM_BIN_DIR, 'clang-cl' + remote_link.exe_suffix())

  def lld_link(self):
    return os.path.join(LLVM_BIN_DIR, 'lld-link' + remote_link.exe_suffix())

  def llvmar(self):
    return os.path.join(LLVM_BIN_DIR, 'llvm-ar' + remote_link.exe_suffix())

  def test_distributed_lto_common_objs(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      os.makedirs('obj')
      subprocess.check_call([
          self.clangcl(), '-c', '-Os', '-flto=thin', 'main.cpp',
          '-Foobj/main.obj'
      ])
      subprocess.check_call([
          self.clangcl(), '-c', '-Os', '-flto=thin', 'foo.cpp', '-Foobj/foo.obj'
      ])
      subprocess.check_call([
          self.clangcl(), '-c', '-Os', '-flto=thin', 'bar.cpp', '-Foobj/bar.obj'
      ])
      subprocess.check_call([
          self.llvmar(), 'crsT', 'obj/foobar.lib', 'obj/bar.obj', 'obj/foo.obj'
      ])
      with open('main.rsp', 'w') as f:
        f.write('obj/main.obj\n' 'obj/foobar.lib\n')
      with open('my_reclient.sh', 'w') as f:
        f.write('#! /bin/sh\n\nrewrapper "$@"\n')
      os.chmod('my_reclient.sh', 0o755)
      rc = remote_link.RemoteLinkWindows().main([
          'remote_link.py', '--wrapper', './my_reclient.sh', '--ar-path',
          self.llvmar(), '--',
          self.lld_link(), '-nodefaultlib', '-entry:main', '-out:main.exe',
          '@main.rsp'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # Check codegen parameters.
      with open(os.path.join(d, 'lto.main.exe', 'build.ninja')) as f:
        buildrules = f.read()
        codegen_match = re.search('^rule codegen\\b.*?^[^ ]', buildrules,
                                  re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(codegen_match)
        codegen_text = codegen_match.group(0)
        self.assertIn('my_reclient.sh', codegen_text)
        self.assertNotIn('-flto', codegen_text)
        self.assertIn('build common_objs/obj/main.obj.stamp : codegen ',
                      buildrules)
        self.assertIn('build common_objs/obj/foo.obj.stamp : codegen ',
                      buildrules)
        self.assertIn(' index = common_objs/empty.thinlto.bc', buildrules)
        link_match = re.search('^build main.exe : native-link\\b.*?^[^ ]',
                               buildrules, re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(link_match)
        link_text = link_match.group(0)
        self.assertNotIn('main.exe.split.obj', link_text)
      # Check that main does not call foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main.exe'])
      # There are no symbols in the disassembly, but we're expecting two
      # functions, one of which calls the other.
      self.assertTrue(b'call' in disasm or b'jmp' in disasm)

  def test_distributed_lto_allowlist(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      os.makedirs('obj')
      subprocess.check_call([
          self.clangcl(), '-c', '-Os', '-flto=thin', '-m32', 'main.cpp',
          '-Foobj/main.obj'
      ])
      subprocess.check_call([
          self.clangcl(), '-c', '-Os', '-flto=thin', '-m32', 'foo.cpp',
          '-Foobj/foo.obj'
      ])
      subprocess.check_call([
          self.clangcl(), '-c', '-Os', '-flto=thin', '-m32', 'bar.cpp',
          '-Foobj/bar.obj'
      ])
      subprocess.check_call([
          self.llvmar(), 'crsT', 'obj/foobar.lib', 'obj/bar.obj', 'obj/foo.obj'
      ])
      with open('main.rsp', 'w') as f:
        f.write('obj/main.obj\n' 'obj/foobar.lib\n')
      rc = RemoteLinkWindowsAllowMain().main([
          'remote_link.py', '--wrapper', 'rewrapper', '--ar-path',
          self.llvmar(), '--',
          self.lld_link(), '-nodefaultlib', '-entry:main', '-machine:X86',
          '-opt:lldlto=2', '-mllvm:-import-instr-limit=10', '-out:main.exe',
          '@main.rsp'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # Check codegen parameters.
      with open(os.path.join(d, 'lto.main.exe', 'build.ninja')) as f:
        buildrules = f.read()
        codegen_match = re.search('^rule codegen\\b.*?^[^ ]', buildrules,
                                  re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(codegen_match)
        codegen_text = codegen_match.group(0)
        self.assertIn('rewrapper', codegen_text)
        self.assertIn('-m32', codegen_text)
        self.assertIn('-mllvm -import-instr-limit=10', codegen_text)
        self.assertNotIn('-flto', codegen_text)
        self.assertIn('build lto.main.exe/obj/main.obj.stamp : codegen ',
                      buildrules)
        self.assertIn('build lto.main.exe/obj/foo.obj.stamp : codegen ',
                      buildrules)
        link_match = re.search('^build main.exe : native-link\\b.*?^[^ ]',
                               buildrules, re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(link_match)
        link_text = link_match.group(0)
        self.assertIn('main.exe.split.obj', link_text)
      # Check that main does not call foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main.exe'])
      # There are no symbols in the disassembly, but we're expecting a single
      # function, with no calls or jmps.
      self.assertNotIn(b'jmp', disasm)
      self.assertNotIn(b'call', disasm)

  def test_override_allowlist(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      os.makedirs('obj')
      subprocess.check_call([
          self.clangcl(), '-c', '-O2', '-flto=thin', 'main.cpp',
          '-Foobj/main.obj'
      ])
      subprocess.check_call([
          self.clangcl(), '-c', '-O2', '-flto=thin', 'foo.cpp', '-Foobj/foo.obj'
      ])
      rc = remote_link.RemoteLinkWindows().main([
          'remote_link.py', '--generate', '--allowlist', '--ar-path',
          self.llvmar(), '--',
          self.lld_link(), '-nodefaultlib', '-entry:main', '-opt:lldlto=2',
          '-out:main.exe', 'obj/main.obj', 'obj/foo.obj'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # Check that we have rules for main and foo, and that they are
      # not common objects.
      with open(os.path.join(d, 'lto.main.exe', 'build.ninja')) as f:
        buildrules = f.read()
        codegen_match = re.search(r'^rule codegen\b.*?^[^ ]', buildrules,
                                  re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(codegen_match)
        codegen_text = codegen_match.group(0)
        self.assertNotIn('-flto', codegen_text)
        self.assertIn('build lto.main.exe/obj/main.obj.stamp : codegen ',
                      buildrules)
        self.assertIn('build lto.main.exe/obj/foo.obj.stamp : codegen ',
                      buildrules)
        link_match = re.search(r'^build main.exe : native-link\b.*?^[^ ]',
                               buildrules, re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(link_match)


class RemoteLdIntegrationTest(unittest.TestCase):
  def clangxx(self):
    return os.path.join(LLVM_BIN_DIR, 'clang++' + remote_link.exe_suffix())

  def llvmar(self):
    return os.path.join(LLVM_BIN_DIR, 'llvm-ar' + remote_link.exe_suffix())

  def test_nonlto(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', 'main.cpp', '-o', 'main.o'])
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', 'foo.cpp', '-o', 'foo.o'])
      rc = RemoteLinkUnixAllowMain().main([
          'remote_ld.py', '--wrapper', 'rewrapper', '--ar-path',
          self.llvmar(), '--',
          self.clangxx(), '-fuse-ld=lld', 'main.o', 'foo.o', '-o', 'main'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # lto.main directory should not be present.
      self.assertFalse(os.path.exists(os.path.join(d, 'lto.main')))
      # Check that main calls foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main'])
      main_idx = disasm.index(b' <main>:\n')
      after_main_idx = disasm.index(b'\n\n', main_idx)
      main_disasm = disasm[main_idx:after_main_idx]
      self.assertIn(b'foo', main_disasm)

  def test_fallback_lto(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', 'main.cpp', '-o', 'main.o'
      ])
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', '-flto=thin', 'foo.cpp', '-o', 'foo.o'])
      rc = remote_ld.RemoteLinkUnix().main([
          'remote_ld.py', '--wrapper', 'rewrapper', '--ar-path',
          self.llvmar(), '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', 'main.o', 'foo.o', '-o',
          'main'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # lto.main directory should not be present.
      self.assertFalse(os.path.exists(os.path.join(d, 'lto.main')))
      # Check that main does not call foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main'])
      main_idx = disasm.index(b' <main>:\n')
      after_main_idx = disasm.index(b'\n\n', main_idx)
      main_disasm = disasm[main_idx:after_main_idx]
      self.assertNotIn(b'foo', main_disasm)

  def test_distributed_lto(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', 'main.cpp', '-o', 'main.o'
      ])
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', '-flto=thin', 'foo.cpp', '-o', 'foo.o'])
      rc = RemoteLinkUnixAllowMain().main([
          'remote_ld.py', '-j', '16', '--ar-path',
          self.llvmar(), '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', 'main.o', 'foo.o', '-o',
          'main'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # build.ninja file should have rewrapper invocations in it.
      with open(os.path.join(d, 'lto.main', 'build.ninja')) as f:
        buildrules = f.read()
        self.assertIn('rewrapper ', buildrules)
        self.assertIn('build lto.main/main.o.stamp : codegen ', buildrules)
        self.assertIn('build lto.main/foo.o.stamp : codegen ', buildrules)
      # Check that main does not call foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main'])
      main_idx = disasm.index(b' <main>:\n')
      after_main_idx = disasm.index(b'\n\n', main_idx)
      main_disasm = disasm[main_idx:after_main_idx]
      self.assertNotIn(b'foo', main_disasm)

  def test_distributed_lto_thin_archive_same_dir(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', 'main.cpp', '-o', 'main.o'
      ])
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', '-flto=thin', 'foo.cpp', '-o', 'foo.o'])
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', '-flto=thin', 'bar.cpp', '-o', 'bar.o'])
      subprocess.check_call(
          [self.llvmar(), 'crsT', 'libfoobar.a', 'bar.o', 'foo.o'])
      rc = RemoteLinkUnixAllowMain().main([
          'remote_ld.py', '--ar-path',
          self.llvmar(), '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', 'main.o', 'libfoobar.a',
          '-o', 'main'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # build.ninja file should have rewrapper invocations in it.
      with open(os.path.join(d, 'lto.main', 'build.ninja')) as f:
        buildrules = f.read()
        self.assertIn('rewrapper ', buildrules)
        self.assertIn('build lto.main/main.o.stamp : codegen ', buildrules)
        self.assertIn('build lto.main/foo.o.stamp : codegen ', buildrules)
      # Check that main does not call foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main'])
      main_idx = disasm.index(b' <main>:\n')
      after_main_idx = disasm.index(b'\n\n', main_idx)
      main_disasm = disasm[main_idx:after_main_idx]
      self.assertNotIn(b'foo', main_disasm)

  def test_distributed_lto_thin_archive_subdir(self):
    self.run_archive_test(bitcode_archive=True,
                          bitcode_main=True,
                          thin_archive=True)

  def test_distributed_machine_code_thin_archive_bitcode_main_subdir(self):
    self.run_archive_test(bitcode_archive=False,
                          bitcode_main=True,
                          thin_archive=True)

  def test_distributed_machine_code_thin_archive_subdir(self):
    self.run_archive_test(bitcode_archive=False,
                          bitcode_main=False,
                          thin_archive=True)

  def test_distributed_bitcode_thick_archive_subdir(self):
    self.run_archive_test(bitcode_archive=True,
                          bitcode_main=True,
                          thin_archive=False)

  def test_distributed_machine_code_thick_archive_subdir(self):
    self.run_archive_test(bitcode_archive=False,
                          bitcode_main=False,
                          thin_archive=False)

  def test_distributed_machine_code_thick_archive_bitcode_main_subdir(self):
    self.run_archive_test(bitcode_archive=False,
                          bitcode_main=True,
                          thin_archive=False)

  def run_archive_test(self, bitcode_archive, bitcode_main, thin_archive):
    """
    Runs a test to ensure correct remote linking handling of
    an archive.
    Arguments:
    bitcode_archive: whether the archive should contain bitcode (true)
    or machine code (false)
    bitcode_main: whether the main object, outside the archive, should
    contain bitcode (true) or machine code (false)
    thin_archive: whether to create a thin archive instead of a regular archive.
    """
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      os.makedirs('obj')
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', 'main.cpp', '-o', 'obj/main.o'] +
          _lto_args(bitcode_main))
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', 'foo.cpp', '-o', 'obj/foo.o'] +
          _lto_args(bitcode_archive))
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', 'bar.cpp', '-o', 'obj/bar.o'] +
          _lto_args(bitcode_archive))
      archive_creation_arg = 'crs'
      if thin_archive:
        archive_creation_arg = 'crsT'
      subprocess.check_call([
          self.llvmar(), archive_creation_arg, 'obj/libfoobar.a', 'obj/bar.o',
          'obj/foo.o'
      ])
      rc = RemoteLinkUnixAllowMain().main([
          'remote_ld.py', '--ar-path',
          self.llvmar(), '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', 'obj/main.o',
          'obj/libfoobar.a', '-o', 'main'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      if bitcode_main or bitcode_archive:
        # build.ninja file should have rewrapper invocations in it.
        with open(os.path.join(d, 'lto.main', 'build.ninja')) as f:
          buildrules = f.read()
          self.assertIn('rewrapper ', buildrules)
          if bitcode_main:
            self.assertIn('build lto.main/obj/main.o.stamp : codegen ',
                          buildrules)
          if bitcode_archive:
            if thin_archive:
              self.assertIn('build lto.main/obj/foo.o.stamp : codegen ',
                            buildrules)
            else:
              self.assertIn(
                  'build lto.main/expanded_archives/obj/libfoobar.a/' +
                  'foo.o.stamp : codegen ', buildrules)
      # Check that main does not call foo.
      if bitcode_archive:
        disasm = subprocess.check_output(['llvm-objdump', '-d', 'main'])
        main_idx = disasm.index(b' <main>:\n')
        after_main_idx = disasm.index(b'\n\n', main_idx)
        main_disasm = disasm[main_idx:after_main_idx]
        self.assertNotIn(b'foo', main_disasm)

  def test_debug_params(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      os.makedirs('obj')
      subprocess.check_call([
          self.clangxx(), '-c', '-g', '-gsplit-dwarf', '-flto=thin', 'main.cpp',
          '-o', 'obj/main.o'
      ])
      subprocess.check_call([
          self.clangxx(), '-c', '-g', '-gsplit-dwarf', '-flto=thin', 'foo.cpp',
          '-o', 'obj/foo.o'
      ])
      with open('main.rsp', 'w') as f:
        f.write('obj/main.o\n' 'obj/foo.o\n')
      rc = RemoteLinkUnixAllowMain().main([
          'remote_ld.py', '--ar-path',
          self.llvmar(), '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', '-g', '-gsplit-dwarf',
          '-Wl,--lto-O2', '-o', 'main', '@main.rsp'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # Check debug info present, refers to .dwo file, and does not
      # contain full debug info for foo.cpp.
      dbginfo = subprocess.check_output(
          ['llvm-dwarfdump', '-debug-info',
           'main']).decode('utf-8', 'backslashreplace')
      self.assertRegexpMatches(dbginfo, '\\bDW_AT_GNU_dwo_name\\b.*\\.dwo"')
      self.assertNotRegexpMatches(dbginfo, '\\bDW_AT_name\\b.*foo\\.cpp"')

  def test_distributed_lto_params(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      os.makedirs('obj')
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', '-m32', '-fsplit-lto-unit',
          '-fwhole-program-vtables', 'main.cpp', '-o', 'obj/main.o'
      ])
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', '-m32', '-fsplit-lto-unit',
          '-fwhole-program-vtables', 'foo.cpp', '-o', 'obj/foo.o'
      ])
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', '-m32', '-fsplit-lto-unit',
          '-fwhole-program-vtables', 'bar.cpp', '-o', 'obj/bar.o'
      ])
      subprocess.check_call(
          [self.llvmar(), 'crsT', 'obj/libfoobar.a', 'obj/bar.o', 'obj/foo.o'])
      with open('main.rsp', 'w') as f:
        f.write('-fsplit-lto-unit\n'
                '-fwhole-program-vtables\n'
                'obj/main.o\n'
                'obj/libfoobar.a\n')
      rc = RemoteLinkUnixAllowMain().main([
          'remote_ld.py', '--ar-path',
          self.llvmar(), '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', '-m32', '-Wl,-mllvm',
          '-Wl,-generate-type-units', '-Wl,--lto-O2', '-o', 'main',
          '-Wl,--start-group', '@main.rsp', '-Wl,--end-group'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # Check codegen parameters.
      with open(os.path.join(d, 'lto.main', 'build.ninja')) as f:
        buildrules = f.read()
        codegen_match = re.search('^rule codegen\\b.*?^[^ ]', buildrules,
                                  re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(codegen_match)
        codegen_text = codegen_match.group(0)
        self.assertIn('rewrapper', codegen_text)
        self.assertIn('-m32', codegen_text)
        self.assertIn('-mllvm -generate-type-units', codegen_text)
        self.assertNotIn('-flto', codegen_text)
        self.assertIn('build lto.main/obj/main.o.stamp : codegen ', buildrules)
        self.assertIn('build lto.main/obj/foo.o.stamp : codegen ', buildrules)
        link_match = re.search('^build main : native-link\\b.*?^[^ ]',
                               buildrules, re.MULTILINE | re.DOTALL)
        self.assertIsNotNone(link_match)
        link_text = link_match.group(0)
        self.assertIn('main.split.o', link_text)
      # Check that main does not call foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main'])
      main_idx = disasm.index(b' <main>:\n')
      after_main_idx = disasm.index(b'\n\n', main_idx)
      main_disasm = disasm[main_idx:after_main_idx]
      self.assertNotIn(b'foo', main_disasm)

  def test_no_rewrapper(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', 'main.cpp', '-o', 'main.o'
      ])
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', '-flto=thin', 'foo.cpp', '-o', 'foo.o'])
      rc = RemoteLinkUnixAllowMain().main([
          'remote_ld.py', '--ar-path',
          self.llvmar(), '--no-wrapper', '-j', '16', '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', 'main.o', 'foo.o', '-o',
          'main'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # build.ninja file should not have rewrapper invocations in it.
      with open(os.path.join(d, 'lto.main', 'build.ninja')) as f:
        buildrules = f.read()
        self.assertNotIn('rewrapper ', buildrules)
        self.assertIn('build lto.main/main.o.stamp : codegen ', buildrules)
        self.assertIn('build lto.main/foo.o.stamp : codegen ', buildrules)
      # Check that main does not call foo.
      disasm = subprocess.check_output(['llvm-objdump', '-d', 'main'])
      main_idx = disasm.index(b' <main>:\n')
      after_main_idx = disasm.index(b'\n\n', main_idx)
      main_disasm = disasm[main_idx:after_main_idx]
      self.assertNotIn(b'foo', main_disasm)

  def test_generate_no_codegen(self):
    with named_directory() as d, working_directory(d):
      with open('main.o', 'wb') as f:
        f.write(b'\7fELF')
      with mock.patch('sys.stderr', new_callable=StringIO) as stderr:
        rc = RemoteLinkUnixAllowMain().main([
            'remote_ld.py', '--ar-path',
            self.llvmar(), '--generate', '--',
            self.clangxx(), 'main.o', '-o', 'main'
        ])
        self.assertEqual(rc, 5)
        self.assertIn('no ninja file generated.\n', stderr.getvalue())

  def test_generate(self):
    with named_directory() as d, working_directory(d):
      with open('main.o', 'wb') as f:
        f.write(b'BC\xc0\xde')
      with mock.patch('sys.stderr', new_callable=StringIO) as stderr:
        rc = RemoteLinkUnixAllowMain().main([
            'remote_ld.py', '--ar-path',
            self.llvmar(), '--generate', '--',
            self.clangxx(), 'main.o', '-o', 'main'
        ])
        self.assertEqual(rc, 0)
        m = re.search('ninja file (.*)', stderr.getvalue())
        self.assertIsNotNone(m)
        path = shlex.split(m.group(1))[0]
        self.assertTrue(os.path.exists(path))
        content = open(path).read()
        self.assertRegex(
            content,
            re.compile('^build [^:]+/main\\.o\\.stamp : codegen ',
                       re.MULTILINE))

  def test_override_allowlist(self):
    with named_directory() as d, working_directory(d):
      _create_inputs(d)
      subprocess.check_call([
          self.clangxx(), '-c', '-Os', '-flto=thin', 'main.cpp', '-o', 'main.o'
      ])
      subprocess.check_call(
          [self.clangxx(), '-c', '-Os', '-flto=thin', 'foo.cpp', '-o', 'foo.o'])
      rc = remote_ld.RemoteLinkUnix().main([
          'remote_ld.py', '--ar-path',
          self.llvmar(), '--generate', '--allowlist', '--',
          self.clangxx(), '-fuse-ld=lld', '-flto=thin', 'main.o', 'foo.o', '-o',
          'main'
      ])
      # Should succeed.
      self.assertEqual(rc, 0)
      # build.ninja file should have rules for main and foo.
      ninjafile = os.path.join(d, 'lto.main', 'build.ninja')
      self.assertTrue(os.path.exists(ninjafile))
      with open(ninjafile) as f:
        buildrules = f.read()
        self.assertIn('build lto.main/main.o.stamp : codegen ', buildrules)
        self.assertIn('build lto.main/foo.o.stamp : codegen ', buildrules)


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