| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2022 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import contextlib |
| import io |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| |
| from pathlib import Path |
| import unittest |
| from unittest import mock |
| |
| import cxx_remote_wrapper |
| |
| import cl_utils |
| import fuchsia |
| import cxx |
| import remote_action |
| |
| from typing import Any, Sequence |
| |
| |
| class ImmediateExit(Exception): |
| """For mocking functions that do not return.""" |
| |
| pass |
| |
| |
| def _strs(items: Sequence[Any]) -> Sequence[str]: |
| return [str(i) for i in items] |
| |
| |
| def _paths(items: Sequence[Any]) -> Sequence[Path]: |
| if isinstance(items, list): |
| return [Path(i) for i in items] |
| elif isinstance(items, set): |
| return {Path(i) for i in items} |
| elif isinstance(items, tuple): |
| return tuple(Path(i) for i in items) |
| |
| t = type(items) |
| raise TypeError(f"Unhandled sequence type: {t}") |
| |
| |
| class CheckMissingRemoteToolsTests(unittest.TestCase): |
| _PROJECT_ROOT = Path("..", "..", "fake", "project", "root") |
| |
| def test_clang_exists(self): |
| with mock.patch.object(Path, "is_dir", return_value=True): |
| self.assertEqual( |
| list( |
| cxx_remote_wrapper.check_missing_remote_tools( |
| cxx.Compiler.CLANG, self._PROJECT_ROOT |
| ) |
| ), |
| [], |
| ) |
| |
| def test_clang_not_exists(self): |
| with mock.patch.object(Path, "is_dir", return_value=False): |
| self.assertEqual( |
| list( |
| cxx_remote_wrapper.check_missing_remote_tools( |
| cxx.Compiler.CLANG, self._PROJECT_ROOT |
| ) |
| ), |
| [fuchsia.REMOTE_CLANG_SUBDIR], |
| ) |
| |
| def test_gcc_exists(self): |
| with mock.patch.object(Path, "is_dir", return_value=True): |
| self.assertEqual( |
| list( |
| cxx_remote_wrapper.check_missing_remote_tools( |
| cxx.Compiler.GCC, self._PROJECT_ROOT |
| ) |
| ), |
| [], |
| ) |
| |
| def test_gcc_not_exists(self): |
| with mock.patch.object(Path, "is_dir", return_value=False): |
| self.assertEqual( |
| list( |
| cxx_remote_wrapper.check_missing_remote_tools( |
| cxx.Compiler.GCC, self._PROJECT_ROOT |
| ) |
| ), |
| [fuchsia.REMOTE_GCC_SUBDIR], |
| ) |
| |
| |
| class CxxRemoteActionTests(unittest.TestCase): |
| def test_clang_cxx(self): |
| fake_root = Path("/home/project") |
| fake_builddir = Path("out/really-not-default") |
| fake_cwd = fake_root / fake_builddir |
| compiler = Path("clang++") |
| source = Path("hello.cc") |
| output = Path("hello.o") |
| command = _strs( |
| [ |
| compiler, |
| "--target=riscv64-apple-darwin21", |
| "-c", |
| source, |
| "-o", |
| output, |
| ] |
| ) |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| ["--"] + command, |
| exec_root=fake_root, |
| working_dir=fake_cwd, |
| host_platform=fuchsia.REMOTE_PLATFORM, # host = remote exec |
| auto_reproxy=False, |
| ) |
| self.assertFalse(c.verbose) |
| self.assertFalse(c.dry_run) |
| self.assertEqual(c.cxx_action.compiler.tool, compiler) |
| self.assertTrue(c.cxx_action.compiler_is_clang) |
| self.assertEqual(c.cxx_action.output_file, output) |
| self.assertEqual(c.cxx_action.target, "riscv64-apple-darwin21") |
| self.assertEqual(c.cpp_strategy, "integrated") |
| self.assertEqual(c.original_compile_command, command) |
| self.assertFalse(c.local_only) |
| |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| with mock.patch.object( |
| fuchsia, |
| "remote_clang_compiler_toolchain_inputs", |
| return_value=iter([]), |
| ) as mock_tc_inputs: |
| self.assertEqual(c.prepare(), 0) |
| self.assertEqual( |
| c.remote_action.inputs_relative_to_project_root, |
| [fake_builddir / source], |
| ) |
| with mock.patch.object( |
| cxx_remote_wrapper.CxxRemoteAction, |
| "_run_remote_action", |
| return_value=0, |
| ) as mock_call: |
| exit_code = c.run() |
| self.assertEqual(exit_code, 0) |
| mock_call.assert_called_once() |
| |
| def test_pp_asm_local_only(self): |
| compiler = Path("clang++") |
| source = Path("hello.S") |
| output = Path("hello.o") |
| exec_root = Path("/exec/root") |
| command = _strs( |
| [ |
| compiler, |
| "--target=riscv64-apple-darwin21", |
| "-c", |
| source, |
| "-o", |
| output, |
| ] |
| ) |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| ["--"] + command, |
| auto_reproxy=False, |
| exec_root=exec_root, |
| working_dir=exec_root / "work", |
| ) |
| self.assertTrue(c.local_only) |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| with mock.patch.object( |
| subprocess, "call", return_value=0 |
| ) as mock_call: |
| exit_code = c.run() |
| self.assertEqual(exit_code, 0) |
| mock_call.assert_called_with(command, cwd=c.working_dir) # ran locally |
| |
| def test_objc_local_only(self): |
| compiler = Path("clang++") |
| source = Path("hello.mm") |
| output = Path("hello.o") |
| exec_root = Path("/exec/root") |
| command = _strs( |
| [ |
| compiler, |
| "--target=riscv64-apple-darwin21", |
| "-c", |
| source, |
| "-o", |
| output, |
| ] |
| ) |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| ["--"] + command, |
| auto_reproxy=False, |
| exec_root=exec_root, |
| working_dir=exec_root / "work", |
| ) |
| self.assertTrue(c.local_only) |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| with mock.patch.object( |
| subprocess, "call", return_value=0 |
| ) as mock_call: |
| exit_code = c.run() |
| self.assertEqual(exit_code, 0) |
| mock_call.assert_called_with(command, cwd=c.working_dir) # ran locally |
| |
| def test_remote_action_paths(self): |
| fake_root = Path("/home/project") |
| fake_builddir = Path("out/not-default") |
| fake_cwd = fake_root / fake_builddir |
| compiler = Path("clang++") |
| source = Path("hello.cc") |
| output = Path("hello.o") |
| command = _strs( |
| [ |
| compiler, |
| "--target=riscv64-apple-darwin21", |
| "-c", |
| source, |
| "-o", |
| output, |
| ] |
| ) |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| with mock.patch.object( |
| fuchsia, |
| "remote_clang_compiler_toolchain_inputs", |
| return_value=iter([]), |
| ) as mock_tc_inputs: |
| with mock.patch.object(os, "curdir", fake_cwd): |
| with mock.patch.object( |
| remote_action, "PROJECT_ROOT", fake_root |
| ): |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| ["--"] + command, |
| host_platform=fuchsia.REMOTE_PLATFORM, # host = remote exec |
| auto_reproxy=False, |
| ) |
| self.assertEqual(c.prepare(), 0) |
| self.assertEqual(c.remote_action.exec_root, fake_root) |
| self.assertEqual( |
| c.remote_action.build_subdir, fake_builddir |
| ) |
| |
| def test_clang_crash_diagnostics_dir(self): |
| fake_root = Path("/usr/project") |
| fake_builddir = Path("build-it") |
| fake_cwd = fake_root / fake_builddir |
| crash_dir = Path("boom/b00m") |
| compiler = Path("clang++") |
| source = Path("hello.cc") |
| output = Path("hello.o") |
| command = _strs( |
| [ |
| compiler, |
| "--target=riscv64-apple-darwin21", |
| f"-fcrash-diagnostics-dir={crash_dir}", |
| "-c", |
| source, |
| "-o", |
| output, |
| ] |
| ) |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| with mock.patch.object(remote_action, "PROJECT_ROOT", fake_root): |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| ["--"] + command, |
| working_dir=fake_cwd, |
| host_platform=fuchsia.REMOTE_PLATFORM, # host = remote exec |
| auto_reproxy=False, |
| ) |
| with mock.patch.object( |
| fuchsia, |
| "remote_clang_compiler_toolchain_inputs", |
| return_value=iter([]), |
| ) as mock_tc_inputs: |
| self.assertEqual(c.prepare(), 0) |
| |
| self.assertTrue(c.cxx_action.compiler_is_clang) |
| self.assertEqual(c.remote_action.exec_root, fake_root) |
| self.assertEqual(c.remote_action.build_subdir, fake_builddir) |
| self.assertEqual( |
| c.remote_action.output_dirs_relative_to_working_dir, |
| [crash_dir], |
| ) |
| self.assertEqual( |
| c.remote_action.output_dirs_relative_to_project_root, |
| [fake_builddir / crash_dir], |
| ) |
| mock_tc_inputs.assert_called_once() |
| |
| def test_remote_flag_back_propagating(self): |
| compiler = Path("clang++") |
| source = Path("hello.cc") |
| output = Path("hello.o") |
| flag = "--foo-bar" |
| command = _strs( |
| [ |
| compiler, |
| "--target=riscv64-apple-darwin21", |
| f"--remote-flag={flag}", |
| "-c", |
| source, |
| "-o", |
| output, |
| ] |
| ) |
| filtered_command = _strs( |
| [ |
| compiler, |
| "--target=riscv64-apple-darwin21", |
| "-c", |
| source, |
| "-o", |
| output, |
| ] |
| ) |
| |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| ["--"] + command, |
| host_platform=fuchsia.REMOTE_PLATFORM, # host = remote exec |
| auto_reproxy=False, |
| ) |
| |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| with mock.patch.object( |
| fuchsia, |
| "remote_clang_compiler_toolchain_inputs", |
| return_value=iter([]), |
| ) as mock_tc_inputs: |
| self.assertEqual(c.prepare(), 0) |
| # check that rewrapper option sees --foo=bar |
| remote_action_command = c.remote_action.launch_command |
| prefix, sep, wrapped_command = cl_utils.partition_sequence( |
| remote_action_command, "--" |
| ) |
| self.assertIn(flag, prefix) |
| self.assertEqual(wrapped_command, filtered_command) |
| |
| def test_gcc_cxx(self): |
| fake_root = remote_action.PROJECT_ROOT |
| fake_builddir = Path("make-it-so") |
| fake_cwd = fake_root / fake_builddir |
| compiler = Path("g++") |
| source = Path("hello.cc") |
| output = Path("hello.o") |
| depfile = Path("hello.d") |
| command = _strs([compiler, "-MF", depfile, "-c", source, "-o", output]) |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| ["--"] + command, |
| working_dir=fake_cwd, |
| exec_root=fake_root, |
| host_platform=fuchsia.REMOTE_PLATFORM, # host = remote exec |
| auto_reproxy=False, |
| ) |
| self.assertFalse(c.verbose) |
| self.assertFalse(c.dry_run) |
| self.assertEqual(c.cxx_action.compiler.tool, compiler) |
| self.assertTrue(c.cxx_action.compiler_is_gcc) |
| self.assertEqual(c.cxx_action.output_file, output) |
| self.assertEqual(c.cpp_strategy, "integrated") |
| self.assertEqual(c.original_compile_command, command) |
| self.assertFalse(c.local_only) |
| self.assertEqual(c.depfile, depfile) |
| |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| with mock.patch.object( |
| fuchsia, "gcc_support_tools", return_value=iter([]) |
| ) as mock_tools: |
| self.assertEqual(c.prepare(), 0) |
| mock_tools.assert_called_with( |
| c.compiler_path, parser=True, assembler=True |
| ) |
| self.assertEqual( |
| c.remote_action.inputs_relative_to_project_root, |
| [fake_builddir / source], |
| ) |
| self.assertEqual( |
| c.remote_action.output_files_relative_to_project_root, |
| [fake_builddir / output, fake_builddir / depfile], |
| ) |
| self.assertEqual( |
| set(c.remote_action.expected_downloads), {output, depfile} |
| ) |
| |
| with mock.patch.object( |
| remote_action.RemoteAction, |
| "_run_maybe_remotely", |
| return_value=cl_utils.SubprocessResult(0), |
| ) as mock_remote: |
| exit_code = c.run() |
| |
| self.assertEqual(exit_code, 0) |
| |
| def test_rewrite_remote_depfile(self): |
| compiler = Path("ppc-macho-g++") |
| source = Path("hello.cc") |
| output = Path("hello.o") |
| depfile = Path("hello.d") |
| with tempfile.TemporaryDirectory() as td: |
| fake_root = Path(td) |
| fake_builddir = Path("make-it-so") |
| fake_cwd = fake_root / fake_builddir |
| command = _strs( |
| [compiler, "-MF", depfile, "-c", source, "-o", output] |
| ) |
| c = cxx_remote_wrapper.CxxRemoteAction( |
| # For this test, make the remote/local working dirs match |
| ["--canonicalize_working_dir=false", "--"] + command, |
| working_dir=fake_cwd, |
| exec_root=fake_root, |
| host_platform=fuchsia.REMOTE_PLATFORM, # host = remote exec |
| auto_reproxy=False, |
| ) |
| |
| remote_cwd = remote_action._REMOTE_PROJECT_ROOT / fake_builddir |
| fake_cwd.mkdir(parents=True, exist_ok=True) |
| (fake_cwd / depfile).write_text( |
| f"{remote_cwd}/lib/bar.a: {remote_cwd}/obj/foo.o\n", |
| ) |
| |
| with mock.patch.object( |
| cxx_remote_wrapper, "check_missing_remote_tools" |
| ) as mock_check: |
| # create the remote action |
| self.assertEqual(c.prepare(), 0) |
| |
| mock_check.assert_called_once() |
| self.assertEqual( |
| set(c.remote_action.expected_downloads), {output, depfile} |
| ) |
| self.assertEqual(c.remote_action.remote_working_dir, remote_cwd) |
| c._rewrite_remote_depfile() |
| new_depfile = (fake_cwd / depfile).read_text() |
| self.assertEqual(new_depfile, "lib/bar.a: obj/foo.o\n") |
| |
| |
| class MainTests(unittest.TestCase): |
| def test_help_implicit(self): |
| # Just make sure help exits successfully, without any exceptions |
| # due to argument parsing. |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| sys, "exit", side_effect=ImmediateExit |
| ) as mock_exit: |
| with self.assertRaises(ImmediateExit): |
| cxx_remote_wrapper.main([]) |
| mock_exit.assert_called_with(0) |
| |
| def test_help_flag(self): |
| # Just make sure help exits successfully, without any exceptions |
| # due to argument parsing. |
| stdout = io.StringIO() |
| with contextlib.redirect_stdout(stdout): |
| with mock.patch.object( |
| sys, "exit", side_effect=ImmediateExit |
| ) as mock_exit: |
| with self.assertRaises(ImmediateExit): |
| cxx_remote_wrapper.main(["--help"]) |
| mock_exit.assert_called_with(0) |
| |
| def test_local_mode_forced(self): |
| exit_code = 24 |
| with mock.patch.object( |
| remote_action, "auto_relaunch_with_reproxy" |
| ) as mock_relaunch: |
| with mock.patch.object( |
| cxx_remote_wrapper.CxxRemoteAction, |
| "_run_locally", |
| return_value=exit_code, |
| ) as mock_run: |
| self.assertEqual( |
| cxx_remote_wrapper.main( |
| [ |
| "--local", |
| "--", |
| "clang++", |
| "-c", |
| "foo.cc", |
| "-o", |
| "foo.o", |
| ] |
| ), |
| exit_code, |
| ) |
| mock_relaunch.assert_called_once() |
| mock_run.assert_called_with() |
| |
| def test_auto_relaunched_with_reproxy(self): |
| argv = ["--", "clang++", "-c", "foo.cc", "-o", "foo.o"] |
| with mock.patch.object( |
| os.environ, "get", return_value=None |
| ) as mock_env: |
| with mock.patch.object( |
| cl_utils, "exec_relaunch", side_effect=ImmediateExit |
| ) as mock_relaunch: |
| with self.assertRaises(ImmediateExit): |
| cxx_remote_wrapper.main(argv) |
| mock_env.assert_called() |
| mock_relaunch.assert_called_once() |
| args, kwargs = mock_relaunch.call_args_list[0] |
| relaunch_cmd = args[0] |
| self.assertEqual(relaunch_cmd[0], str(fuchsia.REPROXY_WRAP)) |
| cmd_slices = cl_utils.split_into_subsequences(relaunch_cmd[1:], "--") |
| reproxy_args, self_script, wrapped_command = cmd_slices |
| self.assertEqual(reproxy_args, ["-v"]) |
| self.assertIn("python", self_script[0]) |
| self.assertTrue(self_script[-1].endswith("cxx_remote_wrapper.py")) |
| self.assertEqual(wrapped_command, argv[1:]) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |